#include "fesa/app/output_paths.h" #include #include #include #include #include #include #include "fesa/analysis/analysis.h" namespace { class TempDirectory { public: explicit TempDirectory(const std::string& label) { const auto tick = std::chrono::steady_clock::now().time_since_epoch().count(); path_ = std::filesystem::temp_directory_path() / ("fesa-output-paths-" + label + "-" + std::to_string(tick)); std::error_code error; if (!std::filesystem::create_directory(path_, error) || error) { throw std::runtime_error{"Unable to create an output-path test fixture."}; } } TempDirectory(const TempDirectory&) = delete; TempDirectory& operator=(const TempDirectory&) = delete; ~TempDirectory() { std::error_code ignored; std::filesystem::remove_all(path_, ignored); } const std::filesystem::path& Path() const noexcept { return path_; } private: std::filesystem::path path_; }; void ExpectFailureCode(const fesa::Status& status, const char* code) { ASSERT_FALSE(status.IsOk()); ASSERT_EQ(status.Diagnostics().size(), 1U); EXPECT_EQ(status.Diagnostics().front().code, code); } // NSNR-CLI-001 and NSNR-CLI-002: a token containing the literal substring // must fail before input path validation or output derivation. TEST(OutputPaths, RejectsFirstTokenContainingOutputSubstring) { const auto paths = fesa::DeriveSingleInputOutputPaths( {"abc--outputdef", "model.inp", "--output=ignored"}, std::filesystem::path{"C:/invocation"}); ASSERT_FALSE(paths.HasValue()); ExpectFailureCode(paths.GetStatus(), "cli-usage"); EXPECT_EQ(paths.GetStatus().Diagnostics().front().entity_identity, "argument_index=0;argument=abc--outputdef"); } TEST(OutputPaths, RejectsEveryOutputSubstringVariant) { for (const char* argument : {"abc--outputdef", "--output-path", "--output=foo", "--output"}) { const auto paths = fesa::DeriveSingleInputOutputPaths( {"model.inp", argument}, std::filesystem::path{"C:/invocation"}); ASSERT_FALSE(paths.HasValue()) << argument; ExpectFailureCode(paths.GetStatus(), "cli-usage"); } } // NSNR-CLI-004: derivation is lexical and does not inspect the filesystem. TEST(OutputPaths, DerivesNormalizedSiblingNamesFromRelativeInput) { const auto paths = fesa::DeriveSingleInputOutputPaths( {"inputs/../models/Beam.Model.INP"}, std::filesystem::path{"C:/invocation/current"}); ASSERT_TRUE(paths.HasValue()) << paths.GetStatus().Diagnostics().size(); EXPECT_EQ( paths.Value().input_path, std::filesystem::path{"C:/invocation/current/models/Beam.Model.INP"}); EXPECT_EQ(paths.Value().results_path, std::filesystem::path{ "C:/invocation/current/models/Beam.Model_results.h5"}); EXPECT_EQ(paths.Value().log_path, std::filesystem::path{ "C:/invocation/current/models/Beam.Model_log.txt"}); } // NSNR-CLI-005: only the final, case-insensitive input extension is removed. TEST(OutputPaths, PreservesBasenameCaseAndPrecedingDots) { const auto paths = fesa::DeriveSingleInputOutputPaths( {"C:/models/.Beam.v2.iNp"}, std::filesystem::path{"C:/unused"}); ASSERT_TRUE(paths.HasValue()); EXPECT_EQ(paths.Value().results_path, std::filesystem::path{"C:/models/.Beam.v2_results.h5"}); EXPECT_EQ(paths.Value().log_path, std::filesystem::path{"C:/models/.Beam.v2_log.txt"}); } TEST(OutputPaths, RejectsInputWithoutInpFinalExtension) { const auto paths = fesa::DeriveSingleInputOutputPaths( {"model.inp.bak"}, std::filesystem::path{"C:/invocation"}); ASSERT_FALSE(paths.HasValue()); ExpectFailureCode(paths.GetStatus(), "cli-usage"); } // NSNR-CLI-006: the lease is exclusive and does not create public outputs. TEST(OutputPaths, LeaseCollisionPreservesPublicOutputPaths) { TempDirectory directory{"lease"}; const fesa::DerivedOutputPaths paths{ directory.Path() / "model.inp", directory.Path() / "model_results.h5", directory.Path() / "model_log.txt"}; auto first = fesa::AcquireOutputWriteLease(paths); ASSERT_TRUE(first.HasValue()) << first.GetStatus().Diagnostics().size(); EXPECT_FALSE(std::filesystem::exists(paths.results_path)); EXPECT_FALSE(std::filesystem::exists(paths.log_path)); const auto second = fesa::AcquireOutputWriteLease(paths); ASSERT_FALSE(second.HasValue()); ExpectFailureCode(second.GetStatus(), "output-busy"); EXPECT_FALSE(std::filesystem::exists(paths.results_path)); EXPECT_FALSE(std::filesystem::exists(paths.log_path)); } // NSNR-CLI-009: procedure calls receive all derived output identities. TEST(OutputPaths, AnalysisRequestCarriesInputResultAndLogPaths) { const fesa::AnalysisRequest request{ std::filesystem::path{"C:/models/model.inp"}, std::filesystem::path{"C:/models/model_results.h5"}, std::filesystem::path{"C:/models/model_log.txt"}}; EXPECT_EQ(request.input_path, "C:/models/model.inp"); EXPECT_EQ(request.output_path, "C:/models/model_results.h5"); EXPECT_EQ(request.log_path, "C:/models/model_log.txt"); } } // namespace