#include #include #include #include #include #include #include "fesa/io/abaqus/domain_mapper.h" #include "fesa/io/abaqus/input_reader.h" #include "io/abaqus/domain_builder.h" #include "io/abaqus/domain_mapping_context.h" #include "io/abaqus/material_property_mapper.h" #include "io/abaqus/step_definition_mapper.h" #include "io/abaqus/topology_mapper.h" namespace fesa::abaqus_internal { namespace { class TemporaryComponentInput { public: TemporaryComponentInput(const std::string& stem, const std::string& content) : path_{std::filesystem::temp_directory_path() / ("fesa-domain-components-" + stem + ".inp")} { std::ofstream stream{path_, std::ios::binary | std::ios::trunc}; stream.write(content.data(), static_cast(content.size())); if (!stream) { throw std::runtime_error{"Unable to create mapper component fixture."}; } } ~TemporaryComponentInput() { std::error_code error; std::filesystem::remove(path_, error); } const std::filesystem::path& Path() const noexcept { return path_; } private: std::filesystem::path path_; }; ParsedInput ParseComponentInput(const std::string& stem, const std::string& content) { const TemporaryComponentInput input{stem, content}; auto parsed = AbaqusInputReader{}.Read(input.Path()); if (!parsed.HasValue()) { throw std::runtime_error{"Unable to parse mapper component fixture."}; } return std::move(parsed.Value()); } std::string ValidComponentDeck() { return R"inp(*Preprint *Part, name=BeamPart *Node 1, 0., 0., 0. 2, 1., 0., 0. *Element, type=B33 1, 1, 2 *Nset, nset=LocalRoot 1 *Elset, elset=BeamSet 1 *Beam General Section, elset=BeamSet, material=Steel, section=GENERAL 1., 1., 0., 1., 1. 0., 1., 0. *End Part *Assembly, name=Assembly *Instance, name=Beam-1, part=BeamPart *End Instance *Nset, nset=Root, instance=Beam-1 1 *Nset, nset=Tip, instance=Beam-1 2 *End Assembly *Material, name=Steel *Elastic 100., 0.25 *Boundary Root, 1, 6 *Step, name=Load, nlgeom=NO *Static 0.1, 1., 0.01, 1. *Boundary Tip, 1, 1, 0.125 *Cload Tip, 2, -1. *Output, field *Node Output U *End Step )inp"; } std::string ReplaceOnce(std::string text, const std::string& from, const std::string& to) { const std::size_t position = text.find(from); if (position == std::string::npos) { throw std::logic_error{"Mapper component mutation source was not found."}; } text.replace(position, from.size(), to); return text; } void ExpectSameDiagnostic(const Diagnostic& actual, const Diagnostic& expected) { EXPECT_EQ(actual.severity, expected.severity); EXPECT_EQ(actual.code, expected.code); EXPECT_EQ(actual.location.file, expected.location.file); EXPECT_EQ(actual.location.line, expected.location.line); EXPECT_EQ(actual.keyword, expected.keyword); EXPECT_EQ(actual.entity_identity, expected.entity_identity); EXPECT_EQ(actual.message, expected.message); } TEST(DomainMappingComponents, CModule001BuildsStableCandidatesBeforeAtomicDomainCommit) { ParsedInput input = ParseComponentInput("valid", ValidComponentDeck()); DomainMappingContext context{input}; TopologyMapper topology_mapper; MaterialPropertyMapper material_property_mapper; StepDefinitionMapper step_definition_mapper; ASSERT_TRUE(topology_mapper.Map(context).IsOk()); DomainMappingCandidateSummary summary = context.CandidateSummary(); EXPECT_EQ(summary.part_count, 1U); EXPECT_EQ(summary.node_count, 2U); EXPECT_EQ(summary.element_count, 1U); EXPECT_EQ(summary.instance_count, 1U); EXPECT_EQ(summary.node_set_count, 3U); EXPECT_EQ(summary.element_set_count, 1U); EXPECT_EQ(summary.material_count, 1U); EXPECT_EQ(summary.beam_section_count, 1U); EXPECT_EQ(summary.shell_section_count, 0U); EXPECT_EQ(summary.model_boundary_count, 1U); EXPECT_EQ(summary.step_boundary_count, 1U); EXPECT_EQ(summary.step_load_count, 1U); EXPECT_TRUE(summary.has_static_step); ASSERT_TRUE(material_property_mapper.Map(context).IsOk()); ASSERT_TRUE(topology_mapper.Finalize(context).IsOk()); ASSERT_TRUE(step_definition_mapper.Map(context).IsOk()); auto result = DomainBuilder{}.Build(context); ASSERT_TRUE(result.HasValue()); const Domain& domain = result.Value(); EXPECT_EQ(domain.Nodes().size(), 2U); EXPECT_EQ(domain.Elements().Size(), 1U); EXPECT_EQ(domain.Materials().Size(), 1U); EXPECT_EQ(domain.Properties().Size(), 1U); ASSERT_EQ(domain.Steps().Size(), 1U); EXPECT_EQ(domain.Steps()[0].BoundaryConditions().size(), 7U); EXPECT_EQ(domain.Steps()[0].Loads().size(), 1U); ASSERT_EQ(domain.Warnings().size(), 3U); EXPECT_EQ(domain.Warnings()[0].keyword, "PREPRINT"); EXPECT_EQ(domain.Warnings()[1].keyword, "OUTPUT"); EXPECT_EQ(domain.Warnings()[2].keyword, "NODE OUTPUT"); } TEST(DomainMappingComponents, CModule001PreservesExactFirstFailureThroughFacade) { ParsedInput input = ParseComponentInput( "invalid", "*Node\n1, 0., 0., 0.\n*Material, name=Steel\n"); DomainMappingContext context{input}; const Status component_status = TopologyMapper{}.Map(context); ASSERT_FALSE(component_status.IsOk()); ASSERT_EQ(component_status.Diagnostics().size(), 1U); const Diagnostic& component_diagnostic = component_status.Diagnostics().front(); EXPECT_EQ(component_status.Category(), FailureCategory::kInput); EXPECT_EQ(component_diagnostic.severity, Severity::kError); EXPECT_EQ(component_diagnostic.code, "unsupported-keyword"); EXPECT_EQ(component_diagnostic.location.line, 1U); EXPECT_EQ(component_diagnostic.keyword, "NODE"); EXPECT_TRUE(component_diagnostic.entity_identity.empty()); EXPECT_EQ(component_diagnostic.message, "The keyword is outside the approved Abaqus subset."); auto facade_result = AbaqusDomainMapper{}.Map(input); ASSERT_FALSE(facade_result.HasValue()); ASSERT_EQ(facade_result.GetStatus().Diagnostics().size(), 1U); EXPECT_EQ(facade_result.GetStatus().Category(), component_status.Category()); ExpectSameDiagnostic(facade_result.GetStatus().Diagnostics().front(), component_diagnostic); } TEST(DomainMappingComponents, CModule001RejectsInvalidMaterialAtOwningComponent) { ParsedInput input = ParseComponentInput( "invalid-material", ReplaceOnce(ValidComponentDeck(), "100., 0.25", "-100., 0.25")); DomainMappingContext context{input}; ASSERT_TRUE(TopologyMapper{}.Map(context).IsOk()); const Status status = MaterialPropertyMapper{}.Map(context); ASSERT_FALSE(status.IsOk()); ASSERT_EQ(status.Diagnostics().size(), 1U); EXPECT_EQ(status.Category(), FailureCategory::kModel); const Diagnostic& diagnostic = status.Diagnostics().front(); EXPECT_EQ(diagnostic.code, "invalid-beam-property"); EXPECT_EQ(diagnostic.location.line, 26U); EXPECT_EQ(diagnostic.keyword, "ELASTIC"); EXPECT_EQ(diagnostic.entity_identity, "Steel"); EXPECT_EQ(diagnostic.message, "E and the derived G=E/(2*(1+nu)) must be positive."); } TEST(DomainMappingComponents, CModule001RejectsUnresolvedLoadAtOwningComponent) { ParsedInput input = ParseComponentInput( "invalid-step", ReplaceOnce(ValidComponentDeck(), "Tip, 2, -1.", "Missing, 2, -1.")); DomainMappingContext context{input}; TopologyMapper topology_mapper; ASSERT_TRUE(topology_mapper.Map(context).IsOk()); ASSERT_TRUE(MaterialPropertyMapper{}.Map(context).IsOk()); ASSERT_TRUE(topology_mapper.Finalize(context).IsOk()); const Status status = StepDefinitionMapper{}.Map(context); ASSERT_FALSE(status.IsOk()); ASSERT_EQ(status.Diagnostics().size(), 1U); EXPECT_EQ(status.Category(), FailureCategory::kInput); const Diagnostic& diagnostic = status.Diagnostics().front(); EXPECT_EQ(diagnostic.code, "unresolved-reference"); EXPECT_EQ(diagnostic.location.line, 35U); EXPECT_EQ(diagnostic.keyword, "CLOAD"); EXPECT_EQ(diagnostic.entity_identity, "Missing"); EXPECT_EQ(diagnostic.message, "The boundary or load target must resolve unambiguously to one " "node or one node set."); } } // namespace } // namespace fesa::abaqus_internal