feat(nonlinear-static-newton-raphson): step 9 - nonlinear-hdf5-schema-v1-writer

This commit is contained in:
KOKO\Mimi
2026-08-24 15:19:07 +09:00
parent 0978b019ce
commit dd3c6191c8
10 changed files with 2146 additions and 4 deletions
@@ -0,0 +1,591 @@
#include "fesa/io/hdf5/nonlinear_hdf5_results_writer.h"
#include <gtest/gtest.h>
#include <Windows.h>
#include <hdf5.h>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <memory>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include "fesa/analysis/analysis_model.h"
#include "fesa/analysis/nonlinear_analysis_state.h"
#include "fesa/fem/dof_manager.h"
#include "fesa/model/domain.h"
#include "fesa/results/nonlinear_results_writer.h"
#include "io/hdf5/hdf5_raii.h"
namespace {
using fesa::hdf5_internal::Hdf5Handle;
constexpr std::array<std::array<double, 3>, 3> kIdentityTriad{
std::array<double, 3>{1.0, 0.0, 0.0},
std::array<double, 3>{0.0, 1.0, 0.0},
std::array<double, 3>{0.0, 0.0, 1.0}};
class TempDirectory {
public:
explicit TempDirectory(const std::string& leaf)
: path_{std::filesystem::temp_directory_path() /
("fesa-nonlinear-hdf5-" + leaf)} {
std::error_code error;
std::filesystem::remove_all(path_, error);
if (!std::filesystem::create_directory(path_)) {
throw std::runtime_error{"Unable to create nonlinear HDF5 test directory."};
}
}
~TempDirectory() {
std::error_code error;
std::filesystem::remove_all(path_, error);
}
const std::filesystem::path& Path() const noexcept { return path_; }
private:
std::filesystem::path path_;
};
class WinHandle {
public:
explicit WinHandle(const HANDLE handle) : handle_{handle} {}
WinHandle(const WinHandle&) = delete;
WinHandle& operator=(const WinHandle&) = delete;
~WinHandle() {
if (handle_ != INVALID_HANDLE_VALUE) {
(void)CloseHandle(handle_);
}
}
HANDLE Get() const noexcept { return handle_; }
private:
HANDLE handle_{INVALID_HANDLE_VALUE};
};
struct Fixture {
std::unique_ptr<fesa::Domain> domain;
std::unique_ptr<fesa::DofManager> dofs;
fesa::NonlinearAnalysisState state;
};
struct AcceptedFrameRow {
std::uint64_t frame_index;
std::uint64_t increment_index;
double load_factor;
std::uint64_t iteration_count;
std::uint8_t is_factor_one;
};
struct DiagnosticRow {
char* severity;
char* code;
char* file;
std::uint64_t line;
char* keyword;
char* entity_identity;
char* message;
};
fesa::ModelDefinition MakeDefinition(const std::filesystem::path& source) {
fesa::ModelDefinition definition{};
definition.source_path = source;
definition.source_content_identity = "nonlinear-hdf5-fixture";
definition.nodes = {
{{"Shell-1", 10, "10"}, {0.0, 0.0, 0.0}, {source, 1U}},
{{"Shell-1", 20, "20"}, {1.0, 0.0, 0.0}, {source, 2U}},
{{"Shell-1", 30, "30"}, {1.0, 1.0, 0.0}, {source, 3U}},
{{"Shell-1", 40, "40"}, {0.0, 1.0, 0.0}, {source, 4U}}};
definition.materials = {{"Steel", 210.0e9, 0.3, {source, 5U}}};
definition.shell_sections = {{"Shell", 0.1, 0U, {source, 6U}}};
definition.shell_elements = {{{"Shell-1", 1, "1"},
fesa::ShellSourceElementType::kS4,
{0U, 1U, 2U, 3U},
0U,
0U,
{source, 7U}}};
for (std::size_t node = 0U; node < definition.nodes.size(); ++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.steps = {{"Step-1",
{},
{},
fesa::StaticProcedureKind::kNonlinearFullNewton,
true,
0.5,
1.0,
0.001,
1.0,
{source, 8U}}};
return definition;
}
fesa::NonlinearStateBaselineInput MakeBaseline(const fesa::Domain& domain) {
fesa::NonlinearStateBaselineInput baseline{};
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
baseline.node_baselines.push_back(
{static_cast<fesa::EntityIndex>(node), domain.Nodes()[node].source_id,
kIdentityTriad, {0.0, 0.0, 1.0}});
}
return baseline;
}
fesa::ShellResultRow MakeShellRow(const fesa::ShellMidsurfaceLocation location,
const std::array<double, 2> coordinates,
const double seed) {
return {0U,
location,
coordinates,
kIdentityTriad,
{seed, seed + 1.0, seed + 2.0, seed + 3.0, seed + 4.0,
seed + 5.0, seed + 6.0, seed + 7.0},
{seed + 10.0, seed + 11.0, seed + 12.0, seed + 13.0,
seed + 14.0, seed + 15.0, seed + 16.0, seed + 17.0},
{{{fesa::ShellSectionPosition::kBottom,
-1.0,
{seed + 20.0, seed + 21.0, seed + 22.0}},
{fesa::ShellSectionPosition::kMiddle,
0.0,
{seed + 23.0, seed + 24.0, seed + 25.0}},
{fesa::ShellSectionPosition::kTop,
1.0,
{seed + 26.0, seed + 27.0, seed + 28.0}}}}};
}
std::vector<fesa::ShellResultRow> MakeShellRows(const double seed) {
const double gauss = 1.0 / std::sqrt(3.0);
return {
MakeShellRow(fesa::ShellMidsurfaceLocation::kGp1, {-gauss, -gauss},
seed),
MakeShellRow(fesa::ShellMidsurfaceLocation::kGp2, {gauss, -gauss},
seed + 30.0),
MakeShellRow(fesa::ShellMidsurfaceLocation::kGp3, {gauss, gauss},
seed + 60.0),
MakeShellRow(fesa::ShellMidsurfaceLocation::kGp4, {-gauss, gauss},
seed + 90.0)};
}
fesa::AcceptedFrameState MakeFrame(const fesa::NonlinearTrialState& committed,
const std::uint32_t frame_index,
const std::uint32_t increment_index,
const double load_factor) {
fesa::AcceptedFrameState frame{
frame_index,
increment_index,
load_factor,
2U,
load_factor == 1.0,
committed.nodal_internal_order,
committed.nodal_source_ids,
committed.generalized_coordinates,
committed.displacement,
committed.rotation_vector,
committed.nodal_triad,
committed.nodal_director,
committed.internal_force,
committed.external_force,
committed.residual,
committed.reaction,
MakeShellRows(100.0 * load_factor),
20.0 * load_factor,
{load_factor, 2.0 * load_factor, 3.0 * load_factor,
4.0 * load_factor, 5.0 * load_factor, 6.0 * load_factor}};
for (std::size_t node = 0U; node < frame.nodal_internal_order.size(); ++node) {
const std::size_t offset = 6U * node;
frame.displacement[offset] = load_factor * static_cast<double>(node + 1U);
frame.displacement[offset + 1U] = -frame.displacement[offset];
frame.displacement[offset + 2U] = 0.5 * frame.displacement[offset];
frame.rotation_vector[offset + 5U] = 0.1 * load_factor;
for (std::size_t component = 0U; component < 6U; ++component) {
frame.generalized_coordinates[offset + component] =
component < 3U ? frame.displacement[offset + component]
: frame.rotation_vector[offset + component];
frame.internal_force[offset + component] =
10.0 * static_cast<double>(component + 1U);
frame.external_force[offset + component] = 0.0;
frame.residual[offset + component] = frame.internal_force[offset + component];
frame.reaction[offset + component] = frame.residual[offset + component];
}
}
return frame;
}
Fixture MakeFixture(const std::filesystem::path& source) {
auto domain_result = fesa::Domain::Create(MakeDefinition(source));
if (!domain_result.HasValue()) {
throw std::runtime_error{"Unable to construct nonlinear HDF5 Domain."};
}
auto domain = std::make_unique<fesa::Domain>(std::move(domain_result.Value()));
auto model_result = fesa::AnalysisModel::Create(*domain);
if (!model_result.HasValue()) {
throw std::runtime_error{"Unable to construct nonlinear HDF5 model."};
}
auto dofs_result = fesa::DofManager::Create(model_result.Value());
if (!dofs_result.HasValue()) {
throw std::runtime_error{"Unable to construct nonlinear HDF5 DOFs."};
}
auto dofs = std::make_unique<fesa::DofManager>(std::move(dofs_result.Value()));
auto state_result =
fesa::NonlinearAnalysisState::Create(*domain, *dofs, MakeBaseline(*domain));
if (!state_result.HasValue()) {
throw std::runtime_error{"Unable to construct nonlinear HDF5 state."};
}
fesa::NonlinearAnalysisState state = std::move(state_result.Value());
if (!state.CommitAcceptedFrame(
MakeFrame(state.CommittedState(), 0U, 1U, 0.5))
.IsOk() ||
!state.CommitAcceptedFrame(
MakeFrame(state.CommittedState(), 1U, 2U, 1.0))
.IsOk()) {
throw std::runtime_error{"Unable to commit nonlinear HDF5 test frames."};
}
return {std::move(domain), std::move(dofs), std::move(state)};
}
Hdf5Handle OpenFile(const std::filesystem::path& path) {
return {H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), H5Fclose};
}
Hdf5Handle OpenDataset(const hid_t file, const std::string& path) {
return {H5Dopen2(file, path.c_str(), H5P_DEFAULT), H5Dclose};
}
std::vector<hsize_t> DatasetDimensions(const hid_t file,
const std::string& path) {
const auto dataset = OpenDataset(file, path);
Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose};
const int rank = H5Sget_simple_extent_ndims(space.Get());
std::vector<hsize_t> dimensions(static_cast<std::size_t>(rank));
if (rank > 0) {
EXPECT_GE(H5Sget_simple_extent_dims(space.Get(), dimensions.data(), nullptr),
0);
}
return dimensions;
}
std::string ReadStringAttribute(const hid_t object, const char* name) {
Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose};
char* raw = nullptr;
EXPECT_GE(H5Aread(attribute.Get(), type.Get(), &raw), 0);
const std::string value = raw == nullptr ? std::string{} : std::string{raw};
if (raw != nullptr) {
EXPECT_GE(H5free_memory(raw), 0);
}
return value;
}
std::uint64_t ReadUint64Attribute(const hid_t object, const char* name) {
Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
std::uint64_t value = 0U;
EXPECT_GE(H5Aread(attribute.Get(), H5T_NATIVE_UINT64, &value), 0);
return value;
}
double ReadDoubleAttribute(const hid_t object, const char* name) {
Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
double value = 0.0;
EXPECT_GE(H5Aread(attribute.Get(), H5T_NATIVE_DOUBLE, &value), 0);
return value;
}
std::vector<double> ReadDoubleDataset(const hid_t file,
const std::string& path) {
const auto dimensions = DatasetDimensions(file, path);
std::size_t count = 1U;
for (const hsize_t dimension : dimensions) {
count *= static_cast<std::size_t>(dimension);
}
std::vector<double> values(count);
const auto dataset = OpenDataset(file, path);
if (!values.empty()) {
EXPECT_GE(H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
H5P_DEFAULT, values.data()),
0);
}
return values;
}
Hdf5Handle AcceptedFrameMemoryType() {
Hdf5Handle type{H5Tcreate(H5T_COMPOUND, sizeof(AcceptedFrameRow)), H5Tclose};
EXPECT_GE(H5Tinsert(type.Get(), "frame_index",
HOFFSET(AcceptedFrameRow, frame_index),
H5T_NATIVE_UINT64),
0);
EXPECT_GE(H5Tinsert(type.Get(), "increment_index",
HOFFSET(AcceptedFrameRow, increment_index),
H5T_NATIVE_UINT64),
0);
EXPECT_GE(H5Tinsert(type.Get(), "load_factor",
HOFFSET(AcceptedFrameRow, load_factor), H5T_NATIVE_DOUBLE),
0);
EXPECT_GE(H5Tinsert(type.Get(), "iteration_count",
HOFFSET(AcceptedFrameRow, iteration_count),
H5T_NATIVE_UINT64),
0);
EXPECT_GE(H5Tinsert(type.Get(), "is_factor_one",
HOFFSET(AcceptedFrameRow, is_factor_one),
H5T_NATIVE_UINT8),
0);
return type;
}
std::vector<AcceptedFrameRow> ReadAcceptedFrames(const hid_t file) {
const auto dataset =
OpenDataset(file, "/steps/Step-1/accepted_frames");
const auto dimensions = DatasetDimensions(file, "/steps/Step-1/accepted_frames");
std::vector<AcceptedFrameRow> rows(static_cast<std::size_t>(dimensions[0U]));
auto type = AcceptedFrameMemoryType();
EXPECT_GE(H5Dread(dataset.Get(), type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT,
rows.data()),
0);
return rows;
}
Hdf5Handle DiagnosticMemoryType() {
Hdf5Handle string_type{H5Tcopy(H5T_C_S1), H5Tclose};
EXPECT_GE(H5Tset_size(string_type.Get(), H5T_VARIABLE), 0);
EXPECT_GE(H5Tset_cset(string_type.Get(), H5T_CSET_UTF8), 0);
Hdf5Handle type{H5Tcreate(H5T_COMPOUND, sizeof(DiagnosticRow)), H5Tclose};
EXPECT_GE(H5Tinsert(type.Get(), "severity", HOFFSET(DiagnosticRow, severity),
string_type.Get()),
0);
EXPECT_GE(H5Tinsert(type.Get(), "code", HOFFSET(DiagnosticRow, code),
string_type.Get()),
0);
EXPECT_GE(H5Tinsert(type.Get(), "file", HOFFSET(DiagnosticRow, file),
string_type.Get()),
0);
EXPECT_GE(H5Tinsert(type.Get(), "line", HOFFSET(DiagnosticRow, line),
H5T_NATIVE_UINT64),
0);
EXPECT_GE(H5Tinsert(type.Get(), "keyword", HOFFSET(DiagnosticRow, keyword),
string_type.Get()),
0);
EXPECT_GE(H5Tinsert(type.Get(), "entity_identity",
HOFFSET(DiagnosticRow, entity_identity), string_type.Get()),
0);
EXPECT_GE(H5Tinsert(type.Get(), "message", HOFFSET(DiagnosticRow, message),
string_type.Get()),
0);
return type;
}
std::vector<DiagnosticRow> ReadDiagnostics(const hid_t file) {
const auto dataset = OpenDataset(file, "/diagnostics");
const auto dimensions = DatasetDimensions(file, "/diagnostics");
std::vector<DiagnosticRow> rows(static_cast<std::size_t>(dimensions[0U]));
auto type = DiagnosticMemoryType();
EXPECT_GE(H5Dread(dataset.Get(), type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT,
rows.data()),
0);
return rows;
}
void ReclaimDiagnostics(const std::vector<DiagnosticRow>& rows) {
for (const auto& row : rows) {
for (char* value : {row.severity, row.code, row.file, row.keyword,
row.entity_identity, row.message}) {
if (value != nullptr) {
EXPECT_GE(H5free_memory(value), 0);
}
}
}
}
std::vector<char> ReadBytes(const std::filesystem::path& path) {
std::ifstream input{path, std::ios::binary};
return {std::istreambuf_iterator<char>{input},
std::istreambuf_iterator<char>{}};
}
void WriteBytes(const std::filesystem::path& path,
const std::vector<char>& bytes) {
std::ofstream output{path, std::ios::binary | std::ios::trunc};
output.write(bytes.data(), static_cast<std::streamsize>(bytes.size()));
if (!output) {
throw std::runtime_error{"Unable to write nonlinear HDF5 test bytes."};
}
}
} // namespace
// NSNR-HDF5-001 through NSNR-HDF5-006, NSNR-HDF5-008 through NSNR-HDF5-010.
TEST(NonlinearHdf5ResultsWriter,
WritesSchemaV1ModelAndCommittedAcceptedFramesOnly) {
TempDirectory directory{"schema"};
auto fixture = MakeFixture(directory.Path() / "model.inp");
const auto output = directory.Path() / "model_results.h5";
fesa::NonlinearHdf5ResultsWriter writer;
const auto status = writer.Write(output, *fixture.domain, fixture.state, {});
ASSERT_TRUE(status.IsOk()) << status.Diagnostics().at(0U).message;
const auto file = OpenFile(output);
ASSERT_GE(file.Get(), 0);
Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
ASSERT_GE(metadata.Get(), 0);
EXPECT_EQ(ReadUint64Attribute(metadata.Get(), "schema_version"), 1U);
EXPECT_EQ(ReadStringAttribute(metadata.Get(), "feature_id"),
"nonlinear-static-newton-raphson");
EXPECT_EQ(ReadStringAttribute(metadata.Get(), "analysis_procedure"),
"nonlinear-static-full-newton");
EXPECT_EQ(ReadStringAttribute(metadata.Get(), "internal_formulation"),
"FESA-MITC4-total-lagrangian");
EXPECT_EQ(ReadStringAttribute(metadata.Get(), "integration_rule"),
"2x2x2-gauss; mitc4-edge-midpoint-shear");
EXPECT_EQ(ReadUint64Attribute(metadata.Get(), "factor_zero_is_frame"), 0U);
EXPECT_DOUBLE_EQ(ReadDoubleAttribute(metadata.Get(), "factor_zero_load_factor"),
0.0);
for (const char* path :
{"/model/nodes", "/model/elements", "/model/shell/materials",
"/model/shell/sections", "/model/shell/nodal_director",
"/model/shell/nodal_frame", "/model/nodal_constraint_mask",
"/model/prescribed_displacement_start",
"/model/prescribed_displacement_target",
"/model/shell/midsurface_locations", "/model/shell/section_positions"}) {
EXPECT_GT(H5Lexists(file.Get(), path, H5P_DEFAULT), 0) << path;
}
EXPECT_EQ(DatasetDimensions(file.Get(), "/steps/Step-1/accepted_frames"),
std::vector<hsize_t>({2U}));
const auto frames = ReadAcceptedFrames(file.Get());
ASSERT_EQ(frames.size(), 2U);
EXPECT_EQ(frames[0U].frame_index, 0U);
EXPECT_EQ(frames[0U].increment_index, 1U);
EXPECT_DOUBLE_EQ(frames[0U].load_factor, 0.5);
EXPECT_EQ(frames[0U].is_factor_one, 0U);
EXPECT_EQ(frames[1U].frame_index, 1U);
EXPECT_EQ(frames[1U].increment_index, 2U);
EXPECT_DOUBLE_EQ(frames[1U].load_factor, 1.0);
EXPECT_EQ(frames[1U].is_factor_one, 1U);
EXPECT_EQ(H5Lexists(file.Get(), "/steps/Step-1/frames/2", H5P_DEFAULT), 0);
EXPECT_EQ(H5Lexists(file.Get(), "/steps/Step-1/trial_history", H5P_DEFAULT),
0);
const std::string root = "/steps/Step-1/frames/1";
for (const std::string& suffix :
{"/nodal/displacement", "/nodal/triad", "/nodal/director",
"/nodal/generalized_internal_force", "/nodal/generalized_external_force",
"/nodal/generalized_residual", "/nodal/reaction",
"/element/shell/local_frame", "/element/shell/generalized_strain",
"/element/shell/section_resultant", "/element/shell/stress",
"/global/energy", "/global/equilibrium"}) {
EXPECT_GT(H5Lexists(file.Get(), (root + suffix).c_str(), H5P_DEFAULT), 0)
<< suffix;
}
EXPECT_EQ(DatasetDimensions(file.Get(), root + "/nodal/displacement"),
std::vector<hsize_t>({4U, 6U}));
EXPECT_EQ(DatasetDimensions(file.Get(), root + "/nodal/triad"),
std::vector<hsize_t>({4U, 3U, 3U}));
EXPECT_EQ(DatasetDimensions(file.Get(), root + "/element/shell/local_frame"),
std::vector<hsize_t>({1U, 4U, 3U, 3U}));
EXPECT_EQ(DatasetDimensions(file.Get(), root + "/element/shell/stress"),
std::vector<hsize_t>({1U, 4U, 3U, 3U}));
const auto displacement = ReadDoubleDataset(file.Get(), root + "/nodal/displacement");
ASSERT_EQ(displacement.size(), 24U);
EXPECT_DOUBLE_EQ(displacement[0U], 1.0);
EXPECT_DOUBLE_EQ(displacement[1U], -1.0);
EXPECT_DOUBLE_EQ(displacement[2U], 0.5);
EXPECT_DOUBLE_EQ(displacement[5U], 0.1);
}
// NSNR-HDF5-007.
TEST(NonlinearHdf5ResultsWriter, ProjectsRotationalReactionToSpatialMoment) {
TempDirectory directory{"reaction"};
auto fixture = MakeFixture(directory.Path() / "reaction.inp");
const auto output = directory.Path() / "reaction_results.h5";
fesa::NonlinearHdf5ResultsWriter writer;
ASSERT_TRUE(writer.Write(output, *fixture.domain, fixture.state, {}).IsOk());
const auto file = OpenFile(output);
const auto reaction = ReadDoubleDataset(
file.Get(), "/steps/Step-1/frames/1/nodal/reaction");
ASSERT_EQ(reaction.size(), 24U);
const double theta = 0.1;
const double quadratic =
(1.0 - 0.5 * theta / std::tan(0.5 * theta)) / (theta * theta);
EXPECT_NEAR(reaction[3U],
(1.0 - quadratic * theta * theta) * 40.0 -
0.5 * theta * 50.0,
1.0e-12);
EXPECT_NEAR(reaction[4U],
0.5 * theta * 40.0 +
(1.0 - quadratic * theta * theta) * 50.0,
1.0e-12);
EXPECT_NEAR(reaction[5U], 60.0, 1.0e-12);
EXPECT_GT(quadratic, 0.0);
}
// NSNR-HDF5-012 and NSNR-HDF5-013.
TEST(NonlinearHdf5ResultsWriter, StoresSortedDiagnosticsThroughBackendNeutralBoundary) {
static_assert(std::is_base_of_v<fesa::NonlinearResultsWriter,
fesa::NonlinearHdf5ResultsWriter>);
TempDirectory directory{"diagnostics"};
auto fixture = MakeFixture(directory.Path() / "diagnostics.inp");
const std::vector<fesa::Diagnostic> diagnostics{
{fesa::Severity::kWarning,
"later",
{fixture.domain->SourcePath(), 20U},
"*OUTPUT",
"",
"Later warning."},
{fesa::Severity::kWarning,
"earlier",
{fixture.domain->SourcePath(), 10U},
"*PREPRINT",
"",
"Earlier warning."}};
const auto output = directory.Path() / "diagnostics_results.h5";
fesa::NonlinearHdf5ResultsWriter writer;
ASSERT_TRUE(writer.Write(output, *fixture.domain, fixture.state, diagnostics)
.IsOk());
const auto file = OpenFile(output);
const auto rows = ReadDiagnostics(file.Get());
ASSERT_EQ(rows.size(), 2U);
EXPECT_STREQ(rows[0U].code, "earlier");
EXPECT_EQ(rows[0U].line, 10U);
EXPECT_STREQ(rows[1U].code, "later");
EXPECT_EQ(rows[1U].line, 20U);
ReclaimDiagnostics(rows);
}
// NSNR-HDF5-011.
TEST(NonlinearHdf5ResultsWriter, CandidateFinalizationFailurePreservesPriorFinal) {
TempDirectory directory{"atomic"};
auto fixture = MakeFixture(directory.Path() / "atomic.inp");
const auto output = directory.Path() / "atomic_results.h5";
const std::vector<char> sentinel{'p', 'r', 'i', 'o', 'r'};
WriteBytes(output, sentinel);
WinHandle lock{CreateFileW(output.c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
ASSERT_NE(lock.Get(), INVALID_HANDLE_VALUE);
fesa::NonlinearHdf5ResultsWriter writer;
const auto status = writer.Write(output, *fixture.domain, fixture.state, {});
ASSERT_FALSE(status.IsOk());
EXPECT_EQ(status.Category(), fesa::FailureCategory::kOutput);
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics()[0U].code, "hdf5-finalization-failure");
EXPECT_EQ(ReadBytes(output), sentinel);
}