#include #include #include #include #include #include #include #include #include #include #include #include #include "fesa/app/fesa_application.h" #include "reference_comparison.h" #ifndef FESA_TEST_SOURCE_DIR #error FESA_TEST_SOURCE_DIR must identify the repository root. #endif #ifndef FESA_TEST_BINARY_DIR #error FESA_TEST_BINARY_DIR must identify the CMake binary root. #endif namespace { constexpr const char* kStressPath = "/steps/Step-1/frames/0/element/stress_s11"; constexpr std::size_t kExpectedRowCount = 212U; constexpr std::size_t kExpectedMetricCount = 6U; class Hdf5Handle { public: using Closer = herr_t (*)(hid_t); Hdf5Handle(const hid_t value, Closer closer) : value_{value}, closer_{closer} {} Hdf5Handle(const Hdf5Handle&) = delete; Hdf5Handle& operator=(const Hdf5Handle&) = delete; ~Hdf5Handle() { if (value_ >= 0 && closer_ != nullptr) { (void)closer_(value_); } } hid_t Get() const noexcept { return value_; } private: hid_t value_; Closer closer_; }; struct ReferenceSnapshotEntry { std::filesystem::path relative_path; bool is_directory; std::string bytes; std::filesystem::file_time_type last_write_time; }; std::string ReadBytes(const std::filesystem::path& path) { std::ifstream stream{path, std::ios::binary}; if (!stream) { throw std::runtime_error{"Unable to read reference evidence: " + path.string()}; } return {std::istreambuf_iterator{stream}, std::istreambuf_iterator{}}; } std::vector SnapshotTree( const std::filesystem::path& root) { std::vector entries; for (const auto& entry : std::filesystem::recursive_directory_iterator{root}) { const bool is_directory = entry.is_directory(); if (!is_directory && !entry.is_regular_file()) { throw std::runtime_error{"Unexpected reference-tree entry type."}; } entries.push_back({std::filesystem::relative(entry.path(), root), is_directory, is_directory ? std::string{} : ReadBytes(entry.path()), entry.last_write_time()}); } std::sort(entries.begin(), entries.end(), [](const ReferenceSnapshotEntry& left, const ReferenceSnapshotEntry& right) { return left.relative_path.generic_string() < right.relative_path.generic_string(); }); return entries; } void ExpectTreeUnchanged(const std::vector& before, const std::vector& after) { ASSERT_EQ(after.size(), before.size()); for (std::size_t index = 0U; index < before.size(); ++index) { EXPECT_EQ(after[index].relative_path, before[index].relative_path); EXPECT_EQ(after[index].is_directory, before[index].is_directory); EXPECT_EQ(after[index].bytes, before[index].bytes) << before[index].relative_path.string(); EXPECT_EQ(after[index].last_write_time, before[index].last_write_time) << before[index].relative_path.string(); } } double Norm(const std::array& value) { return std::sqrt(value[0U] * value[0U] + value[1U] * value[1U] + value[2U] * value[2U]); } std::array Sum(const std::array& left, const std::array& right) { return {left[0U] + right[0U], left[1U] + right[1U], left[2U] + right[2U]}; } std::size_t StressRowCount(const std::filesystem::path& results) { const hid_t file_id = H5Fopen(results.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); if (file_id < 0) { throw std::runtime_error{"Unable to open authoritative HDF5 output."}; } const Hdf5Handle file{file_id, H5Fclose}; if (H5Lexists(file.Get(), kStressPath, H5P_DEFAULT) <= 0) { throw std::runtime_error{"Mandatory stress_s11 dataset is missing."}; } const hid_t dataset_id = H5Dopen2(file.Get(), kStressPath, H5P_DEFAULT); if (dataset_id < 0) { throw std::runtime_error{"Unable to open mandatory stress_s11 dataset."}; } const Hdf5Handle dataset{dataset_id, H5Dclose}; const hid_t space_id = H5Dget_space(dataset.Get()); if (space_id < 0) { throw std::runtime_error{"Unable to inspect stress_s11 dataspace."}; } const Hdf5Handle space{space_id, H5Sclose}; if (H5Sget_simple_extent_ndims(space.Get()) != 1) { throw std::runtime_error{"stress_s11 must be a flat row dataset."}; } hsize_t count = 0U; if (H5Sget_simple_extent_dims(space.Get(), &count, nullptr) < 0) { throw std::runtime_error{"Unable to read stress_s11 extent."}; } return static_cast(count); } } // namespace TEST(B33ReferenceComparison, GeneratesAuthoritativeHdf5AndComparisonEvidence) { const std::filesystem::path source_root{FESA_TEST_SOURCE_DIR}; const std::filesystem::path binary_root{FESA_TEST_BINARY_DIR}; const auto reference_directory = source_root / "reference" / "cantilever beam"; const auto input = reference_directory / "cantilever beam.inp"; const auto output_directory = binary_root / "reference" / "cantilever-beam-b33"; const auto results = output_directory / "results.h5"; const auto comparison = output_directory / "comparison.json"; // Only the exact build-local evidence directory is reset; the approved // reference tree is snapshotted and subsequently opened read-only. std::error_code error; std::filesystem::remove_all(output_directory, error); error.clear(); ASSERT_TRUE(std::filesystem::create_directories(output_directory, error)); ASSERT_FALSE(error); const auto reference_before = SnapshotTree(reference_directory); ASSERT_EQ(reference_before.size(), 4U); EXPECT_EQ(reference_before[0U].relative_path, std::filesystem::path{"cantilever beam displacements.csv"}); EXPECT_EQ(reference_before[1U].relative_path, std::filesystem::path{"cantilever beam elemental forces.csv"}); EXPECT_EQ(reference_before[2U].relative_path, std::filesystem::path{"cantilever beam reactions.csv"}); EXPECT_EQ(reference_before[3U].relative_path, std::filesystem::path{"cantilever beam.inp"}); fesa::FesaApplication application; // FesaApplication receives application operands/options; main strips argv[0]. ASSERT_EQ(application.Run({input.string(), "--output", results.string()}), 0); ASSERT_TRUE(std::filesystem::is_regular_file(results)); ASSERT_GT(H5Fis_hdf5(results.string().c_str()), 0); EXPECT_GT(StressRowCount(results), 0U); auto comparison_result = fesa::test::ReferenceComparison::Compare(results, reference_directory); ASSERT_TRUE(comparison_result.HasValue()); const auto& report = comparison_result.Value(); ASSERT_TRUE(report.passed); ASSERT_EQ(report.rows.size(), kExpectedRowCount); ASSERT_EQ(report.metrics.size(), kExpectedMetricCount); EXPECT_TRUE(std::all_of( report.rows.begin(), report.rows.end(), [](const fesa::test::RowDecision& row) { return row.passed && std::isfinite(row.absolute_error) && std::isfinite(row.tolerance) && row.tolerance >= 0.0 && std::isfinite(row.reference_scale) && std::isfinite(row.near_zero_band) && std::isfinite(row.relative_error) && row.fesa.model_id == "cantilever-beam-b33" && row.reference.model_id == "cantilever-beam-b33" && row.fesa.step_name == "Step-1" && row.reference.step_name == "Step-1" && row.fesa.frame_index == 0U && row.reference.frame_index == 0U && row.fesa.instance_name == "PART-1_1-1" && row.reference.instance_name == "PART-1_1-1" && !row.fesa.hdf5_dataset_path.empty(); })); EXPECT_TRUE( std::all_of(report.metrics.begin(), report.metrics.end(), [](const fesa::test::ComponentMetrics& metric) { return std::isfinite(metric.reference_scale) && std::isfinite(metric.near_zero_band) && std::isfinite(metric.maximum_absolute_error) && std::isfinite(metric.relative_rms) && metric.rms_passed && metric.passed && metric.row_count > 0U && !metric.components.empty(); })); EXPECT_FALSE(report.stress_comparison_applicable); EXPECT_NE(report.stress_comparison_reason.find("N/A"), std::string::npos); EXPECT_NE(report.stress_comparison_reason.find("HDF5"), std::string::npos); EXPECT_TRUE(report.physics_evidence.endpoint_consistency_passed); EXPECT_TRUE(std::isfinite(report.physics_evidence.free_residual_norm)); EXPECT_LE(report.physics_evidence.free_residual_norm, 1.0e-3); EXPECT_LE(Norm(Sum(report.physics_evidence.applied_force, report.physics_evidence.reaction_force)), 1.0e-3); EXPECT_LE(Norm(Sum(report.physics_evidence.applied_moment_about_origin, report.physics_evidence.reaction_moment_about_origin)), 1.0e-2); ASSERT_TRUE(fesa::test::ReferenceComparison::WriteDeterministicJson( report, comparison) .IsOk()); ASSERT_TRUE(std::filesystem::is_regular_file(comparison)); const std::string json = ReadBytes(comparison); EXPECT_NE(json.find("\"stress_comparison_applicable\":false"), std::string::npos); EXPECT_NE(json.find("\"physics_evidence\""), std::string::npos); EXPECT_NE(json.find("\"source_element_label\""), std::string::npos); EXPECT_NE(json.find("\"family_identity\""), std::string::npos); EXPECT_NE(json.find("\"row_count\""), std::string::npos); std::vector generated_names; for (const auto& entry : std::filesystem::directory_iterator{output_directory}) { generated_names.push_back(entry.path().filename().string()); } std::sort(generated_names.begin(), generated_names.end()); EXPECT_EQ(generated_names, (std::vector{"comparison.json", "results.h5"})); ExpectTreeUnchanged(reference_before, SnapshotTree(reference_directory)); }