feat(abaqus-subset-completion): step 2 — single-instance-semantic-validation
This commit is contained in:
@@ -36,6 +36,7 @@ add_library(fesa_core STATIC
|
||||
src/fesa/fem/dof_manager.cpp
|
||||
src/fesa/fem/gauss_rule.cpp
|
||||
src/fesa/fem/line2_shape.cpp
|
||||
src/fesa/io/abaqus/active_input.cpp
|
||||
src/fesa/io/abaqus/parser.cpp
|
||||
src/fesa/io/abaqus/semantic_mapper.cpp
|
||||
src/fesa/io/abaqus/set_resolver.cpp
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <fesa/core/diagnostic.hpp>
|
||||
#include <fesa/io/abaqus/deck_record.hpp>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
struct ActiveInputView final {
|
||||
bool flat = true;
|
||||
std::string part_name;
|
||||
std::string instance_name;
|
||||
std::span<const DeckRecord> part_records;
|
||||
std::span<const DeckRecord> assembly_records;
|
||||
};
|
||||
|
||||
struct ActiveInputResult final {
|
||||
std::optional<ActiveInputView> input;
|
||||
std::vector<Diagnostic> diagnostics;
|
||||
};
|
||||
|
||||
[[nodiscard]] ActiveInputResult select_active_input(const ParsedDeck& deck);
|
||||
|
||||
} // namespace fesa
|
||||
@@ -28,6 +28,7 @@ struct ParsedInstance final {
|
||||
std::string name;
|
||||
std::string part_name;
|
||||
std::vector<std::vector<std::string>> transform_data;
|
||||
std::vector<SourceLocation> transform_sources;
|
||||
SourceLocation source;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
#include <fesa/io/abaqus/active_input.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
const std::string* parameter(
|
||||
const DeckRecord& record,
|
||||
const std::string_view name) {
|
||||
const auto found = record.parameters.find(name);
|
||||
return found == record.parameters.end() ? nullptr : &found->second;
|
||||
}
|
||||
|
||||
bool is_mesh_record(const DeckRecord& record) {
|
||||
return record.keyword == "NODE" || record.keyword == "ELEMENT";
|
||||
}
|
||||
|
||||
ActiveInputResult failure(
|
||||
std::string code,
|
||||
std::string message,
|
||||
const SourceLocation& source) {
|
||||
return {
|
||||
std::nullopt,
|
||||
{{
|
||||
DiagnosticStage::semantic,
|
||||
Severity::error,
|
||||
std::move(code),
|
||||
std::move(message),
|
||||
source,
|
||||
}},
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ActiveInputResult select_active_input(const ParsedDeck& deck) {
|
||||
const bool hierarchical =
|
||||
!deck.parts.empty() || deck.assembly.has_value();
|
||||
if (!hierarchical) {
|
||||
return {
|
||||
ActiveInputView{
|
||||
true,
|
||||
{},
|
||||
{},
|
||||
deck.global_records,
|
||||
{},
|
||||
},
|
||||
{},
|
||||
};
|
||||
}
|
||||
|
||||
const auto flat_mesh =
|
||||
std::ranges::find_if(deck.global_records, is_mesh_record);
|
||||
if (flat_mesh != deck.global_records.end()) {
|
||||
return failure(
|
||||
"abaqus.semantic.mixed_mesh_organization",
|
||||
"Flat mesh records cannot be mixed with Part/Assembly input.",
|
||||
flat_mesh->source);
|
||||
}
|
||||
|
||||
if (!deck.assembly.has_value()) {
|
||||
const SourceLocation source =
|
||||
deck.parts.empty() ? SourceLocation{} : deck.parts.front().source;
|
||||
return failure(
|
||||
"abaqus.semantic.assembly_count",
|
||||
"Hierarchical Phase 1 input requires exactly one Assembly.",
|
||||
source);
|
||||
}
|
||||
|
||||
const ParsedAssembly& assembly = *deck.assembly;
|
||||
if (assembly.instances.size() != 1U) {
|
||||
const SourceLocation& source = assembly.instances.size() > 1U
|
||||
? assembly.instances[1].source
|
||||
: assembly.source;
|
||||
return failure(
|
||||
"abaqus.semantic.instance_count",
|
||||
"Phase 1 requires exactly one Instance.",
|
||||
source);
|
||||
}
|
||||
|
||||
const ParsedInstance& instance = assembly.instances.front();
|
||||
if (!instance.transform_data.empty()) {
|
||||
const SourceLocation& source = instance.transform_sources.empty()
|
||||
? instance.source
|
||||
: instance.transform_sources.front();
|
||||
return failure(
|
||||
"abaqus.semantic.instance_transform",
|
||||
"Instance translation and rotation data are unsupported.",
|
||||
source);
|
||||
}
|
||||
|
||||
const auto part =
|
||||
std::ranges::find(deck.parts, instance.part_name, &ParsedPart::name);
|
||||
if (part == deck.parts.end()) {
|
||||
return failure(
|
||||
"abaqus.semantic.missing_part",
|
||||
"Instance '" + instance.name + "' references missing Part '" +
|
||||
instance.part_name + "'.",
|
||||
instance.source);
|
||||
}
|
||||
|
||||
for (const DeckRecord& record : assembly.records) {
|
||||
if (record.keyword != "NSET" && record.keyword != "ELSET") {
|
||||
continue;
|
||||
}
|
||||
const std::string* record_instance = parameter(record, "INSTANCE");
|
||||
if (record_instance == nullptr || *record_instance != instance.name) {
|
||||
const std::string* name = parameter(
|
||||
record, record.keyword == "NSET" ? "NSET" : "ELSET");
|
||||
return failure(
|
||||
"abaqus.semantic.wrong_instance",
|
||||
"Assembly set '" +
|
||||
(name == nullptr ? std::string{} : *name) +
|
||||
"' must reference the active Instance.",
|
||||
record.source);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ActiveInputView{
|
||||
false,
|
||||
part->name,
|
||||
instance.name,
|
||||
part->records,
|
||||
assembly.records,
|
||||
},
|
||||
{},
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
@@ -214,6 +214,11 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
const std::vector<std::string> fields = split_fields(content);
|
||||
if (scope == Scope::instance) {
|
||||
current_instance->transform_data.push_back(fields);
|
||||
current_instance->transform_sources.push_back(SourceLocation{
|
||||
path,
|
||||
line_number,
|
||||
first_nonspace + 1U,
|
||||
});
|
||||
} else if (current_record != nullptr) {
|
||||
current_record->data.push_back(fields);
|
||||
current_record->data_sources.push_back(SourceLocation{
|
||||
@@ -327,7 +332,7 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
return missing_parameter(keyword, "PART");
|
||||
}
|
||||
current_instance =
|
||||
ParsedInstance{*name, *part_name, {}, source};
|
||||
ParsedInstance{*name, *part_name, {}, {}, source};
|
||||
scope = Scope::instance;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include <fesa/io/abaqus/semantic_mapper.hpp>
|
||||
|
||||
#include <fesa/io/abaqus/active_input.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
@@ -26,24 +28,26 @@ const std::string* parameter(
|
||||
return found == record.parameters.end() ? nullptr : &found->second;
|
||||
}
|
||||
|
||||
bool is_mesh_record(const DeckRecord& record) {
|
||||
return record.keyword == "NODE" || record.keyword == "ELEMENT";
|
||||
}
|
||||
|
||||
class DeckMapper final {
|
||||
public:
|
||||
explicit DeckMapper(const ParsedDeck& deck) : deck_{deck} {}
|
||||
|
||||
[[nodiscard]] DomainBuildResult map() {
|
||||
if (!select_active_scope()) {
|
||||
ActiveInputResult selected = select_active_input(deck_);
|
||||
if (!selected.input.has_value()) {
|
||||
diagnostics_ = std::move(selected.diagnostics);
|
||||
return failure();
|
||||
}
|
||||
active_records_ = selected.input->part_records;
|
||||
assembly_records_ = selected.input->assembly_records;
|
||||
part_name_ = std::move(selected.input->part_name);
|
||||
instance_name_ = std::move(selected.input->instance_name);
|
||||
|
||||
collect_materials();
|
||||
collect_nodes();
|
||||
collect_raw_sets(*active_records_, false);
|
||||
if (active_assembly_ != nullptr) {
|
||||
collect_raw_sets(active_assembly_->records, true);
|
||||
collect_raw_sets(active_records_, false);
|
||||
if (!assembly_records_.empty()) {
|
||||
collect_raw_sets(assembly_records_, true);
|
||||
}
|
||||
collect_sections();
|
||||
collect_elements();
|
||||
@@ -153,71 +157,6 @@ private:
|
||||
return value;
|
||||
}
|
||||
|
||||
bool select_active_scope() {
|
||||
const bool has_hierarchical_input =
|
||||
!deck_.parts.empty() || deck_.assembly.has_value();
|
||||
if (!has_hierarchical_input) {
|
||||
active_records_ = &deck_.global_records;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std::ranges::any_of(deck_.global_records, is_mesh_record)) {
|
||||
add_error(
|
||||
"abaqus.semantic.mixed_mesh_organization",
|
||||
"Flat mesh records cannot be mixed with Part/Assembly input.",
|
||||
std::ranges::find_if(
|
||||
deck_.global_records, is_mesh_record)->source);
|
||||
return false;
|
||||
}
|
||||
if (!deck_.assembly.has_value()) {
|
||||
const SourceLocation source =
|
||||
deck_.parts.empty() ? SourceLocation{} : deck_.parts[0].source;
|
||||
add_error(
|
||||
"abaqus.semantic.assembly_count",
|
||||
"Hierarchical Phase 1 input requires exactly one Assembly.",
|
||||
source);
|
||||
return false;
|
||||
}
|
||||
|
||||
active_assembly_ = &*deck_.assembly;
|
||||
if (active_assembly_->instances.size() != 1U) {
|
||||
const SourceLocation& source =
|
||||
active_assembly_->instances.size() > 1U
|
||||
? active_assembly_->instances[1].source
|
||||
: active_assembly_->source;
|
||||
add_error(
|
||||
"abaqus.semantic.instance_count",
|
||||
"Phase 1 requires exactly one Instance.",
|
||||
source);
|
||||
return false;
|
||||
}
|
||||
|
||||
const ParsedInstance& instance = active_assembly_->instances.front();
|
||||
if (!instance.transform_data.empty()) {
|
||||
add_error(
|
||||
"abaqus.semantic.instance_transform",
|
||||
"Instance translation and rotation data are unsupported.",
|
||||
instance.source);
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto part = std::ranges::find(
|
||||
deck_.parts, instance.part_name, &ParsedPart::name);
|
||||
if (part == deck_.parts.end()) {
|
||||
add_error(
|
||||
"abaqus.semantic.missing_part",
|
||||
"Instance '" + instance.name + "' references missing Part '" +
|
||||
instance.part_name + "'.",
|
||||
instance.source);
|
||||
return false;
|
||||
}
|
||||
|
||||
active_records_ = &part->records;
|
||||
part_name_ = part->name;
|
||||
instance_name_ = instance.name;
|
||||
return true;
|
||||
}
|
||||
|
||||
void collect_materials() {
|
||||
const std::string* current_name = nullptr;
|
||||
for (const DeckRecord& record : deck_.global_records) {
|
||||
@@ -264,7 +203,7 @@ private:
|
||||
}
|
||||
|
||||
void collect_nodes() {
|
||||
for (const DeckRecord& record : *active_records_) {
|
||||
for (const DeckRecord& record : active_records_) {
|
||||
if (record.keyword != "NODE") {
|
||||
continue;
|
||||
}
|
||||
@@ -298,7 +237,7 @@ private:
|
||||
}
|
||||
|
||||
void collect_raw_sets(
|
||||
const std::vector<DeckRecord>& records,
|
||||
const std::span<const DeckRecord> records,
|
||||
const bool assembly_scope) {
|
||||
for (const DeckRecord& record : records) {
|
||||
const bool is_node_set = record.keyword == "NSET";
|
||||
@@ -343,7 +282,7 @@ private:
|
||||
}
|
||||
|
||||
void collect_sections() {
|
||||
for (const DeckRecord& record : *active_records_) {
|
||||
for (const DeckRecord& record : active_records_) {
|
||||
if (record.keyword != "BEAM GENERAL SECTION") {
|
||||
continue;
|
||||
}
|
||||
@@ -422,7 +361,7 @@ private:
|
||||
}
|
||||
|
||||
void collect_elements() {
|
||||
for (const DeckRecord& record : *active_records_) {
|
||||
for (const DeckRecord& record : active_records_) {
|
||||
if (record.keyword != "ELEMENT") {
|
||||
continue;
|
||||
}
|
||||
@@ -661,8 +600,8 @@ private:
|
||||
const ParsedDeck& deck_;
|
||||
DomainBuilder builder_;
|
||||
std::vector<Diagnostic> diagnostics_;
|
||||
const std::vector<DeckRecord>* active_records_ = nullptr;
|
||||
const ParsedAssembly* active_assembly_ = nullptr;
|
||||
std::span<const DeckRecord> active_records_;
|
||||
std::span<const DeckRecord> assembly_records_;
|
||||
std::string part_name_;
|
||||
std::string instance_name_;
|
||||
LabelMap node_ids_;
|
||||
|
||||
+14
-1
@@ -118,6 +118,7 @@ add_test(
|
||||
)
|
||||
|
||||
add_executable(fesa_abaqus_parser_tests
|
||||
unit/io/abaqus/active_input_test.cpp
|
||||
unit/io/abaqus/input_contract_test.cpp
|
||||
unit/io/abaqus/parser_test.cpp
|
||||
unit/io/abaqus/set_resolution_test.cpp
|
||||
@@ -175,6 +176,18 @@ add_test(
|
||||
--gtest_filter=AssemblySet.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME ActiveInput
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=ActiveInput.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME SemanticScope
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=SemanticScope.*
|
||||
)
|
||||
|
||||
add_executable(fesa_deck_to_domain_tests
|
||||
integration/io/minimal_deck_to_domain_test.cpp
|
||||
)
|
||||
@@ -200,7 +213,7 @@ add_test(
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME ActiveInstance
|
||||
NAME SingleInstance
|
||||
COMMAND "$<TARGET_FILE:fesa_deck_to_domain_tests>"
|
||||
--gtest_filter=ActiveInstance.*
|
||||
)
|
||||
|
||||
@@ -219,7 +219,7 @@ TEST(ActiveInstance, RejectsInstanceTransformWithSourceDiagnostic) {
|
||||
ASSERT_TRUE(has_diagnostic(result, "abaqus.semantic.instance_transform"));
|
||||
ASSERT_FALSE(result.diagnostics.empty());
|
||||
ASSERT_TRUE(result.diagnostics.front().source.has_value());
|
||||
EXPECT_EQ(result.diagnostics.front().source->line, 4U);
|
||||
EXPECT_EQ(result.diagnostics.front().source->line, 5U);
|
||||
}
|
||||
|
||||
TEST(ActiveInstance, RejectsMissingPartReferenceWithSourceDiagnostic) {
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
#include <fesa/io/abaqus/active_input.hpp>
|
||||
#include <fesa/io/abaqus/parser.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace {
|
||||
|
||||
class TemporaryDeck final {
|
||||
public:
|
||||
TemporaryDeck(std::string_view name, std::string_view contents)
|
||||
: path_{std::filesystem::path{testing::TempDir()} / name} {
|
||||
std::ofstream output{path_, std::ios::binary};
|
||||
output.write(
|
||||
contents.data(), static_cast<std::streamsize>(contents.size()));
|
||||
if (!output) {
|
||||
throw std::runtime_error{"Failed to write temporary Abaqus deck."};
|
||||
}
|
||||
}
|
||||
|
||||
~TemporaryDeck() {
|
||||
std::error_code error;
|
||||
std::filesystem::remove(path_, error);
|
||||
}
|
||||
|
||||
TemporaryDeck(const TemporaryDeck&) = delete;
|
||||
TemporaryDeck& operator=(const TemporaryDeck&) = delete;
|
||||
|
||||
[[nodiscard]] const std::filesystem::path& path() const noexcept {
|
||||
return path_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::filesystem::path path_;
|
||||
};
|
||||
|
||||
std::filesystem::path fixture_path(std::string_view name) {
|
||||
return std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" /
|
||||
"abaqus" / name;
|
||||
}
|
||||
|
||||
fesa::ParsedDeck parse(const std::filesystem::path& path) {
|
||||
auto parsed = fesa::parse_deck(path);
|
||||
if (!parsed.deck.has_value()) {
|
||||
throw std::runtime_error{"Test deck did not parse."};
|
||||
}
|
||||
return std::move(*parsed.deck);
|
||||
}
|
||||
|
||||
fesa::Diagnostic expect_failure(
|
||||
const fesa::ParsedDeck& deck,
|
||||
const std::string_view code,
|
||||
const std::size_t line) {
|
||||
const fesa::ActiveInputResult result = fesa::select_active_input(deck);
|
||||
EXPECT_FALSE(result.input.has_value());
|
||||
const auto diagnostic = std::ranges::find(
|
||||
result.diagnostics, code, &fesa::Diagnostic::code);
|
||||
if (diagnostic == result.diagnostics.end()) {
|
||||
throw std::runtime_error{"Expected active-input diagnostic was not found."};
|
||||
}
|
||||
EXPECT_EQ(diagnostic->stage, fesa::DiagnosticStage::semantic);
|
||||
EXPECT_TRUE(diagnostic->source.has_value());
|
||||
if (diagnostic->source.has_value()) {
|
||||
EXPECT_EQ(diagnostic->source->line, line);
|
||||
}
|
||||
return *diagnostic;
|
||||
}
|
||||
|
||||
TEST(ActiveInput, SelectsFlatGlobalRecords) {
|
||||
const fesa::ParsedDeck deck =
|
||||
parse(fixture_path("minimal_cantilever.inp"));
|
||||
|
||||
const fesa::ActiveInputResult result = fesa::select_active_input(deck);
|
||||
|
||||
ASSERT_TRUE(result.input.has_value());
|
||||
EXPECT_TRUE(result.diagnostics.empty());
|
||||
EXPECT_TRUE(result.input->flat);
|
||||
EXPECT_TRUE(result.input->part_name.empty());
|
||||
EXPECT_TRUE(result.input->instance_name.empty());
|
||||
EXPECT_EQ(result.input->part_records.data(), deck.global_records.data());
|
||||
EXPECT_EQ(result.input->part_records.size(), deck.global_records.size());
|
||||
EXPECT_TRUE(result.input->assembly_records.empty());
|
||||
}
|
||||
|
||||
TEST(ActiveInput, SelectsOnlyTheReferencedPartAndAssemblyRecords) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-active-input-selection.inp",
|
||||
"*PART, NAME=Unused\n"
|
||||
"*NODE\n"
|
||||
"99, 9.0, 0.0, 0.0\n"
|
||||
"*END PART\n"
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*NODE\n"
|
||||
"1, 0.0, 0.0, 0.0\n"
|
||||
"*END PART\n"
|
||||
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
||||
"*END INSTANCE\n"
|
||||
"*NSET, NSET=Fixed, INSTANCE=Beam-1\n"
|
||||
"1\n"
|
||||
"*END ASSEMBLY\n"};
|
||||
const fesa::ParsedDeck deck = parse(input.path());
|
||||
|
||||
const fesa::ActiveInputResult result = fesa::select_active_input(deck);
|
||||
|
||||
ASSERT_TRUE(result.input.has_value());
|
||||
EXPECT_FALSE(result.input->flat);
|
||||
EXPECT_EQ(result.input->part_name, "BeamPart");
|
||||
EXPECT_EQ(result.input->instance_name, "Beam-1");
|
||||
ASSERT_EQ(result.input->part_records.size(), 1U);
|
||||
EXPECT_EQ(result.input->part_records[0].keyword, "NODE");
|
||||
ASSERT_EQ(result.input->assembly_records.size(), 1U);
|
||||
EXPECT_EQ(result.input->assembly_records[0].keyword, "NSET");
|
||||
}
|
||||
|
||||
TEST(ActiveInput, RejectsMixedFlatAndHierarchicalMeshAtTheFlatRecord) {
|
||||
const fesa::ParsedDeck deck =
|
||||
parse(fixture_path("invalid/mixed_mesh.inp"));
|
||||
|
||||
const fesa::Diagnostic diagnostic = expect_failure(
|
||||
deck, "abaqus.semantic.mixed_mesh_organization", 1U);
|
||||
|
||||
EXPECT_EQ(diagnostic.source->file, fixture_path("invalid/mixed_mesh.inp"));
|
||||
}
|
||||
|
||||
TEST(ActiveInput, RejectsHierarchicalInputWithoutAnAssembly) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-missing-assembly.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*END PART\n"};
|
||||
const fesa::ParsedDeck deck = parse(input.path());
|
||||
|
||||
expect_failure(deck, "abaqus.semantic.assembly_count", 1U);
|
||||
}
|
||||
|
||||
TEST(ActiveInput, RejectsAnAssemblyWithoutExactlyOneInstance) {
|
||||
const TemporaryDeck no_instance{
|
||||
"fesa-no-instance.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*END PART\n"
|
||||
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||
"*END ASSEMBLY\n"};
|
||||
const TemporaryDeck multiple_instances{
|
||||
"fesa-multiple-instances.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*END PART\n"
|
||||
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
||||
"*END INSTANCE\n"
|
||||
"*INSTANCE, NAME=Beam-2, PART=BeamPart\n"
|
||||
"*END INSTANCE\n"
|
||||
"*END ASSEMBLY\n"};
|
||||
|
||||
expect_failure(
|
||||
parse(no_instance.path()), "abaqus.semantic.instance_count", 3U);
|
||||
expect_failure(
|
||||
parse(multiple_instances.path()),
|
||||
"abaqus.semantic.instance_count",
|
||||
6U);
|
||||
}
|
||||
|
||||
TEST(ActiveInput, RejectsTransformAtTheFirstTransformDataRow) {
|
||||
const fesa::ParsedDeck deck =
|
||||
parse(fixture_path("invalid/instance_transform.inp"));
|
||||
|
||||
expect_failure(deck, "abaqus.semantic.instance_transform", 5U);
|
||||
}
|
||||
|
||||
TEST(ActiveInput, RejectsAnInstanceThatReferencesAMissingPart) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-missing-part.inp",
|
||||
"*PART, NAME=OtherPart\n"
|
||||
"*END PART\n"
|
||||
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||
"*INSTANCE, NAME=Beam-1, PART=MissingPart\n"
|
||||
"*END INSTANCE\n"
|
||||
"*END ASSEMBLY\n"};
|
||||
const fesa::ParsedDeck deck = parse(input.path());
|
||||
|
||||
expect_failure(deck, "abaqus.semantic.missing_part", 4U);
|
||||
}
|
||||
|
||||
TEST(ActiveInput, RejectsAssemblySetsForAnotherInstance) {
|
||||
const fesa::ParsedDeck deck =
|
||||
parse(fixture_path("invalid/assembly_set_wrong_instance.inp"));
|
||||
|
||||
expect_failure(deck, "abaqus.semantic.wrong_instance", 6U);
|
||||
}
|
||||
|
||||
TEST(SemanticScope, RejectsInstanceLocalMeshAtTheKeywordRow) {
|
||||
const auto path = fixture_path("invalid/instance_local_mesh.inp");
|
||||
|
||||
const fesa::ParseDeckResult result = fesa::parse_deck(path);
|
||||
|
||||
EXPECT_FALSE(result.deck.has_value());
|
||||
ASSERT_EQ(result.diagnostics.size(), 1U);
|
||||
EXPECT_EQ(result.diagnostics[0].stage, fesa::DiagnosticStage::syntax);
|
||||
EXPECT_EQ(
|
||||
result.diagnostics[0].code,
|
||||
"abaqus.syntax.instance_local_keyword");
|
||||
ASSERT_TRUE(result.diagnostics[0].source.has_value());
|
||||
EXPECT_EQ(result.diagnostics[0].source->file, path);
|
||||
EXPECT_EQ(result.diagnostics[0].source->line, 5U);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user