#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { class TestHdf5Handle final { public: using CloseFunction = herr_t (*)(hid_t); TestHdf5Handle(hid_t id, CloseFunction close) : id_{id}, close_{close} { if (id_ < 0) { throw std::runtime_error{"Failed to open test HDF5 resource."}; } } ~TestHdf5Handle() { if (id_ >= 0) { close_(id_); } } TestHdf5Handle(const TestHdf5Handle&) = delete; TestHdf5Handle& operator=(const TestHdf5Handle&) = delete; TestHdf5Handle(TestHdf5Handle&& other) noexcept : id_{std::exchange(other.id_, H5I_INVALID_HID)}, close_{other.close_} {} TestHdf5Handle& operator=(TestHdf5Handle&&) = delete; [[nodiscard]] hid_t get() const noexcept { return id_; } private: hid_t id_; CloseFunction close_; }; std::string hdf5_path(const std::filesystem::path& path) { const std::u8string value = path.u8string(); return {reinterpret_cast(value.data()), value.size()}; } void require_hdf5_status(const herr_t status) { if (status < 0) { throw std::runtime_error{"Test HDF5 mutation failed."}; } } void replace_step_time_with_vector(const std::filesystem::path& path) { const std::string encoded_path = hdf5_path(path); TestHdf5Handle file{ H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT), &H5Fclose, }; TestHdf5Handle frame{ H5Gopen2( file.get(), "/results/steps/0/frames/0", H5P_DEFAULT), &H5Gclose, }; require_hdf5_status(H5Adelete(frame.get(), "step_time")); const std::array dimensions{2U}; TestHdf5Handle space{ H5Screate_simple(1, dimensions.data(), nullptr), &H5Sclose, }; TestHdf5Handle attribute{ H5Acreate2( frame.get(), "step_time", H5T_IEEE_F64LE, space.get(), H5P_DEFAULT, H5P_DEFAULT), &H5Aclose, }; const std::array values{1.25, 2.5}; require_hdf5_status( H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, values.data())); } void delete_link( const std::filesystem::path& path, const std::string_view link_path) { const std::string encoded_path = hdf5_path(path); const std::string owned_link_path{link_path}; TestHdf5Handle file{ H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT), &H5Fclose, }; require_hdf5_status( H5Ldelete(file.get(), owned_link_path.c_str(), H5P_DEFAULT)); } void copy_object( const std::filesystem::path& path, const std::string_view source_path, const std::string_view target_path) { const std::string encoded_path = hdf5_path(path); const std::string owned_source_path{source_path}; const std::string owned_target_path{target_path}; TestHdf5Handle file{ H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT), &H5Fclose, }; require_hdf5_status(H5Ocopy( file.get(), owned_source_path.c_str(), file.get(), owned_target_path.c_str(), H5P_DEFAULT, H5P_DEFAULT)); } void write_double_attribute( const std::filesystem::path& path, const std::string_view object_path, const std::string_view attribute_name, const double value) { const std::string encoded_path = hdf5_path(path); const std::string owned_object_path{object_path}; const std::string owned_attribute_name{attribute_name}; TestHdf5Handle file{ H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT), &H5Fclose, }; TestHdf5Handle object{ H5Oopen(file.get(), owned_object_path.c_str(), H5P_DEFAULT), &H5Oclose, }; TestHdf5Handle attribute{ H5Aopen(object.get(), owned_attribute_name.c_str(), H5P_DEFAULT), &H5Aclose, }; require_hdf5_status( H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, &value)); } void write_int64_dataset( const std::filesystem::path& path, const std::string_view dataset_path, const std::span values) { const std::string encoded_path = hdf5_path(path); const std::string owned_dataset_path{dataset_path}; TestHdf5Handle file{ H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT), &H5Fclose, }; TestHdf5Handle dataset{ H5Dopen2( file.get(), owned_dataset_path.c_str(), H5P_DEFAULT), &H5Dclose, }; require_hdf5_status(H5Dwrite( dataset.get(), H5T_NATIVE_INT64, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data())); } void write_double_dataset( const std::filesystem::path& path, const std::string_view dataset_path, const std::span values) { const std::string encoded_path = hdf5_path(path); const std::string owned_dataset_path{dataset_path}; TestHdf5Handle file{ H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT), &H5Fclose, }; TestHdf5Handle dataset{ H5Dopen2( file.get(), owned_dataset_path.c_str(), H5P_DEFAULT), &H5Dclose, }; require_hdf5_status(H5Dwrite( dataset.get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data())); } void write_uint8_dataset( const std::filesystem::path& path, const std::string_view dataset_path, const std::span values) { const std::string encoded_path = hdf5_path(path); const std::string owned_dataset_path{dataset_path}; TestHdf5Handle file{ H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT), &H5Fclose, }; TestHdf5Handle dataset{ H5Dopen2( file.get(), owned_dataset_path.c_str(), H5P_DEFAULT), &H5Dclose, }; require_hdf5_status(H5Dwrite( dataset.get(), H5T_NATIVE_UINT8, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data())); } void write_root_string_attribute( const std::filesystem::path& path, const std::string_view name, const std::string_view value) { const std::string encoded_path = hdf5_path(path); const std::string owned_name{name}; const std::string owned_value{value}; TestHdf5Handle file{ H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT), &H5Fclose, }; TestHdf5Handle attribute{ H5Aopen(file.get(), owned_name.c_str(), H5P_DEFAULT), &H5Aclose, }; TestHdf5Handle type{H5Aget_type(attribute.get()), &H5Tclose}; const char* pointer = owned_value.c_str(); require_hdf5_status(H5Awrite(attribute.get(), type.get(), &pointer)); } std::filesystem::path round_trip_path() { return std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-round-trip.h5"; } std::filesystem::path self_contained_path() { return std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-self-contained.h5"; } const fesa::Hdf5InputIdentity& test_input_identity() { static const fesa::Hdf5InputIdentity identity{ "beam model.inp", "fnv1a64:0123456789abcdef", }; return identity; } fesa::Domain make_domain(const bool add_unreported_node = false) { fesa::DomainBuilder builder; builder.add_node({ fesa::NodeId{42}, fesa::EntityOrigin{"BeamPart", "Beam-1", 1001}, fesa::Vec3{1.25, -2.5, 3.75}, }); builder.add_node({ fesa::NodeId{7}, fesa::EntityOrigin{"BeamPart", "Beam-1", 1002}, fesa::Vec3{2.25, -2.5, 3.75}, }); if (add_unreported_node) { builder.add_node({ fesa::NodeId{99}, fesa::EntityOrigin{"BeamPart", "Beam-1", 1003}, fesa::Vec3{8.0, 0.0, 0.0}, }); } builder.add_material({ fesa::MaterialId{6}, "Steel", 210.0e9, 0.3, }); builder.add_section({ fesa::SectionId{4}, "InputShear", 0.04, 1.2e-4, 1.4e-4, 2.0e-4, 0.031, 0.032, fesa::ShearPropertySource::input, fesa::Vec3{0.0, 1.0, 0.0}, {{0.25, -0.5}, {-0.75, 0.125}}, }); builder.add_section({ fesa::SectionId{12}, "DefaultShear", 0.06, 1.5e-4, 1.7e-4, 2.2e-4, 0.05, 0.05, fesa::ShearPropertySource::phase1_default, fesa::Vec3{0.0, 1.0, 0.0}, {}, }); builder.add_beam_element({ fesa::ElementId{9}, fesa::EntityOrigin{"BeamPart", "Beam-1", 2001}, {fesa::NodeId{7}, fesa::NodeId{42}}, fesa::MaterialId{6}, fesa::SectionId{4}, }); builder.add_beam_element({ fesa::ElementId{17}, fesa::EntityOrigin{"BeamPart", "Beam-1", 2002}, {fesa::NodeId{42}, fesa::NodeId{7}}, fesa::MaterialId{6}, fesa::SectionId{4}, }); builder.add_node_set({"Fixed", {fesa::NodeId{42}}}); builder.add_node_set( {"Loaded", {fesa::NodeId{7}, fesa::NodeId{42}}}); builder.add_element_set( {"AllBeams", {fesa::ElementId{9}, fesa::ElementId{17}}}); builder.set_step({ "Load/Case", { {fesa::NodeId{42}, 1, 0.0}, {fesa::NodeId{7}, 6, 0.125}, }, {{ fesa::NodeId{7}, {100.0, -200.0, 300.0, -400.0, 500.0, -600.0}, }}, }); auto built = std::move(builder).build(); EXPECT_TRUE(built.domain.has_value()); EXPECT_TRUE(built.diagnostics.empty()); return std::move(*built.domain); } fesa::BeamSectionResult make_end_result( double xi, fesa::NodeId node, double offset); fesa::ResultDatabase make_database() { fesa::ResultDatabase database{ "2.0.0", {{ "Load/Case", {{ 1.25, { {fesa::NodeId{7}, fesa::NodeId{42}}, { fesa::EntityOrigin{ "BeamPart", "Beam-1", 1002}, fesa::EntityOrigin{ "BeamPart", "Beam-1", 1001}, }, { {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}, {-1.0, -2.0, -3.0, -4.0, -5.0, -6.0}, }, { {10.0, 20.0, 30.0, 40.0, 50.0, 60.0}, {-10.0, -20.0, -30.0, -40.0, -50.0, -60.0}, }, }, {}, {}, }}, }}, }; database.steps[0].frames[0].element.beams = { { fesa::ElementId{9}, {"BeamPart", "Beam-1", 2001}, { {-1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, -1.0}, }, { make_end_result(-1.0, fesa::NodeId{7}, 0.0), make_end_result(1.0, fesa::NodeId{42}, 100.0), }, }, { fesa::ElementId{17}, {"BeamPart", "Beam-1", 2002}, { {1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}, }, { make_end_result(-1.0, fesa::NodeId{42}, 200.0), make_end_result(1.0, fesa::NodeId{7}, 300.0), }, }, }; return database; } fesa::BeamSectionResult make_end_result( const double xi, const fesa::NodeId node, const double offset) { return { xi, node, { offset + 1.0, offset + 2.0, offset + 3.0, offset + 4.0, offset + 5.0, offset + 6.0, }, { offset + 10.0, offset + 20.0, offset + 30.0, offset + 40.0, offset + 50.0, offset + 60.0, }, offset + 70.0, {offset + 80.0, offset + 90.0}, }; } fesa::ResultDatabase make_complete_database() { fesa::ResultDatabase database = make_database(); auto& frame = database.steps[0].frames[0]; frame.diagnostics = { { fesa::DiagnosticStage::solver, fesa::Severity::warning, "solver.residual", "Residual diagnostic", fesa::SourceLocation{"beam model.inp", 41, 7}, }, { fesa::DiagnosticStage::results, fesa::Severity::error, "results.equilibrium", "Equilibrium diagnostic", std::nullopt, }, }; return database; } bool has_results_error( const std::vector& diagnostics, const std::string_view code) { return std::ranges::any_of( diagnostics, [code](const fesa::Diagnostic& diagnostic) { return diagnostic.stage == fesa::DiagnosticStage::results && diagnostic.severity == fesa::Severity::error && diagnostic.code == code; }); } TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) { const auto path = round_trip_path(); std::filesystem::create_directories(path.parent_path()); std::filesystem::remove(path); const auto domain = make_domain(); const auto database = make_database(); const auto write_diagnostics = fesa::write_hdf5( path, domain, database, test_input_identity()); ASSERT_TRUE(write_diagnostics.empty()); const auto read = fesa::read_hdf5_results(path); ASSERT_TRUE(read.diagnostics.empty()); ASSERT_TRUE(read.database.has_value()); ASSERT_TRUE(read.model.has_value()); EXPECT_EQ(read.database->schema_version, "2.0.0"); ASSERT_EQ(read.database->steps.size(), 1U); const auto& step = read.database->steps[0]; EXPECT_EQ(step.name, "Load/Case"); ASSERT_EQ(step.frames.size(), 1U); const auto& frame = step.frames[0]; EXPECT_DOUBLE_EQ(frame.step_time, 1.25); EXPECT_EQ( frame.nodal.node_ids, (std::vector{fesa::NodeId{7}, fesa::NodeId{42}})); EXPECT_EQ( frame.nodal.origins, (std::vector{ {"BeamPart", "Beam-1", 1002}, {"BeamPart", "Beam-1", 1001}, })); EXPECT_EQ( frame.nodal.displacement[0], (std::array{1.0, 2.0, 3.0, 4.0, 5.0, 6.0})); EXPECT_EQ( frame.nodal.displacement[1], (std::array{-1.0, -2.0, -3.0, -4.0, -5.0, -6.0})); EXPECT_EQ( frame.nodal.reaction[0], (std::array{10.0, 20.0, 30.0, 40.0, 50.0, 60.0})); EXPECT_EQ( frame.nodal.reaction[1], (std::array{ -10.0, -20.0, -30.0, -40.0, -50.0, -60.0})); EXPECT_TRUE(frame.diagnostics.empty()); ASSERT_EQ(read.model->nodes.size(), 2U); EXPECT_EQ(read.model->nodes[0].dense_index, 0U); EXPECT_EQ(read.model->nodes[0].id, fesa::NodeId{42}); EXPECT_EQ( read.model->nodes[0].origin, (fesa::EntityOrigin{"BeamPart", "Beam-1", 1001})); EXPECT_DOUBLE_EQ(read.model->nodes[0].coordinates.x, 1.25); EXPECT_DOUBLE_EQ(read.model->nodes[0].coordinates.y, -2.5); EXPECT_DOUBLE_EQ(read.model->nodes[0].coordinates.z, 3.75); EXPECT_EQ(read.model->nodes[1].dense_index, 1U); EXPECT_EQ(read.model->nodes[1].id, fesa::NodeId{7}); EXPECT_EQ( read.model->nodes[1].origin, (fesa::EntityOrigin{"BeamPart", "Beam-1", 1002})); EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.x, 2.25); EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.y, -2.5); EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.z, 3.75); ASSERT_EQ(read.model->elements.size(), 2U); EXPECT_EQ(read.model->elements[0].dense_index, 0U); EXPECT_EQ(read.model->elements[0].id, fesa::ElementId{9}); EXPECT_EQ( read.model->elements[0].connectivity, (std::array{1U, 0U})); EXPECT_EQ(read.model->elements[0].section, fesa::SectionId{4}); EXPECT_EQ(read.model->elements[1].dense_index, 1U); EXPECT_EQ(read.model->elements[1].id, fesa::ElementId{17}); EXPECT_EQ( read.model->elements[1].connectivity, (std::array{0U, 1U})); EXPECT_EQ(read.model->elements[1].section, fesa::SectionId{4}); ASSERT_EQ(read.model->sections.size(), 2U); EXPECT_EQ(read.model->sections[0].id, fesa::SectionId{4}); EXPECT_DOUBLE_EQ(read.model->sections[0].shear_area_y, 0.031); EXPECT_DOUBLE_EQ(read.model->sections[0].shear_area_z, 0.032); EXPECT_EQ( read.model->sections[0].shear_source, fesa::ShearPropertySource::input); EXPECT_EQ(read.model->sections[1].id, fesa::SectionId{12}); EXPECT_DOUBLE_EQ(read.model->sections[1].shear_area_y, 0.05); EXPECT_DOUBLE_EQ(read.model->sections[1].shear_area_z, 0.05); EXPECT_EQ( read.model->sections[1].shear_source, fesa::ShearPropertySource::phase1_default); } TEST(SelfContainedHdf5, PublicReaderReconstructsCompletePhase1Contract) { const auto path = self_contained_path(); std::filesystem::create_directories(path.parent_path()); std::filesystem::remove(path); ASSERT_TRUE(fesa::write_hdf5( path, make_domain(), make_complete_database(), test_input_identity()) .empty()); const fesa::Hdf5ReadResult read = fesa::read_hdf5_results(path); ASSERT_TRUE(read.diagnostics.empty()); ASSERT_TRUE(read.metadata.has_value()); ASSERT_TRUE(read.model.has_value()); ASSERT_TRUE(read.analysis.has_value()); ASSERT_TRUE(read.database.has_value()); EXPECT_EQ(read.metadata->schema_version, "2.0.0"); EXPECT_EQ(read.metadata->fesa_version, "0.1.0"); EXPECT_EQ( read.metadata->unit_policy, "consistent_input_units_no_conversion"); EXPECT_EQ(read.metadata->input_source, "beam model.inp"); EXPECT_EQ( read.metadata->input_fingerprint, "fnv1a64:0123456789abcdef"); ASSERT_EQ(read.model->nodes.size(), 2U); ASSERT_EQ(read.model->elements.size(), 2U); EXPECT_EQ( read.model->elements[0].origin, (fesa::EntityOrigin{"BeamPart", "Beam-1", 2001})); EXPECT_EQ(read.model->elements[0].material, fesa::MaterialId{6}); ASSERT_EQ(read.model->materials.size(), 1U); EXPECT_EQ(read.model->materials[0].name, "Steel"); EXPECT_DOUBLE_EQ(read.model->materials[0].young, 210.0e9); EXPECT_DOUBLE_EQ(read.model->materials[0].poisson, 0.3); ASSERT_EQ(read.model->sections.size(), 2U); EXPECT_EQ(read.model->sections[0].name, "InputShear"); EXPECT_DOUBLE_EQ(read.model->sections[0].area, 0.04); EXPECT_DOUBLE_EQ(read.model->sections[0].iy, 1.2e-4); EXPECT_DOUBLE_EQ(read.model->sections[0].iz, 1.4e-4); EXPECT_DOUBLE_EQ(read.model->sections[0].torsion_j, 2.0e-4); EXPECT_DOUBLE_EQ(read.model->sections[0].orientation.y, 1.0); EXPECT_EQ( read.model->sections[0].recovery_points, (std::vector>{ {0.25, -0.5}, {-0.75, 0.125}})); ASSERT_EQ(read.model->node_sets.size(), 2U); EXPECT_EQ(read.model->node_sets[0].name, "Fixed"); EXPECT_EQ( read.model->node_sets[1].members, (std::vector{ fesa::NodeId{7}, fesa::NodeId{42}})); ASSERT_EQ(read.model->element_sets.size(), 1U); EXPECT_EQ( read.model->element_sets[0].members, (std::vector{ fesa::ElementId{9}, fesa::ElementId{17}})); EXPECT_EQ(read.analysis->step.name, "Load/Case"); ASSERT_EQ(read.analysis->step.prescribed_dofs.size(), 2U); EXPECT_EQ( read.analysis->step.prescribed_dofs[1].node, fesa::NodeId{7}); EXPECT_EQ(read.analysis->step.prescribed_dofs[1].dof, 6U); EXPECT_DOUBLE_EQ( read.analysis->step.prescribed_dofs[1].value, 0.125); ASSERT_EQ(read.analysis->step.nodal_loads.size(), 1U); EXPECT_DOUBLE_EQ( read.analysis->step.nodal_loads[0].values[5], -600.0); EXPECT_EQ(read.analysis->solver.backend, "mkl_pardiso"); EXPECT_EQ( read.analysis->solver.constraint_method, "essential_dof_elimination"); EXPECT_EQ(read.analysis->solver.assembly, "deterministic_serial"); const auto& frame = read.database->steps[0].frames[0]; ASSERT_EQ(frame.element.beams.size(), 2U); EXPECT_EQ(frame.element.beams[0].element, fesa::ElementId{9}); EXPECT_DOUBLE_EQ(frame.element.beams[0].local_frame.ex.x, -1.0); EXPECT_EQ( frame.element.beams[0].end_results[0].end_node, fesa::NodeId{7}); EXPECT_EQ( frame.element.beams[0].end_results[1].section_force, (std::array{ 110.0, 120.0, 130.0, 140.0, 150.0, 160.0})); EXPECT_EQ( frame.element.beams[1].end_results[0].sigma_xx, (std::vector{280.0, 290.0})); ASSERT_EQ(frame.diagnostics.size(), 2U); EXPECT_EQ(frame.diagnostics[0].stage, fesa::DiagnosticStage::solver); EXPECT_EQ(frame.diagnostics[0].severity, fesa::Severity::warning); ASSERT_TRUE(frame.diagnostics[0].source.has_value()); EXPECT_EQ(frame.diagnostics[0].source->file, "beam model.inp"); EXPECT_EQ(frame.diagnostics[0].source->line, 41U); EXPECT_EQ(frame.diagnostics[0].source->column, 7U); EXPECT_FALSE(frame.diagnostics[1].source.has_value()); } TEST(SelfContainedHdf5, RejectsMissingResultFrameBeforeWriting) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-missing-result-frame.h5"; std::filesystem::remove(path); auto database = make_complete_database(); database.steps[0].frames.clear(); const auto diagnostics = fesa::write_hdf5( path, make_domain(), database, test_input_identity()); EXPECT_TRUE(has_results_error( diagnostics, "hdf5.incomplete_result_frame")); EXPECT_FALSE(std::filesystem::exists(path)); } TEST(SelfContainedHdf5, RejectsIncompleteNodalCoverageBeforeWriting) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-incomplete-nodal-results.h5"; std::filesystem::remove(path); const auto database = make_complete_database(); const auto diagnostics = fesa::write_hdf5( path, make_domain(true), database, test_input_identity()); EXPECT_TRUE(has_results_error( diagnostics, "hdf5.incomplete_result_frame")); EXPECT_FALSE(std::filesystem::exists(path)); } TEST(SelfContainedHdf5, RejectsIncompleteBeamCoverageBeforeWriting) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-incomplete-beam-results.h5"; std::filesystem::remove(path); auto database = make_complete_database(); database.steps[0].frames[0].element.beams.pop_back(); const auto diagnostics = fesa::write_hdf5( path, make_domain(), database, test_input_identity()); EXPECT_TRUE(has_results_error( diagnostics, "hdf5.incomplete_result_frame")); EXPECT_FALSE(std::filesystem::exists(path)); } TEST(SelfContainedHdf5, RejectsFiniteLocalFrameThatDisagreesWithModel) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-wrong-local-frame.h5"; std::filesystem::remove(path); auto database = make_complete_database(); database.steps[0].frames[0].element.beams[0].local_frame = { {1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}, }; const auto diagnostics = fesa::write_hdf5( path, make_domain(), database, test_input_identity()); EXPECT_TRUE(has_results_error( diagnostics, "hdf5.result_element_mismatch")); EXPECT_FALSE(std::filesystem::exists(path)); } TEST(Hdf5, RejectsVersion1AfterMajorSchemaChange) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-schema-1.h5"; std::filesystem::remove(path); auto database = make_database(); database.schema_version = "1.0.0"; const auto diagnostics = fesa::write_hdf5( path, make_domain(), database, test_input_identity()); EXPECT_TRUE(has_results_error(diagnostics, "hdf5.unsupported_schema")); EXPECT_FALSE(std::filesystem::exists(path)); } TEST(Hdf5, RejectsMissingInputIdentityBeforeWriting) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-missing-input-identity.h5"; std::filesystem::remove(path); const auto diagnostics = fesa::write_hdf5( path, make_domain(), make_database(), {"", ""}); EXPECT_TRUE(has_results_error( diagnostics, "hdf5.invalid_input_identity")); EXPECT_FALSE(std::filesystem::exists(path)); } TEST(Hdf5, RejectsUnlistedMinorSchemaVersion) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-schema-2-1.h5"; std::filesystem::remove(path); ASSERT_TRUE(fesa::write_hdf5( path, make_domain(), make_database(), test_input_identity()) .empty()); write_root_string_attribute(path, "schema_version", "2.1.0"); const auto read = fesa::read_hdf5_results(path); EXPECT_FALSE(read.database.has_value()); EXPECT_FALSE(read.model.has_value()); EXPECT_FALSE(read.metadata.has_value()); EXPECT_FALSE(read.analysis.has_value()); EXPECT_TRUE(has_results_error( read.diagnostics, "hdf5.unsupported_schema")); } TEST(Hdf5, RejectsInvalidResultDatabaseBeforeWriting) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-invalid.h5"; std::filesystem::remove(path); const auto domain = make_domain(); auto database = make_database(); database.steps[0].frames[0].nodal.reaction.pop_back(); const auto diagnostics = fesa::write_hdf5(path, domain, database, test_input_identity()); EXPECT_TRUE( has_results_error(diagnostics, "results.nodal_size_mismatch")); EXPECT_FALSE(std::filesystem::exists(path)); } TEST(Hdf5, PreservesFrameDiagnosticsRepresentedBySchema) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-unsupported-diagnostics.h5"; std::filesystem::remove(path); const auto domain = make_domain(); auto database = make_database(); database.steps[0].frames[0].diagnostics.push_back({ fesa::DiagnosticStage::solver, fesa::Severity::warning, "solver.residual", "Residual diagnostic", std::nullopt, }); const auto diagnostics = fesa::write_hdf5(path, domain, database, test_input_identity()); ASSERT_TRUE(diagnostics.empty()); const auto read = fesa::read_hdf5_results(path); ASSERT_TRUE(read.diagnostics.empty()); ASSERT_TRUE(read.database.has_value()); const auto& stored = read.database->steps[0].frames[0].diagnostics; ASSERT_EQ(stored.size(), 1U); EXPECT_EQ(stored[0].stage, fesa::DiagnosticStage::solver); EXPECT_EQ(stored[0].severity, fesa::Severity::warning); EXPECT_EQ(stored[0].code, "solver.residual"); EXPECT_EQ(stored[0].message, "Residual diagnostic"); EXPECT_FALSE(stored[0].source.has_value()); } TEST(Hdf5, RejectsResultNodeMissingFromDomainBeforeWriting) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-missing-result-node.h5"; std::filesystem::remove(path); const auto domain = make_domain(); auto database = make_database(); auto& nodal = database.steps[0].frames[0].nodal; nodal.node_ids.push_back(fesa::NodeId{999}); nodal.origins.push_back({"BeamPart", "Beam-1", 1999}); nodal.displacement.push_back({}); nodal.reaction.push_back({}); const auto diagnostics = fesa::write_hdf5(path, domain, database, test_input_identity()); EXPECT_TRUE(has_results_error( diagnostics, "hdf5.result_node_not_in_model")); EXPECT_FALSE(std::filesystem::exists(path)); } TEST(Hdf5, ReportsMissingFileAtResultsStage) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-missing.h5"; std::filesystem::remove(path); const auto read = fesa::read_hdf5_results(path); EXPECT_FALSE(read.database.has_value()); EXPECT_FALSE(read.model.has_value()); EXPECT_TRUE(has_results_error(read.diagnostics, "hdf5.open_failed")); } TEST(Hdf5, RejectsNonScalarStepTimeAttribute) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-nonscalar-step-time.h5"; std::filesystem::remove(path); const auto write_diagnostics = fesa::write_hdf5( path, make_domain(), make_database(), test_input_identity()); ASSERT_TRUE(write_diagnostics.empty()); replace_step_time_with_vector(path); const auto read = fesa::read_hdf5_results(path); EXPECT_FALSE(read.database.has_value()); EXPECT_FALSE(read.model.has_value()); EXPECT_TRUE(has_results_error(read.diagnostics, "hdf5.read_failed")); } TEST(Hdf5, RejectsSerializedResultWithoutRequiredFrame) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-missing-serialized-frame.h5"; std::filesystem::remove(path); ASSERT_TRUE(fesa::write_hdf5( path, make_domain(), make_database(), test_input_identity()) .empty()); delete_link(path, "/results/steps/0/frames/0"); const auto read = fesa::read_hdf5_results(path); EXPECT_FALSE(read.database.has_value()); EXPECT_FALSE(read.model.has_value()); EXPECT_FALSE(read.metadata.has_value()); EXPECT_FALSE(read.analysis.has_value()); EXPECT_TRUE(has_results_error( read.diagnostics, "hdf5.incomplete_result_frame")); } TEST(Hdf5, RejectsSerializedResultWithExtraFrame) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-extra-serialized-frame.h5"; std::filesystem::remove(path); ASSERT_TRUE(fesa::write_hdf5( path, make_domain(), make_database(), test_input_identity()) .empty()); copy_object( path, "/results/steps/0/frames/0", "/results/steps/0/frames/1"); write_double_attribute( path, "/results/steps/0/frames/1", "step_time", 2.5); const auto read = fesa::read_hdf5_results(path); EXPECT_FALSE(read.database.has_value()); EXPECT_FALSE(read.model.has_value()); EXPECT_FALSE(read.metadata.has_value()); EXPECT_FALSE(read.analysis.has_value()); EXPECT_TRUE(has_results_error( read.diagnostics, "hdf5.incomplete_result_frame")); } TEST(Hdf5, RejectsInvalidSerializedInputFingerprint) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-invalid-input-fingerprint.h5"; std::filesystem::remove(path); ASSERT_TRUE(fesa::write_hdf5( path, make_domain(), make_database(), test_input_identity()) .empty()); write_root_string_attribute( path, "input_fingerprint", "sha256:not-the-contract"); const auto read = fesa::read_hdf5_results(path); EXPECT_FALSE(read.database.has_value()); EXPECT_FALSE(read.model.has_value()); EXPECT_FALSE(read.metadata.has_value()); EXPECT_FALSE(read.analysis.has_value()); EXPECT_TRUE(has_results_error( read.diagnostics, "hdf5.invalid_input_identity")); } TEST(Hdf5, RejectsResultNodeMissingFromSerializedModel) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-invalid-result-node.h5"; std::filesystem::remove(path); auto database = make_database(); auto& nodal = database.steps[0].frames[0].nodal; nodal.node_ids.push_back(fesa::NodeId{99}); nodal.origins.push_back({"BeamPart", "Beam-1", 1003}); nodal.displacement.push_back({}); nodal.reaction.push_back({}); const auto write_diagnostics = fesa::write_hdf5( path, make_domain(true), database, test_input_identity()); ASSERT_TRUE(write_diagnostics.empty()); const std::array node_ids{42, 7, 100}; write_int64_dataset(path, "/model/nodes/internal_id", node_ids); const auto read = fesa::read_hdf5_results(path); EXPECT_FALSE(read.database.has_value()); EXPECT_FALSE(read.model.has_value()); EXPECT_TRUE(has_results_error( read.diagnostics, "hdf5.result_node_not_in_model")); } TEST(Hdf5, RejectsDuplicateSerializedNodeIds) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-duplicate-node-ids.h5"; std::filesystem::remove(path); ASSERT_TRUE(fesa::write_hdf5( path, make_domain(), make_database(), test_input_identity()) .empty()); const std::array ids{42, 42}; write_int64_dataset(path, "/model/nodes/internal_id", ids); const auto read = fesa::read_hdf5_results(path); EXPECT_FALSE(read.database.has_value()); EXPECT_FALSE(read.model.has_value()); EXPECT_TRUE( has_results_error(read.diagnostics, "hdf5.invalid_model_data")); } TEST(Hdf5, RejectsDuplicateSerializedElementIds) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-duplicate-element-ids.h5"; std::filesystem::remove(path); ASSERT_TRUE(fesa::write_hdf5( path, make_domain(), make_database(), test_input_identity()) .empty()); const std::array ids{9, 9}; write_int64_dataset(path, "/model/elements/internal_id", ids); const auto read = fesa::read_hdf5_results(path); EXPECT_FALSE(read.database.has_value()); EXPECT_FALSE(read.model.has_value()); EXPECT_TRUE( has_results_error(read.diagnostics, "hdf5.invalid_model_data")); } TEST(Hdf5, RejectsDuplicateSerializedSectionIds) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-duplicate-section-ids.h5"; std::filesystem::remove(path); ASSERT_TRUE(fesa::write_hdf5( path, make_domain(), make_database(), test_input_identity()) .empty()); const std::array ids{4, 4}; write_int64_dataset(path, "/model/sections/internal_id", ids); const auto read = fesa::read_hdf5_results(path); EXPECT_FALSE(read.database.has_value()); EXPECT_FALSE(read.model.has_value()); EXPECT_TRUE( has_results_error(read.diagnostics, "hdf5.invalid_model_data")); } TEST(Hdf5, RejectsNonfiniteSerializedCoordinates) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-nonfinite-coordinates.h5"; std::filesystem::remove(path); ASSERT_TRUE(fesa::write_hdf5( path, make_domain(), make_database(), test_input_identity()) .empty()); const std::array coordinates{ std::numeric_limits::quiet_NaN(), -2.5, 3.75, 2.25, -2.5, 3.75, }; write_double_dataset(path, "/model/nodes/coordinates", coordinates); const auto read = fesa::read_hdf5_results(path); EXPECT_FALSE(read.database.has_value()); EXPECT_FALSE(read.model.has_value()); EXPECT_TRUE( has_results_error(read.diagnostics, "hdf5.invalid_model_data")); } TEST(Hdf5, RejectsSerializedZeroLengthElement) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-zero-length-element.h5"; std::filesystem::remove(path); ASSERT_TRUE(fesa::write_hdf5( path, make_domain(), make_database(), test_input_identity()) .empty()); const std::array coordinates{ 1.25, -2.5, 3.75, 1.25, -2.5, 3.75}; write_double_dataset(path, "/model/nodes/coordinates", coordinates); const auto read = fesa::read_hdf5_results(path); EXPECT_FALSE(read.database.has_value()); EXPECT_FALSE(read.model.has_value()); EXPECT_TRUE( has_results_error(read.diagnostics, "hdf5.invalid_model_data")); } TEST(Hdf5, RejectsSerializedOrientationParallelToElement) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-parallel-orientation.h5"; std::filesystem::remove(path); ASSERT_TRUE(fesa::write_hdf5( path, make_domain(), make_database(), test_input_identity()) .empty()); const std::array orientations{ 1.0, 0.0, 0.0, 0.0, 1.0, 0.0}; write_double_dataset( path, "/model/sections/orientation", orientations); const auto read = fesa::read_hdf5_results(path); EXPECT_FALSE(read.database.has_value()); EXPECT_FALSE(read.model.has_value()); EXPECT_TRUE( has_results_error(read.diagnostics, "hdf5.invalid_model_data")); } TEST(Hdf5, RejectsDuplicateSerializedNodeOrigins) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-duplicate-node-origins.h5"; std::filesystem::remove(path); ASSERT_TRUE(fesa::write_hdf5( path, make_domain(), make_database(), test_input_identity()) .empty()); const std::array labels{1001, 1001}; write_int64_dataset(path, "/model/nodes/local_label", labels); const auto read = fesa::read_hdf5_results(path); EXPECT_FALSE(read.database.has_value()); EXPECT_FALSE(read.model.has_value()); EXPECT_TRUE( has_results_error(read.diagnostics, "hdf5.invalid_model_data")); } TEST(Hdf5, RejectsDuplicateSerializedBoundaryConditions) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-duplicate-boundary-conditions.h5"; std::filesystem::remove(path); ASSERT_TRUE(fesa::write_hdf5( path, make_domain(), make_database(), test_input_identity()) .empty()); const std::array node_ids{42, 42}; const std::array dofs{1, 1}; write_int64_dataset( path, "/analysis/steps/0/boundary_conditions/node_ids", node_ids); write_uint8_dataset( path, "/analysis/steps/0/boundary_conditions/dofs", dofs); const auto read = fesa::read_hdf5_results(path); EXPECT_FALSE(read.database.has_value()); EXPECT_FALSE(read.model.has_value()); EXPECT_TRUE( has_results_error(read.diagnostics, "hdf5.invalid_model_data")); } TEST(Hdf5, RejectsSerializedLocalFrameThatDisagreesWithModel) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-serialized-wrong-local-frame.h5"; std::filesystem::remove(path); ASSERT_TRUE(fesa::write_hdf5( path, make_domain(), make_database(), test_input_identity()) .empty()); const std::array local_frames{ 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, }; write_double_dataset( path, "/results/steps/0/frames/0/element/beam/local_frame", local_frames); const auto read = fesa::read_hdf5_results(path); EXPECT_FALSE(read.database.has_value()); EXPECT_FALSE(read.model.has_value()); EXPECT_TRUE(has_results_error( read.diagnostics, "hdf5.result_element_mismatch")); } TEST(Hdf5, RejectsNonfiniteSerializedShearArea) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-nonfinite-shear-area.h5"; std::filesystem::remove(path); ASSERT_TRUE(fesa::write_hdf5( path, make_domain(), make_database(), test_input_identity()) .empty()); const std::array shear_areas{ std::numeric_limits::infinity(), 0.05, }; write_double_dataset(path, "/model/sections/shear_area_y", shear_areas); const auto read = fesa::read_hdf5_results(path); EXPECT_FALSE(read.database.has_value()); EXPECT_FALSE(read.model.has_value()); EXPECT_TRUE( has_results_error(read.diagnostics, "hdf5.invalid_model_data")); } TEST(Hdf5, RejectsNonpositiveSerializedShearArea) { const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" / "Temporary" / "fesa-nonpositive-shear-area.h5"; std::filesystem::remove(path); ASSERT_TRUE(fesa::write_hdf5( path, make_domain(), make_database(), test_input_identity()) .empty()); const std::array shear_areas{-0.031, 0.05}; write_double_dataset(path, "/model/sections/shear_area_y", shear_areas); const auto read = fesa::read_hdf5_results(path); EXPECT_FALSE(read.database.has_value()); EXPECT_FALSE(read.model.has_value()); EXPECT_TRUE( has_results_error(read.diagnostics, "hdf5.invalid_model_data")); } } // namespace