feat(nonlinear-static-newton-raphson): step 0 - cli-output-lease-and-derived-names

This commit is contained in:
KOKO\Mimi
2026-08-24 10:42:14 +09:00
parent 41816c8aef
commit fc16ef6221
61 changed files with 10890 additions and 115 deletions
+89 -33
View File
@@ -1,5 +1,7 @@
#include "fesa/app/fesa_application.h"
#include "fesa/app/output_paths.h"
#include <algorithm>
#include <gtest/gtest.h>
#include <hdf5.h>
@@ -9,6 +11,7 @@
#include <cstddef>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <utility>
@@ -332,32 +335,37 @@ void ExpectDiagnosticFieldOrder(const std::string& text) {
}
}
std::vector<std::string> ExplicitOutputArguments(
const std::filesystem::path& input, const std::filesystem::path& output) {
// FesaApplication receives argv[0]-excluded operands and options.
return {input.string(), "--output", output.string()};
struct FileSnapshot {
std::string contents;
std::filesystem::file_time_type write_time;
};
FileSnapshot SnapshotFile(const std::filesystem::path& path) {
std::ifstream stream{path, std::ios::binary};
if (!stream) {
throw std::runtime_error{"Unable to open an app output fixture."};
}
return {{std::istreambuf_iterator<char>{stream},
std::istreambuf_iterator<char>{}},
std::filesystem::last_write_time(path)};
}
} // namespace
TEST(LinearStaticCli, DefaultAndExplicitOutputProduceFesaHdf5) {
// NSNR-CLI-008: the application no longer uses a CWD-global results.h5.
TEST(LinearStaticCli, DefaultOutputUsesInputSiblingDerivedName) {
TempDirectory directory{"paths"};
const auto input = directory.Path() / "model.inp";
const auto input = directory.Path() / "Beam.Model.INP";
WriteText(input, AxialDeck(true, false, false));
const auto default_output = directory.Path() / "results.h5";
const auto default_output = directory.Path() / "Beam.Model_results.h5";
{
CurrentDirectoryGuard current_directory{directory.Path()};
const auto result = RunApplication({input.string()});
ASSERT_EQ(result.exit_code, 0) << result.standard_error;
}
ExpectFesaHdf5Identity(default_output, input);
const auto explicit_output = directory.Path() / "named-output.h5";
const auto result =
RunApplication(ExplicitOutputArguments(input, explicit_output));
ASSERT_EQ(result.exit_code, 0) << result.standard_error;
ExpectFesaHdf5Identity(explicit_output, input);
EXPECT_FALSE(std::filesystem::exists(directory.Path() / "results.h5"));
}
TEST(LinearStaticCli, ReturnsEveryExactExitCodeAndOrderedDiagnostic) {
@@ -369,8 +377,7 @@ TEST(LinearStaticCli, ReturnsEveryExactExitCodeAndOrderedDiagnostic) {
WriteText(model_input, AxialDeck(true, true, false));
WriteText(solver_input, AxialDeck(false, false, false));
const auto success = RunApplication(
ExplicitOutputArguments(valid_input, directory.Path() / "success.h5"));
const auto success = RunApplication({valid_input.string()});
const auto usage = RunApplication({});
const auto missing_input = directory.Path() / "missing.inp";
const auto input = RunApplication({missing_input.string()});
@@ -378,25 +385,19 @@ TEST(LinearStaticCli, ReturnsEveryExactExitCodeAndOrderedDiagnostic) {
RunApplication({missing_input.string(), "--output", "--output"});
const auto unknown_output_option =
RunApplication({missing_input.string(), "--output", "--bogus"});
const auto model = RunApplication(ExplicitOutputArguments(
model_input, directory.Path() / "model-failure.h5"));
const auto solver = RunApplication(ExplicitOutputArguments(
solver_input, directory.Path() / "solver-failure.h5"));
const auto output = RunApplication(ExplicitOutputArguments(
valid_input, directory.Path() / "nonexistent-parent" / "results.h5"));
const auto model = RunApplication({model_input.string()});
const auto solver = RunApplication({solver_input.string()});
EXPECT_EQ(success.exit_code, 0) << success.standard_error;
EXPECT_EQ(usage.exit_code, 2);
EXPECT_EQ(input.exit_code, 3);
EXPECT_EQ(model.exit_code, 4);
EXPECT_EQ(solver.exit_code, 5);
EXPECT_EQ(output.exit_code, 6);
ExpectDiagnosticFieldOrder(usage.standard_error);
ExpectDiagnosticFieldOrder(input.standard_error);
ExpectDiagnosticFieldOrder(model.standard_error);
ExpectDiagnosticFieldOrder(solver.standard_error);
ExpectDiagnosticFieldOrder(output.standard_error);
EXPECT_EQ(repeated_output.exit_code, 2);
ExpectDiagnosticFieldOrder(repeated_output.standard_error);
EXPECT_NE(repeated_output.standard_error.find("code=cli-usage"),
@@ -407,24 +408,79 @@ TEST(LinearStaticCli, ReturnsEveryExactExitCodeAndOrderedDiagnostic) {
std::string::npos);
}
// NSNR-CLI-003 and NSNR-CLI-007: usage failure precedes every output side
// effect, including replacement of existing derived files.
TEST(LinearStaticCli, OutputSubstringRejectionLeavesDerivedFilesUnchanged) {
TempDirectory directory{"usage-no-output"};
const auto input = directory.Path() / "model.inp";
const auto results = directory.Path() / "model_results.h5";
const auto log = directory.Path() / "model_log.txt";
WriteText(input, AxialDeck(true, false, false));
WriteText(results, "previous-results");
WriteText(log, "previous-log");
const FileSnapshot previous_results = SnapshotFile(results);
const FileSnapshot previous_log = SnapshotFile(log);
for (const char* argument :
{"abc--outputdef", "--output-path", "--output=foo", "--output"}) {
const auto result = RunApplication({input.string(), argument});
EXPECT_EQ(result.exit_code, 2) << argument;
EXPECT_EQ(
std::count(result.standard_error.begin(), result.standard_error.end(), '\n'),
1U)
<< result.standard_error;
EXPECT_NE(result.standard_error.find("code=cli-usage"),
std::string::npos);
EXPECT_EQ(SnapshotFile(results).contents, previous_results.contents);
EXPECT_EQ(SnapshotFile(results).write_time, previous_results.write_time);
EXPECT_EQ(SnapshotFile(log).contents, previous_log.contents);
EXPECT_EQ(SnapshotFile(log).write_time, previous_log.write_time);
}
}
// NSNR-CLI-006: a held lease blocks the application before any public output
// is opened, truncated, or replaced.
TEST(LinearStaticCli, BusyOutputLeaseLeavesDerivedFilesUnchanged) {
TempDirectory directory{"busy-output"};
const auto input = directory.Path() / "model.inp";
const auto results = directory.Path() / "model_results.h5";
const auto log = directory.Path() / "model_log.txt";
WriteText(input, AxialDeck(true, false, false));
WriteText(results, "previous-results");
WriteText(log, "previous-log");
const FileSnapshot previous_results = SnapshotFile(results);
const FileSnapshot previous_log = SnapshotFile(log);
const fesa::DerivedOutputPaths paths{input, results, log};
auto lease = fesa::AcquireOutputWriteLease(paths);
ASSERT_TRUE(lease.HasValue()) << lease.GetStatus().Diagnostics().size();
const auto result = RunApplication({input.string()});
EXPECT_EQ(result.exit_code, 6) << result.standard_error;
EXPECT_NE(result.standard_error.find("code=output-busy"),
std::string::npos);
EXPECT_EQ(SnapshotFile(results).contents, previous_results.contents);
EXPECT_EQ(SnapshotFile(results).write_time, previous_results.write_time);
EXPECT_EQ(SnapshotFile(log).contents, previous_log.contents);
EXPECT_EQ(SnapshotFile(log).write_time, previous_log.write_time);
}
TEST(LinearStaticCli, OutputRequestsDoNotFilterMandatoryResults) {
TempDirectory directory{"output-requests"};
const auto plain_input = directory.Path() / "plain.inp";
const auto requested_input = directory.Path() / "requested.inp";
const auto plain_output = directory.Path() / "plain.h5";
const auto requested_output = directory.Path() / "requested.h5";
WriteText(plain_input, AxialDeck(true, false, false));
WriteText(requested_input, AxialDeck(true, false, true));
const auto plain =
RunApplication(ExplicitOutputArguments(plain_input, plain_output));
const auto requested = RunApplication(
ExplicitOutputArguments(requested_input, requested_output));
const auto plain = RunApplication({plain_input.string()});
const auto requested = RunApplication({requested_input.string()});
ASSERT_EQ(plain.exit_code, 0) << plain.standard_error;
ASSERT_EQ(requested.exit_code, 0) << requested.standard_error;
const auto plain_file = OpenFile(plain_output);
const auto requested_file = OpenFile(requested_output);
const auto plain_file =
OpenFile(plain_input.parent_path() / "plain_results.h5");
const auto requested_file =
OpenFile(requested_input.parent_path() / "requested_results.h5");
ExpectMandatoryInventory(plain_file.Get());
ExpectMandatoryInventory(requested_file.Get());
@@ -449,10 +505,10 @@ TEST(LinearStaticCli, OutputRequestsDoNotFilterMandatoryResults) {
TEST(Mitc4ShellCli, WritesShellHdf5ThroughTheExistingApplicationRoute) {
TempDirectory directory{"shell-route"};
const auto input = directory.Path() / "shell.inp";
const auto output = directory.Path() / "shell-results.h5";
const auto output = directory.Path() / "shell_results.h5";
WriteText(input, AllConstrainedShellDeck());
const auto result = RunApplication(ExplicitOutputArguments(input, output));
const auto result = RunApplication({input.string()});
ASSERT_EQ(result.exit_code, 0) << result.standard_error;
ExpectShellHdf5Identity(output, input);
}