feat(cpp-object-oriented-modular-refactoring): step 6 - io-application-google-style

This commit is contained in:
KOKO\Mimi
2026-08-16 06:59:50 +09:00
parent bb178c9d3c
commit 83fd1d1c7e
31 changed files with 10499 additions and 11445 deletions
+189 -217
View File
@@ -1,10 +1,5 @@
#include "reference_comparison.hpp"
#include "fesa/app/fesa_application.hpp"
#include <hdf5.h>
#include <gtest/gtest.h>
#include <hdf5.h>
#include <algorithm>
#include <array>
@@ -17,6 +12,9 @@
#include <string>
#include <vector>
#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
@@ -27,250 +25,224 @@
namespace {
constexpr const char* kStressPath =
"/steps/Step-1/frames/0/element/stress_s11";
constexpr const char* kStressPath = "/steps/Step-1/frames/0/element/stress_s11";
constexpr std::size_t kExpectedRowCount = 176U;
constexpr std::size_t kExpectedMetricCount = 16U;
class Hdf5Handle {
public:
using Closer = herr_t (*)(hid_t);
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_);
}
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_; }
hid_t Get() const noexcept { return value_; }
private:
hid_t value_;
Closer closer_;
private:
hid_t value_;
Closer closer_;
};
struct ReferenceSnapshotEntry {
std::filesystem::path relativePath;
bool isDirectory;
std::string bytes;
std::filesystem::file_time_type lastWriteTime;
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<char>{stream},
std::istreambuf_iterator<char>{}};
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<char>{stream},
std::istreambuf_iterator<char>{}};
}
std::vector<ReferenceSnapshotEntry> snapshotTree(
std::vector<ReferenceSnapshotEntry> SnapshotTree(
const std::filesystem::path& root) {
std::vector<ReferenceSnapshotEntry> entries;
for (const auto& entry : std::filesystem::recursive_directory_iterator{root}) {
const bool isDirectory = entry.is_directory();
if (!isDirectory && !entry.is_regular_file()) {
throw std::runtime_error{"Unexpected reference-tree entry type."};
}
entries.push_back({
std::filesystem::relative(entry.path(), root),
isDirectory,
isDirectory ? std::string{} : readBytes(entry.path()),
entry.last_write_time()});
std::vector<ReferenceSnapshotEntry> 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."};
}
std::sort(
entries.begin(),
entries.end(),
[](const ReferenceSnapshotEntry& left,
const ReferenceSnapshotEntry& right) {
return left.relativePath.generic_string() <
right.relativePath.generic_string();
});
return entries;
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<ReferenceSnapshotEntry>& before,
const std::vector<ReferenceSnapshotEntry>& after) {
ASSERT_EQ(after.size(), before.size());
for (std::size_t index = 0U; index < before.size(); ++index) {
EXPECT_EQ(after[index].relativePath, before[index].relativePath);
EXPECT_EQ(after[index].isDirectory, before[index].isDirectory);
EXPECT_EQ(after[index].bytes, before[index].bytes)
<< before[index].relativePath.string();
EXPECT_EQ(after[index].lastWriteTime, before[index].lastWriteTime)
<< before[index].relativePath.string();
}
void ExpectTreeUnchanged(const std::vector<ReferenceSnapshotEntry>& before,
const std::vector<ReferenceSnapshotEntry>& 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<double, 3>& value) {
return std::sqrt(
value[0U] * value[0U] + value[1U] * value[1U] +
value[2U] * value[2U]);
double Norm(const std::array<double, 3>& value) {
return std::sqrt(value[0U] * value[0U] + value[1U] * value[1U] +
value[2U] * value[2U]);
}
std::array<double, 3> sum(
const std::array<double, 3>& left,
const std::array<double, 3>& right) {
return {
left[0U] + right[0U],
left[1U] + right[1U],
left[2U] + right[2U]};
std::array<double, 3> Sum(const std::array<double, 3>& left,
const std::array<double, 3>& 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 fileId =
H5Fopen(results.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
if (fileId < 0) {
throw std::runtime_error{"Unable to open authoritative HDF5 output."};
}
const Hdf5Handle file{fileId, H5Fclose};
if (H5Lexists(file.get(), kStressPath, H5P_DEFAULT) <= 0) {
throw std::runtime_error{"Mandatory stress_s11 dataset is missing."};
}
const hid_t datasetId = H5Dopen2(file.get(), kStressPath, H5P_DEFAULT);
if (datasetId < 0) {
throw std::runtime_error{"Unable to open mandatory stress_s11 dataset."};
}
const Hdf5Handle dataset{datasetId, H5Dclose};
const hid_t spaceId = H5Dget_space(dataset.get());
if (spaceId < 0) {
throw std::runtime_error{"Unable to inspect stress_s11 dataspace."};
}
const Hdf5Handle space{spaceId, 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<std::size_t>(count);
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<std::size_t>(count);
}
} // namespace
} // namespace
TEST(B33ReferenceComparison,
GeneratesAuthoritativeHdf5AndComparisonEvidence) {
const std::filesystem::path sourceRoot{FESA_TEST_SOURCE_DIR};
const std::filesystem::path binaryRoot{FESA_TEST_BINARY_DIR};
const auto referenceDirectory =
sourceRoot / "reference" / "cantilever beam";
const auto input = referenceDirectory / "cantilever beam.inp";
const auto outputDirectory =
binaryRoot / "reference" / "cantilever-beam-b33";
const auto results = outputDirectory / "results.h5";
const auto comparison = outputDirectory / "comparison.json";
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(outputDirectory, error);
error.clear();
ASSERT_TRUE(std::filesystem::create_directories(outputDirectory, error));
ASSERT_FALSE(error);
const auto referenceBefore = snapshotTree(referenceDirectory);
ASSERT_EQ(referenceBefore.size(), 4U);
EXPECT_EQ(
referenceBefore[0U].relativePath,
std::filesystem::path{"cantilever beam displacements.csv"});
EXPECT_EQ(
referenceBefore[1U].relativePath,
std::filesystem::path{"cantilever beam elemental forces.csv"});
EXPECT_EQ(
referenceBefore[2U].relativePath,
std::filesystem::path{"cantilever beam reactions.csv"});
EXPECT_EQ(
referenceBefore[3U].relativePath,
std::filesystem::path{"cantilever beam.inp"});
// 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);
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 comparisonResult = fesa::test::ReferenceComparison::compare(
results, referenceDirectory);
ASSERT_TRUE(comparisonResult.HasValue());
const auto& report = comparisonResult.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.absoluteError) &&
std::isfinite(row.tolerance) && row.tolerance > 0.0 &&
row.fesa.modelId == "cantilever-beam-b33" &&
row.reference.modelId == "cantilever-beam-b33" &&
row.fesa.stepName == "Step-1" &&
row.reference.stepName == "Step-1" &&
row.fesa.frameIndex == 0U &&
row.reference.frameIndex == 0U &&
row.fesa.instanceName == "PART-1_1-1" &&
row.reference.instanceName == "PART-1_1-1" &&
!row.fesa.hdf5DatasetPath.empty();
}));
EXPECT_TRUE(std::all_of(
report.metrics.begin(),
report.metrics.end(),
[](const fesa::test::ComponentMetrics& metric) {
return std::isfinite(metric.referenceScale) &&
std::isfinite(metric.maximumAbsoluteError) &&
std::isfinite(metric.maximumNormalizedError) &&
std::isfinite(metric.rmsError) &&
std::isfinite(metric.normError) &&
metric.maximumNormalizedError <= 1.0;
}));
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 &&
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.maximum_absolute_error) &&
std::isfinite(metric.maximum_normalized_error) &&
std::isfinite(metric.rms_error) &&
std::isfinite(metric.norm_error) &&
metric.maximum_normalized_error <= 1.0;
}));
EXPECT_FALSE(report.stressComparisonApplicable);
EXPECT_NE(report.stressComparisonReason.find("N/A"), std::string::npos);
EXPECT_NE(report.stressComparisonReason.find("HDF5"), std::string::npos);
EXPECT_TRUE(report.physicsEvidence.endpointConsistencyPassed);
EXPECT_TRUE(std::isfinite(report.physicsEvidence.freeResidualNorm));
EXPECT_LE(report.physicsEvidence.freeResidualNorm, 1.0e-3);
EXPECT_LE(
norm(sum(
report.physicsEvidence.appliedForce,
report.physicsEvidence.reactionForce)),
1.0e-3);
EXPECT_LE(
norm(sum(
report.physicsEvidence.appliedMomentAboutOrigin,
report.physicsEvidence.reactionMomentAboutOrigin)),
1.0e-2);
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);
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);
std::vector<std::string> generatedNames;
for (const auto& entry :
std::filesystem::directory_iterator{outputDirectory}) {
generatedNames.push_back(entry.path().filename().string());
}
std::sort(generatedNames.begin(), generatedNames.end());
EXPECT_EQ(
generatedNames,
(std::vector<std::string>{"comparison.json", "results.h5"}));
std::vector<std::string> 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<std::string>{"comparison.json", "results.h5"}));
expectTreeUnchanged(referenceBefore, snapshotTree(referenceDirectory));
ExpectTreeUnchanged(reference_before, SnapshotTree(reference_directory));
}