#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_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(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