#include #include #include #include #include #include #include #include #include #include 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(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, RejectsGlobalSetsInHierarchicalInput) { const TemporaryDeck input{ "fesa-global-set-with-hierarchy.inp", "*NSET, NSET=Ignored\n" "*PART, NAME=BeamPart\n" "*END PART\n" "*ASSEMBLY, NAME=RootAssembly\n" "*INSTANCE, NAME=Beam-1, PART=BeamPart\n" "*END INSTANCE\n" "*END ASSEMBLY\n"}; expect_failure( parse(input.path()), "abaqus.semantic.mixed_mesh_organization", 1U); } TEST(ActiveInput, RejectsDuplicatePartNamesAtTheSecondDefinition) { const TemporaryDeck input{ "fesa-duplicate-parts.inp", "*PART, NAME=BeamPart\n" "*END PART\n" "*PART, NAME=BeamPart\n" "*END PART\n" "*ASSEMBLY, NAME=RootAssembly\n" "*INSTANCE, NAME=Beam-1, PART=BeamPart\n" "*END INSTANCE\n" "*END ASSEMBLY\n"}; expect_failure( parse(input.path()), "abaqus.semantic.duplicate_part", 3U); } 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