Files
FESADev/tests/unit/io/hdf5/eigen_hdf5_results_writer_test.cpp
T

205 lines
9.1 KiB
C++

#define NOMINMAX
#include "fesa/io/hdf5/eigen_hdf5_results_writer.h"
#include <Windows.h>
#include <gtest/gtest.h>
#include <hdf5.h>
#include <array>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iterator>
#include <limits>
#include <memory>
#include <string>
#include "fesa/analysis/linear_frequency_procedure.h"
#include "fesa/model/domain.h"
#include "io/hdf5/eigen_hdf5_self_check.h"
#include "io/hdf5/mitc4_drilling_diagnostics.h"
#include "mitc4_fault_test_helpers.h"
namespace {
fesa::ModelDefinition Definition(const std::filesystem::path& source) {
fesa::ModelDefinition definition{};
definition.source_path = source;
definition.source_content_identity = "fnv1a64:0123456789abcdef";
definition.nodes = {{{"I", 1, "1"}, {-1.0, -1.0, 0.0}, {source, 1U}},
{{"I", 2, "2"}, {1.0, -1.0, 0.0}, {source, 2U}},
{{"I", 3, "3"}, {1.0, 1.0, 0.0}, {source, 3U}},
{{"I", 4, "4"}, {-1.0, 1.0, 0.0}, {source, 4U}}};
definition.materials = {{"Steel", 12.0, 0.0, {source, 5U}}};
definition.shell_sections = {{"S", 1.0, 0U, {source, 6U}}};
definition.shell_elements = {{{"I", 1, "1"},
fesa::ShellSourceElementType::kS4,
{0U, 1U, 2U, 3U},
0U,
0U,
{source, 7U}}};
for (std::size_t node = 0U; node < 4U; ++node) {
definition.shell_node_initial_frames.push_back(
{static_cast<fesa::EntityIndex>(node),
{0.0, 0.0, 1.0},
{1.0, 0.0, 0.0},
{0.0, 1.0, 0.0}});
}
definition.densities = {{0U, 1.0, {source, 8U}}};
definition.frequency_step =
fesa::FrequencyStepDefinition{"Step-2",
"STEP-2",
{source, 9U},
{source, 10U},
{source, 11U},
1,
false,
false,
false,
false,
false,
false,
fesa::FrequencyEigensolver::kLanczos,
fesa::FrequencyNormalization::kMass,
{}};
return definition;
}
TEST(EigenHdf5ResultsWriter, WritesModalSchemaAndRejectsTamperedState) {
const auto path =
std::filesystem::temp_directory_path() / "fesa-eigen-writer-test.h5";
std::error_code ignored;
std::filesystem::remove(path, ignored);
const auto source = path.parent_path() / "fesa-eigen-writer-test.inp";
auto domain_result = fesa::Domain::Create(Definition(source));
ASSERT_TRUE(domain_result.HasValue());
fesa::Domain domain{std::move(domain_result.Value())};
fesa::EigenHdf5ResultsWriter writer;
auto execution = fesa::LinearFrequencyProcedureExecutor{}.Execute(domain);
ASSERT_TRUE(execution.HasValue());
fesa::EigenAnalysisState state = std::move(execution.Value());
ASSERT_TRUE(writer.Write(path, domain, state, {}).IsOk());
const hid_t file =
H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
ASSERT_GE(file, 0);
const auto drilling = fesa::hdf5_internal::ReadDrillingDiagnostics(file);
ASSERT_TRUE(drilling);
EXPECT_EQ(drilling->formulation, fesa::Mitc4DrillingModeName(*state.drilling_mode));
EXPECT_TRUE(drilling->frames.empty());
EXPECT_GT(H5Lexists(file, "/steps/Step-1/eigen/modes", H5P_DEFAULT), 0);
const hid_t controls =
H5Dopen2(file, "/steps/Step-1/eigen/backend/base_fpm", H5P_DEFAULT);
ASSERT_GE(controls, 0);
std::array<std::int64_t, 128> stored_controls{};
ASSERT_GE(H5Dread(controls, H5T_NATIVE_INT64, H5S_ALL, H5S_ALL,
H5P_DEFAULT, stored_controls.data()),
0);
EXPECT_EQ(stored_controls[5U], 0);
EXPECT_GE(H5Dclose(controls), 0);
EXPECT_GE(H5Fclose(file), 0);
std::ifstream original_file{path, std::ios::binary};
const std::string original{std::istreambuf_iterator<char>{original_file}, {}};
original_file.close();
const std::vector<std::function<void(fesa::EigenAnalysisState&)>> mutations{
[](auto& candidate) {
candidate.vectors(0U, 0U) = std::numeric_limits<double>::quiet_NaN();
},
[](auto& candidate) { candidate.eigenpair_checks[0].rho_b = 1e-5; },
[](auto& candidate) { candidate.eigenpair_checks[0].eigenvalue += 1.0; },
[](auto& candidate) { candidate.modes[0].cluster_id = 7U; },
[](auto& candidate) { candidate.constraint_mask[0] = 1U; },
[](auto& candidate) { candidate.nodal_reduction[0].rcond_inf = 1e-15; },
[](auto& candidate) { candidate.backend.base_fpm[5U] = 1; },
[](auto& candidate) { candidate.eta_gram = 1e-5; },
[](auto& candidate) { candidate.drilling_mode.reset(); }};
for (const auto& mutate : mutations) {
auto candidate = state;
mutate(candidate);
EXPECT_FALSE(writer.Write(path, domain, candidate, {}).IsOk());
}
std::ifstream retained_file{path, std::ios::binary};
const std::string retained{std::istreambuf_iterator<char>{retained_file}, {}};
EXPECT_TRUE(retained == original);
retained_file.close();
// Denying delete sharing forces the actual atomic replacement to fail after
// writing and validating a fresh candidate, while retaining a valid final.
const HANDLE lock = CreateFileW(path.c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
ASSERT_NE(lock, INVALID_HANDLE_VALUE);
const auto locked_write = writer.Write(path, domain, state, {});
EXPECT_NE(CloseHandle(lock), 0);
ASSERT_FALSE(locked_write.IsOk());
ASSERT_FALSE(locked_write.Diagnostics().empty());
EXPECT_EQ(locked_write.Diagnostics()[0].code, "hdf5-finalization-failure");
std::ifstream locked_final{path, std::ios::binary};
const std::string after_failure{std::istreambuf_iterator<char>{locked_final},
{}};
EXPECT_EQ(after_failure, original);
locked_final.close();
ASSERT_TRUE(
fesa::hdf5_internal::SelfCheckEigenFile(path, domain, state, {}).IsOk());
for (const auto mode : {fesa::Mitc4DrillingMode::kDirectSpring,
fesa::Mitc4DrillingMode::kLegacyCorotatedTwist}) {
state.drilling_mode = mode;
ASSERT_TRUE(writer.Write(path, domain, state, {}).IsOk());
fesa::hdf5_internal::Hdf5Handle stored_file{
H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), H5Fclose};
const auto stored = fesa::hdf5_internal::ReadDrillingDiagnostics(stored_file.Get());
ASSERT_TRUE(stored);
EXPECT_EQ(stored->formulation, fesa::Mitc4DrillingModeName(mode));
EXPECT_TRUE(stored->frames.empty());
}
{
fesa::hdf5_internal::Hdf5Handle stored_file{
H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose};
fesa::hdf5_internal::Hdf5Handle metadata{
H5Gopen2(stored_file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
ASSERT_GE(H5Adelete(metadata.Get(), "mitc4_drilling_formulation"), 0);
ASSERT_GE(H5Adelete(metadata.Get(), "drilling_diagnostics_version"), 0);
}
EXPECT_FALSE(fesa::hdf5_internal::SelfCheckEigenFile(path, domain, state, {}).IsOk());
ASSERT_TRUE(writer.Write(path, domain, state, {}).IsOk());
const hid_t corrupt_file =
H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT);
ASSERT_GE(corrupt_file, 0);
const hid_t nodes = H5Dopen2(corrupt_file, "/model/nodes", H5P_DEFAULT);
ASSERT_GE(nodes, 0);
const hid_t id_type = H5Tcreate(H5T_COMPOUND, sizeof(std::uint64_t));
ASSERT_GE(H5Tinsert(id_type, "internal_node_id", 0U, H5T_NATIVE_UINT64), 0);
const std::array<std::uint64_t, 4> duplicate_ids{0U, 0U, 2U, 3U};
ASSERT_GE(H5Dwrite(nodes, id_type, H5S_ALL, H5S_ALL, H5P_DEFAULT,
duplicate_ids.data()),
0);
EXPECT_GE(H5Tclose(id_type), 0);
EXPECT_GE(H5Dclose(nodes), 0);
EXPECT_GE(H5Fclose(corrupt_file), 0);
EXPECT_FALSE(
fesa::hdf5_internal::SelfCheckEigenFile(path, domain, state, {}).IsOk());
std::filesystem::remove(path, ignored);
}
} // namespace
TEST(EigenHdf5ResultsWriter, DrillingLifecycleFaultsPreserveFinalBytes) {
const auto path = std::filesystem::temp_directory_path() / "fesa-eigen-drilling-fault.h5";
auto domain = fesa::Domain::Create(Definition(path.parent_path() / "shell.inp"));
ASSERT_TRUE(domain.HasValue());
auto state = fesa::LinearFrequencyProcedureExecutor{}.Execute(domain.Value());
ASSERT_TRUE(state.HasValue());
for (const std::string fault : {"candidate-close", "reader-close", "markers"}) {
SCOPED_TRACE(fault);
std::ofstream(path, std::ios::binary | std::ios::trunc) << "prior";
EXPECT_FALSE(fesa::hdf5_internal::WriteEigenFile(path, domain.Value(), state.Value(), {},
fesa::test::Mitc4LifecycleFault(fault)).IsOk());
std::ifstream stream{path, std::ios::binary};
EXPECT_EQ((std::string{std::istreambuf_iterator<char>{stream}, {}}), "prior");
}
std::error_code ignored;
std::filesystem::remove(path, ignored);
}