#include #include #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; } const fesa::DeckRecord& record( const std::vector& records, const std::string_view keyword) { const auto found = std::ranges::find( records, keyword, &fesa::DeckRecord::keyword); if (found == records.end()) { throw std::runtime_error{"Expected deck record was not parsed."}; } return *found; } TEST(AbaqusParser, ParsesCaseInsensitiveKeywordsCommentsAndCommaFields) { const auto path = fixture_path("minimal_cantilever.inp"); const auto result = fesa::parse_deck(path); ASSERT_TRUE(result.deck.has_value()); EXPECT_TRUE(result.diagnostics.empty()); EXPECT_EQ( result.input_fingerprint, "fnv1a64:73f31da4615f09b3"); EXPECT_TRUE(result.deck->parts.empty()); EXPECT_FALSE(result.deck->assembly.has_value()); const auto& nodes = record(result.deck->global_records, "NODE"); ASSERT_EQ(nodes.data.size(), 2U); EXPECT_EQ(nodes.data[0], (std::vector{ "1", "0.0", "0.0", "0.0"})); EXPECT_EQ(nodes.data[1], (std::vector{ "2", "1.0", "0.0", "0.0"})); EXPECT_EQ(nodes.source.file, path); EXPECT_EQ(nodes.source.line, 3U); EXPECT_EQ(nodes.source.column, 1U); const auto& element = record(result.deck->global_records, "ELEMENT"); EXPECT_EQ(element.parameters.at("TYPE"), "B31"); EXPECT_EQ(element.parameters.at("ELSET"), "Beam"); EXPECT_EQ(element.source.line, 8U); } TEST(AbaqusParser, PreservesUtf8ParameterValues) { const std::string utf8_name{ "\xEB\xB9\x94\xEB\xB6\x80\xED\x92\x88"}; const TemporaryDeck input{ "fesa-parser-utf8.inp", "*MATERIAL, NAME=" + utf8_name + "\n" "*ELASTIC\n" "210000.0, 0.3\n"}; const auto result = fesa::parse_deck(input.path()); ASSERT_TRUE(result.deck.has_value()); ASSERT_TRUE(result.diagnostics.empty()); const auto& material = record(result.deck->global_records, "MATERIAL"); EXPECT_EQ(material.parameters.at("NAME"), utf8_name); } TEST(AbaqusParser, RejectsUnsupportedKeywordsInsteadOfIgnoringThem) { const TemporaryDeck input{ "fesa-parser-unsupported.inp", "** The error source must identify the keyword line.\n" "*INCLUDE, INPUT=other.inp\n"}; const auto result = fesa::parse_deck(input.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.unsupported_keyword"); ASSERT_TRUE(result.diagnostics[0].source.has_value()); EXPECT_EQ(result.diagnostics[0].source->file, input.path()); EXPECT_EQ(result.diagnostics[0].source->line, 2U); } TEST(AbaqusParser, RejectsUnsupportedParametersOnSupportedKeywords) { struct Case final { std::string_view name; std::string_view contents; std::size_t line; }; const Case cases[]{ {"node", "*NODE, EXTRA=1\n1, 0.0, 0.0, 0.0\n", 1U}, {"element", "*ELEMENT, TYPE=B31, EXTRA=1\n1, 1, 2\n", 1U}, {"part", "*PART, NAME=P, EXTRA=1\n*END PART\n", 1U}, {"end-part", "*PART, NAME=P\n*END PART, EXTRA=1\n", 2U}, { "assembly", "*ASSEMBLY, NAME=A, EXTRA=1\n*END ASSEMBLY\n", 1U, }, { "end-assembly", "*ASSEMBLY, NAME=A\n*END ASSEMBLY, EXTRA=1\n", 2U, }, { "instance", "*ASSEMBLY, NAME=A\n" "*INSTANCE, NAME=I, PART=P, EXTRA=1\n" "*END INSTANCE\n" "*END ASSEMBLY\n", 2U, }, { "end-instance", "*ASSEMBLY, NAME=A\n" "*INSTANCE, NAME=I, PART=P\n" "*END INSTANCE, EXTRA=1\n" "*END ASSEMBLY\n", 3U, }, {"nset", "*NSET, NSET=S, EXTRA=1\n1\n", 1U}, {"elset", "*ELSET, ELSET=S, EXTRA=1\n1\n", 1U}, {"material", "*MATERIAL, NAME=M, EXTRA=1\n", 1U}, { "elastic", "*MATERIAL, NAME=M\n*ELASTIC, EXTRA=1\n1.0, 0.3\n", 2U, }, { "beam-general-section", "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=S, " "MATERIAL=M, EXTRA=1\n" "1.0, 1.0, 0.0, 1.0, 1.0\n" "0.0, 1.0, 0.0\n", 1U, }, { "transverse-shear-stiffness", "*TRANSVERSE SHEAR STIFFNESS, EXTRA=1\n1.0, 1.0, 0.0\n", 1U, }, }; for (const auto& test_case : cases) { const TemporaryDeck input{ "fesa-parser-unsupported-parameter-" + std::string{test_case.name} + ".inp", test_case.contents, }; const auto result = fesa::parse_deck(input.path()); SCOPED_TRACE(test_case.name); EXPECT_FALSE(result.deck.has_value()); ASSERT_EQ(result.diagnostics.size(), 1U); EXPECT_EQ( result.diagnostics[0].code, "abaqus.syntax.unsupported_parameter"); ASSERT_TRUE(result.diagnostics[0].source.has_value()); EXPECT_EQ(result.diagnostics[0].source->line, test_case.line); } } TEST(AbaqusParser, RejectsInvalidParameterFormsAndSetScopeOptions) { struct Case final { std::string_view name; std::string_view contents; std::string_view code; }; const Case cases[]{ { "generate-valued", "*NSET, NSET=S, GENERATE=YES\n1, 2, 1\n", "abaqus.syntax.unsupported_parameter", }, { "generate-empty-valued", "*NSET, NSET=S, GENERATE=\n1, 2, 1\n", "abaqus.syntax.unsupported_parameter", }, { "empty-element-set", "*ELEMENT, TYPE=B31, ELSET=\n1, 1, 2\n", "abaqus.syntax.invalid_parameter", }, { "instance-on-flat-set", "*NSET, NSET=S, INSTANCE=I\n1\n", "abaqus.syntax.unsupported_parameter", }, { "instance-on-part-set", "*PART, NAME=P\n" "*NSET, NSET=S, INSTANCE=I\n" "1\n" "*END PART\n", "abaqus.syntax.unsupported_parameter", }, }; for (const auto& test_case : cases) { const TemporaryDeck input{ "fesa-parser-parameter-form-" + std::string{test_case.name} + ".inp", test_case.contents, }; const auto result = fesa::parse_deck(input.path()); SCOPED_TRACE(test_case.name); EXPECT_FALSE(result.deck.has_value()); ASSERT_EQ(result.diagnostics.size(), 1U); EXPECT_EQ(result.diagnostics[0].code, test_case.code); ASSERT_TRUE(result.diagnostics[0].source.has_value()); EXPECT_EQ(result.diagnostics[0].source->line, 1U + (test_case.name == "instance-on-part-set" ? 1U : 0U)); } } TEST(AbaqusParser, RejectsModelKeywordsOutsideTheirDocumentedScopes) { struct Case final { std::string_view name; std::string_view contents; std::string_view code; std::size_t line; }; const Case cases[]{ { "node-in-assembly", "*ASSEMBLY, NAME=A\n" "*NODE\n" "1, 0.0, 0.0, 0.0\n" "*END ASSEMBLY\n", "abaqus.syntax.invalid_node_scope", 2U, }, { "material-in-part", "*PART, NAME=P\n" "*MATERIAL, NAME=M\n" "*END PART\n", "abaqus.syntax.invalid_material_scope", 2U, }, { "section-in-assembly", "*ASSEMBLY, NAME=A\n" "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=S, MATERIAL=M\n" "1.0, 1.0, 0.0, 1.0, 1.0\n" "0.0, 1.0, 0.0\n" "*END ASSEMBLY\n", "abaqus.syntax.invalid_section_scope", 2U, }, }; for (const auto& test_case : cases) { const TemporaryDeck input{ "fesa-parser-invalid-scope-" + std::string{test_case.name} + ".inp", test_case.contents, }; const auto result = fesa::parse_deck(input.path()); SCOPED_TRACE(test_case.name); EXPECT_FALSE(result.deck.has_value()); ASSERT_EQ(result.diagnostics.size(), 1U); EXPECT_EQ(result.diagnostics[0].code, test_case.code); ASSERT_TRUE(result.diagnostics[0].source.has_value()); EXPECT_EQ(result.diagnostics[0].source->line, test_case.line); } } TEST(AbaqusParser, RejectsDataRowsOnMaterial) { const TemporaryDeck input{ "fesa-parser-material-data.inp", "*MATERIAL, NAME=Steel\n" "unexpected data\n"}; const auto result = fesa::parse_deck(input.path()); EXPECT_FALSE(result.deck.has_value()); ASSERT_EQ(result.diagnostics.size(), 1U); EXPECT_EQ( result.diagnostics[0].code, "abaqus.syntax.data_without_keyword"); ASSERT_TRUE(result.diagnostics[0].source.has_value()); EXPECT_EQ(result.diagnostics[0].source->line, 2U); } TEST(ScopedDeck, PreservesPartAssemblyAndInstanceScopes) { const auto path = fixture_path("minimal_part_instance_cantilever.inp"); const auto result = fesa::parse_deck(path); ASSERT_TRUE(result.deck.has_value()); EXPECT_TRUE(result.diagnostics.empty()); ASSERT_EQ(result.deck->parts.size(), 1U); EXPECT_EQ(result.deck->parts[0].name, "BeamPart"); EXPECT_EQ(result.deck->parts[0].source.file, path); EXPECT_EQ(result.deck->parts[0].source.line, 2U); EXPECT_EQ(record(result.deck->parts[0].records, "NODE").source.line, 3U); ASSERT_TRUE(result.deck->assembly.has_value()); EXPECT_EQ(result.deck->assembly->name, "RootAssembly"); EXPECT_EQ(result.deck->assembly->source.line, 19U); ASSERT_EQ(result.deck->assembly->instances.size(), 1U); EXPECT_EQ(result.deck->assembly->instances[0].name, "Beam-1"); EXPECT_EQ(result.deck->assembly->instances[0].part_name, "BeamPart"); EXPECT_EQ(result.deck->assembly->instances[0].source.line, 20U); EXPECT_TRUE( result.deck->assembly->instances[0].transform_data.empty()); EXPECT_EQ( record(result.deck->assembly->records, "NSET") .parameters.at("INSTANCE"), "Beam-1"); EXPECT_NE( std::ranges::find( result.deck->global_records, "MATERIAL", &fesa::DeckRecord::keyword), result.deck->global_records.end()); } TEST(ScopedDeck, PreservesInstanceTransformDataWithoutApplyingIt) { const TemporaryDeck input{ "fesa-parser-transform.inp", "*PART, NAME=BeamPart\n" "*END PART\n" "*ASSEMBLY, NAME=RootAssembly\n" "*INSTANCE, NAME=Beam-1, PART=BeamPart\n" "1.0, 2.0, 3.0\n" "0.0, 0.0, 1.0, 90.0\n" "*END INSTANCE\n" "*END ASSEMBLY\n"}; const auto result = fesa::parse_deck(input.path()); ASSERT_TRUE(result.deck.has_value()); ASSERT_TRUE(result.deck->assembly.has_value()); ASSERT_EQ(result.deck->assembly->instances.size(), 1U); EXPECT_EQ( result.deck->assembly->instances[0].transform_data, (std::vector>{ {"1.0", "2.0", "3.0"}, {"0.0", "0.0", "1.0", "90.0"}})); } TEST(ScopedDeck, ReportsUnexpectedAndUnclosedScopeTerminators) { struct Case final { std::string_view name; std::string_view contents; std::string_view code; std::size_t line; }; const Case cases[]{ { "fesa-parser-unexpected-end-part.inp", "*END PART\n", "abaqus.syntax.unexpected_end_part", 1U, }, { "fesa-parser-unclosed-part.inp", "*PART, NAME=BeamPart\n" "*NODE\n" "1, 0.0, 0.0, 0.0\n", "abaqus.syntax.unclosed_part", 1U, }, { "fesa-parser-unexpected-end-instance.inp", "*ASSEMBLY, NAME=RootAssembly\n" "*END INSTANCE\n", "abaqus.syntax.unexpected_end_instance", 2U, }, { "fesa-parser-unclosed-assembly.inp", "*ASSEMBLY, NAME=RootAssembly\n", "abaqus.syntax.unclosed_assembly", 1U, }, }; for (const auto& test_case : cases) { const TemporaryDeck input{test_case.name, test_case.contents}; const auto result = fesa::parse_deck(input.path()); SCOPED_TRACE(test_case.name); EXPECT_FALSE(result.deck.has_value()); ASSERT_EQ(result.diagnostics.size(), 1U); EXPECT_EQ(result.diagnostics[0].code, test_case.code); ASSERT_TRUE(result.diagnostics[0].source.has_value()); EXPECT_EQ(result.diagnostics[0].source->line, test_case.line); } } } // namespace