#include #include #include #include #include #include #include #include #include #include #include #include #include #include "fesa/analysis/analysis_model.h" #include "fesa/analysis/analysis_state.h" #include "fesa/fem/dof_manager.h" #include "fesa/model/domain.h" #include "io/hdf5/hdf5_atomic_file.h" #include "io/hdf5/hdf5_result_writer.h" #include "io/hdf5/hdf5_self_check.h" namespace { struct WriterFixture { std::unique_ptr domain; std::unique_ptr dofs; std::unique_ptr state; }; fesa::ModelDefinition MakeDefinition(const std::filesystem::path& source) { fesa::ModelDefinition definition{}; definition.source_path = source; definition.source_content_identity = "fnv1a64:0123456789abcdef"; definition.nodes = {{{"Beam-1", 101, "101"}, {0.0, 0.0, 0.0}, {source, 10U}}, {{"Beam-1", 202, "202"}, {3.0, 4.0, 0.0}, {source, 11U}}}; definition.materials = {{"Steel", 210.0e9, 0.3, {source, 20U}}}; definition.sections = {{"General", 0.02, 3.0e-5, 0.0, 4.0e-5, 5.0e-5, {0.0, 0.0, 1.0}, {{-0.1, 0.2}}, {source, 30U}}}; definition.elements = { {{"Beam-1", 303, "303"}, {0U, 1U}, 0U, 0U, {source, 40U}}}; definition.steps = {{"Step-1", {}, {}, 0.1, 1.0, 0.01, 1.0, {source, 50U}}}; return definition; } WriterFixture MakeFixture(const std::filesystem::path& source) { auto domain_result = fesa::Domain::Create(MakeDefinition(source)); if (!domain_result.HasValue()) { throw std::runtime_error{"Component fixture Domain construction failed."}; } auto domain = std::make_unique(std::move(domain_result.Value())); auto model_result = fesa::AnalysisModel::Create(*domain); if (!model_result.HasValue()) { throw std::runtime_error{ "Component fixture AnalysisModel construction failed."}; } const fesa::AnalysisModel model = std::move(model_result.Value()); auto dofs_result = fesa::DofManager::Create(model); if (!dofs_result.HasValue()) { throw std::runtime_error{ "Component fixture DofManager construction failed."}; } auto dofs = std::make_unique(std::move(dofs_result.Value())); auto state = std::make_unique( fesa::AnalysisState::Create(*dofs, {"Step-1", 0U})); for (std::size_t index = 0U; index < state->Displacement().Size(); ++index) { state->Displacement()[index] = 0.25 + static_cast(index); state->ExternalForce()[index] = 100.0 + static_cast(index); state->InternalForce()[index] = 200.0 + static_cast(index); state->Residual()[index] = 100.0 + static_cast(index); state->Reaction()[index] = 100.0 + static_cast(index); } state->EndpointResults() = {{0U, 0, domain->Nodes()[0U].source_id, {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}, {11.0, 12.0, 13.0, 14.0}}, {0U, 1, domain->Nodes()[1U].source_id, {7.0, 8.0, 9.0, 10.0, 11.0, 12.0}, {15.0, 16.0, 17.0, 18.0}}}; state->GaussResults() = { {0U, 1, {0.01, 0.02, 0.03, 0.04}, {21.0, 22.0, 23.0, 24.0}}, {0U, 2, {0.05, 0.06, 0.07, 0.08}, {25.0, 26.0, 27.0, 28.0}}}; state->StressResults() = {{0U, 1, 1U, -0.1, 0.2, 31.0, "input"}, {0U, 2, 1U, -0.1, 0.2, 32.0, "input"}}; return {std::move(domain), std::move(dofs), std::move(state)}; } std::vector ReadBytes(const std::filesystem::path& path) { std::ifstream input{path, std::ios::binary}; return {std::istreambuf_iterator{input}, std::istreambuf_iterator{}}; } void WriteBytes(const std::filesystem::path& path, const std::vector& bytes) { std::ofstream output{path, std::ios::binary | std::ios::trunc}; output.write(bytes.data(), static_cast(bytes.size())); if (!output) { throw std::runtime_error{"Unable to write component-test bytes."}; } } void ExpectOutputFailure(const fesa::Status& status, const std::string& expected_code) { ASSERT_FALSE(status.IsOk()); EXPECT_EQ(status.Category(), fesa::FailureCategory::kOutput); ASSERT_EQ(status.Diagnostics().size(), 1U); EXPECT_EQ(status.Diagnostics()[0U].code, expected_code); } } // namespace TEST(Hdf5WriterComponents, BuildsStableBackendNeutralBeamDatasetPlans) { const auto directory = std::filesystem::temp_directory_path() / "fesa-hdf5-component-plan"; auto fixture = MakeFixture(directory / "beam.inp"); auto plan_result = fesa::hdf5_internal::BuildWriterPlan( directory / "results.h5", *fixture.domain, *fixture.state, {}); ASSERT_TRUE(plan_result.HasValue()); const auto& datasets = plan_result.Value().result_datasets; ASSERT_EQ(datasets.size(), 6U); EXPECT_EQ(datasets[0U].path, "/steps/Step-1/frames/0/nodal/displacement"); EXPECT_EQ(datasets[0U].dimensions, std::vector({2U, 6U})); EXPECT_EQ(datasets[2U].path, "/steps/Step-1/frames/0/element/end_force_local"); EXPECT_EQ(datasets[2U].dimensions, std::vector({1U, 2U, 6U})); EXPECT_EQ(datasets[5U].path, "/steps/Step-1/frames/0/element/generalized_resultant"); EXPECT_TRUE(fesa::hdf5_internal::ValidateFiniteInventory(datasets).IsOk()); } TEST(Hdf5WriterComponents, RejectsNonfiniteDatasetPlanInventory) { fesa::hdf5_internal::DoubleDatasetPlan plan{ "/steps/Step-1/frames/0/global/energy", {1U}, {std::numeric_limits::quiet_NaN()}, "PHYSICAL_STRAIN_ENERGY", "force*length", "global", "global"}; ExpectOutputFailure( fesa::hdf5_internal::ValidateFiniteInventory({std::move(plan)}), "invalid-result-dataset-plan"); } TEST(Hdf5WriterComponents, SelfCheckFailurePreservesExistingFinalThroughAtomicSeam) { const auto directory = std::filesystem::temp_directory_path() / "fesa-hdf5-component-atomic"; std::error_code ignored; std::filesystem::remove_all(directory, ignored); ASSERT_TRUE(std::filesystem::create_directory(directory)); const auto final = directory / "results.h5"; const auto candidate = directory / ".results.h5.candidate"; const std::vector sentinel{'v', 'a', 'l', 'i', 'd'}; WriteBytes(final, sentinel); WriteBytes(candidate, {'n', 'o', 't', '-', 'h', 'd', 'f', '5'}); auto fixture = MakeFixture(directory / "beam.inp"); auto plan_result = fesa::hdf5_internal::BuildWriterPlan( final, *fixture.domain, *fixture.state, {}); ASSERT_TRUE(plan_result.HasValue()); const fesa::Status self_check = fesa::hdf5_internal::SelfCheck(candidate, *fixture.domain, *fixture.state, 0U, plan_result.Value().model_data); ExpectOutputFailure(self_check, "hdf5-write-failure"); const fesa::Status finalize = fesa::hdf5_internal::AtomicFinalizeValidatedCandidate(candidate, final, self_check); ExpectOutputFailure(finalize, "hdf5-write-failure"); EXPECT_EQ(ReadBytes(final), sentinel); EXPECT_FALSE(std::filesystem::exists(candidate)); std::filesystem::remove_all(directory, ignored); }