fix(result-contract-completion): enforce complete result provenance
This commit is contained in:
@@ -98,6 +98,63 @@ void replace_step_time_with_vector(const std::filesystem::path& path) {
|
||||
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,
|
||||
@@ -146,6 +203,30 @@ void write_double_dataset(
|
||||
values.data()));
|
||||
}
|
||||
|
||||
void write_uint8_dataset(
|
||||
const std::filesystem::path& path,
|
||||
const std::string_view dataset_path,
|
||||
const std::span<const std::uint8_t> 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,
|
||||
@@ -176,7 +257,15 @@ std::filesystem::path self_contained_path() {
|
||||
"Temporary" / "fesa-self-contained.h5";
|
||||
}
|
||||
|
||||
fesa::Domain make_domain() {
|
||||
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},
|
||||
@@ -186,8 +275,15 @@ fesa::Domain make_domain() {
|
||||
builder.add_node({
|
||||
fesa::NodeId{7},
|
||||
fesa::EntityOrigin{"BeamPart", "Beam-1", 1002},
|
||||
fesa::Vec3{4.0, 5.5, -6.25},
|
||||
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",
|
||||
@@ -257,8 +353,13 @@ fesa::Domain make_domain() {
|
||||
return std::move(*built.domain);
|
||||
}
|
||||
|
||||
fesa::BeamSectionResult make_end_result(
|
||||
double xi,
|
||||
fesa::NodeId node,
|
||||
double offset);
|
||||
|
||||
fesa::ResultDatabase make_database() {
|
||||
return {
|
||||
fesa::ResultDatabase database{
|
||||
"2.0.0",
|
||||
{{
|
||||
"Load/Case",
|
||||
@@ -286,6 +387,35 @@ fesa::ResultDatabase make_database() {
|
||||
}},
|
||||
}},
|
||||
};
|
||||
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(
|
||||
@@ -319,34 +449,6 @@ fesa::BeamSectionResult make_end_result(
|
||||
fesa::ResultDatabase make_complete_database() {
|
||||
fesa::ResultDatabase database = make_database();
|
||||
auto& frame = database.steps[0].frames[0];
|
||||
frame.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),
|
||||
},
|
||||
},
|
||||
};
|
||||
frame.diagnostics = {
|
||||
{
|
||||
fesa::DiagnosticStage::solver,
|
||||
@@ -385,8 +487,8 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
||||
const auto domain = make_domain();
|
||||
const auto database = make_database();
|
||||
|
||||
const auto write_diagnostics =
|
||||
fesa::write_hdf5(path, domain, 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);
|
||||
@@ -439,9 +541,9 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
||||
EXPECT_EQ(
|
||||
read.model->nodes[1].origin,
|
||||
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1002}));
|
||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.x, 4.0);
|
||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.y, 5.5);
|
||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.z, -6.25);
|
||||
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);
|
||||
@@ -477,9 +579,12 @@ TEST(SelfContainedHdf5, PublicReaderReconstructsCompletePhase1Contract) {
|
||||
std::filesystem::create_directories(path.parent_path());
|
||||
std::filesystem::remove(path);
|
||||
|
||||
ASSERT_TRUE(
|
||||
fesa::write_hdf5(path, make_domain(), make_complete_database())
|
||||
.empty());
|
||||
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());
|
||||
@@ -493,6 +598,10 @@ TEST(SelfContainedHdf5, PublicReaderReconstructsCompletePhase1Contract) {
|
||||
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);
|
||||
@@ -547,7 +656,7 @@ TEST(SelfContainedHdf5, PublicReaderReconstructsCompletePhase1Contract) {
|
||||
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_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});
|
||||
@@ -568,6 +677,73 @@ TEST(SelfContainedHdf5, PublicReaderReconstructsCompletePhase1Contract) {
|
||||
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";
|
||||
@@ -575,19 +751,34 @@ TEST(Hdf5, RejectsVersion1AfterMajorSchemaChange) {
|
||||
auto database = make_database();
|
||||
database.schema_version = "1.0.0";
|
||||
|
||||
const auto diagnostics =
|
||||
fesa::write_hdf5(path, make_domain(), database);
|
||||
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()).empty());
|
||||
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);
|
||||
@@ -608,7 +799,8 @@ TEST(Hdf5, RejectsInvalidResultDatabaseBeforeWriting) {
|
||||
auto database = make_database();
|
||||
database.steps[0].frames[0].nodal.reaction.pop_back();
|
||||
|
||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
||||
const auto diagnostics =
|
||||
fesa::write_hdf5(path, domain, database, test_input_identity());
|
||||
|
||||
EXPECT_TRUE(
|
||||
has_results_error(diagnostics, "results.nodal_size_mismatch"));
|
||||
@@ -630,7 +822,8 @@ TEST(Hdf5, PreservesFrameDiagnosticsRepresentedBySchema) {
|
||||
std::nullopt,
|
||||
});
|
||||
|
||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
||||
const auto diagnostics =
|
||||
fesa::write_hdf5(path, domain, database, test_input_identity());
|
||||
|
||||
ASSERT_TRUE(diagnostics.empty());
|
||||
|
||||
@@ -653,9 +846,14 @@ TEST(Hdf5, RejectsResultNodeMissingFromDomainBeforeWriting) {
|
||||
std::filesystem::remove(path);
|
||||
const auto domain = make_domain();
|
||||
auto database = make_database();
|
||||
database.steps[0].frames[0].nodal.node_ids[0] = fesa::NodeId{999};
|
||||
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);
|
||||
const auto diagnostics =
|
||||
fesa::write_hdf5(path, domain, database, test_input_identity());
|
||||
|
||||
EXPECT_TRUE(has_results_error(
|
||||
diagnostics, "hdf5.result_node_not_in_model"));
|
||||
@@ -679,8 +877,8 @@ TEST(Hdf5, RejectsNonScalarStepTimeAttribute) {
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-nonscalar-step-time.h5";
|
||||
std::filesystem::remove(path);
|
||||
const auto write_diagnostics =
|
||||
fesa::write_hdf5(path, make_domain(), make_database());
|
||||
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);
|
||||
|
||||
@@ -691,19 +889,88 @@ TEST(Hdf5, RejectsNonScalarStepTimeAttribute) {
|
||||
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);
|
||||
const auto write_diagnostics =
|
||||
fesa::write_hdf5(path, make_domain(), make_database());
|
||||
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<std::int64_t, 2> node_ids{999, 42};
|
||||
write_int64_dataset(
|
||||
path,
|
||||
"/results/steps/0/frames/0/nodal/node_ids",
|
||||
node_ids);
|
||||
const std::array<std::int64_t, 3> node_ids{42, 7, 100};
|
||||
write_int64_dataset(path, "/model/nodes/internal_id", node_ids);
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
@@ -718,8 +985,9 @@ TEST(Hdf5, RejectsDuplicateSerializedNodeIds) {
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-duplicate-node-ids.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(
|
||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
const std::array<std::int64_t, 2> ids{42, 42};
|
||||
write_int64_dataset(path, "/model/nodes/internal_id", ids);
|
||||
|
||||
@@ -736,8 +1004,9 @@ TEST(Hdf5, RejectsDuplicateSerializedElementIds) {
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-duplicate-element-ids.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(
|
||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
const std::array<std::int64_t, 2> ids{9, 9};
|
||||
write_int64_dataset(path, "/model/elements/internal_id", ids);
|
||||
|
||||
@@ -754,8 +1023,9 @@ TEST(Hdf5, RejectsDuplicateSerializedSectionIds) {
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-duplicate-section-ids.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(
|
||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
const std::array<std::int64_t, 2> ids{4, 4};
|
||||
write_int64_dataset(path, "/model/sections/internal_id", ids);
|
||||
|
||||
@@ -772,15 +1042,16 @@ TEST(Hdf5, RejectsNonfiniteSerializedCoordinates) {
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-nonfinite-coordinates.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(
|
||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
const std::array<double, 6> coordinates{
|
||||
std::numeric_limits<double>::quiet_NaN(),
|
||||
-2.5,
|
||||
3.75,
|
||||
4.0,
|
||||
5.5,
|
||||
-6.25,
|
||||
2.25,
|
||||
-2.5,
|
||||
3.75,
|
||||
};
|
||||
write_double_dataset(path, "/model/nodes/coordinates", coordinates);
|
||||
|
||||
@@ -792,13 +1063,128 @@ TEST(Hdf5, RejectsNonfiniteSerializedCoordinates) {
|
||||
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<double, 6> 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<double, 6> 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<std::int64_t, 2> 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<std::int64_t, 2> node_ids{42, 42};
|
||||
const std::array<std::uint8_t, 2> 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<double, 18> 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()).empty());
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
const std::array<double, 2> shear_areas{
|
||||
std::numeric_limits<double>::infinity(),
|
||||
0.05,
|
||||
@@ -818,8 +1204,9 @@ TEST(Hdf5, RejectsNonpositiveSerializedShearArea) {
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-nonpositive-shear-area.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(
|
||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
const std::array<double, 2> shear_areas{-0.031, 0.05};
|
||||
write_double_dataset(path, "/model/sections/shear_area_y", shear_areas);
|
||||
|
||||
|
||||
@@ -69,6 +69,11 @@ std::string quote(const std::filesystem::path& path) {
|
||||
return '"' + path.string() + '"';
|
||||
}
|
||||
|
||||
std::string path_utf8(const std::filesystem::path& path) {
|
||||
const std::u8string value = path.u8string();
|
||||
return {reinterpret_cast<const char*>(value.data()), value.size()};
|
||||
}
|
||||
|
||||
std::string read_text(const std::filesystem::path& path) {
|
||||
std::ifstream input{path, std::ios::binary};
|
||||
return {
|
||||
@@ -93,8 +98,15 @@ TEST(MinimalCantileverPipeline, WritesReadableFiniteEquilibratedResults) {
|
||||
fesa::read_hdf5_results(output.path());
|
||||
ASSERT_TRUE(read.database.has_value());
|
||||
ASSERT_TRUE(read.model.has_value());
|
||||
ASSERT_TRUE(read.metadata.has_value());
|
||||
EXPECT_TRUE(read.diagnostics.empty());
|
||||
EXPECT_EQ(read.database->schema_version, "2.0.0");
|
||||
EXPECT_EQ(
|
||||
read.metadata->input_source,
|
||||
path_utf8(fixture_path("minimal_cantilever.inp")));
|
||||
EXPECT_EQ(
|
||||
read.metadata->input_fingerprint,
|
||||
"fnv1a64:73f31da4615f09b3");
|
||||
|
||||
ASSERT_EQ(read.model->nodes.size(), 2U);
|
||||
EXPECT_EQ(read.model->nodes[0].id, fesa::NodeId{0});
|
||||
|
||||
Reference in New Issue
Block a user