#include "fesa/app/fesa_application.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr const char* kStepRoot = "/steps/Step-1/frames/0"; class TempDirectory { public: explicit TempDirectory(const std::string& label) { static std::atomic sequence{0U}; const auto tick = std::chrono::steady_clock::now().time_since_epoch().count(); path_ = std::filesystem::temp_directory_path() / ("fesa-step24-app-" + label + "-" + std::to_string(tick) + "-" + std::to_string(sequence.fetch_add(1U))); std::error_code error; if (!std::filesystem::create_directory(path_, error) || error) { throw std::runtime_error{"Unable to create the Step 24 app 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_; }; class CurrentDirectoryGuard { public: explicit CurrentDirectoryGuard(const std::filesystem::path& replacement) : original_{std::filesystem::current_path()} { std::filesystem::current_path(replacement); } CurrentDirectoryGuard(const CurrentDirectoryGuard&) = delete; CurrentDirectoryGuard& operator=(const CurrentDirectoryGuard&) = delete; ~CurrentDirectoryGuard() { std::error_code ignored; std::filesystem::current_path(original_, ignored); } private: std::filesystem::path original_; }; class Hdf5Handle { public: using Closer = herr_t (*)(hid_t); Hdf5Handle(const hid_t value, Closer closer) : value_{value}, closer_{closer} {} Hdf5Handle(const Hdf5Handle&) = delete; Hdf5Handle& operator=(const Hdf5Handle&) = delete; Hdf5Handle(Hdf5Handle&& other) noexcept : value_{other.value_}, closer_{other.closer_} { other.value_ = -1; other.closer_ = nullptr; } ~Hdf5Handle() { if (value_ >= 0 && closer_ != nullptr) { (void)closer_(value_); } } hid_t Get() const noexcept { return value_; } private: hid_t value_{-1}; Closer closer_{nullptr}; }; void WriteText(const std::filesystem::path& path, const std::string& text) { std::ofstream stream{path, std::ios::binary | std::ios::trunc}; stream.write(text.data(), static_cast(text.size())); if (!stream) { throw std::runtime_error{"Unable to write the Step 24 app input."}; } } std::string AxialDeck(const bool constrained, const bool zero_length, const bool output_requests) { const std::string second_node = zero_length ? "2, 0., 0., 0.\n" : "2, 2., 0., 0.\n"; const std::string boundaries = constrained ? "*Boundary\nRoot, 1, 6\nTip, 2, 6\n" : ""; const std::string outputs = output_requests ? R"inp(*Output, field *Node Output U, RF *Element Output, directions=YES S, SF *Output, history *Contact Output )inp" : ""; return std::string{R"inp(*Part, name=BeamPart *Node 1, 0., 0., 0. )inp"} + second_node + R"inp(*Element, type=B33 1, 1, 2 *Elset, elset=BeamSet 1 *Beam General Section, elset=BeamSet, material=Steel, section=GENERAL 2., 0.5, 0., 0.75, 0.25 0., 1., 0. *End Part *Assembly, name=Assembly *Instance, name=Beam-1, part=BeamPart *End Instance *Nset, nset=Root, instance=Beam-1 1 *Nset, nset=Tip, instance=Beam-1 2 *End Assembly *Material, name=Steel *Elastic 100., 0.25 )inp" + boundaries + R"inp(*Step, name=Load, nlgeom=NO *Static 0.1, 1., 0.01, 1. *Cload Tip, 1, 10. )inp" + outputs + R"inp(*End Step )inp"; } std::string AllConstrainedShellDeck() { return R"inp(*Part, name=ShellPart *Node 1, 0., 0., 0. 2, 1., 0., 0. 3, 1., 1., 0. 4, 0., 1., 0. *Element, type=S4 1, 1, 2, 3, 4 *Elset, elset=ShellSet 1 *Shell Section, elset=ShellSet, material=Steel 0.1 *End Part *Assembly, name=Assembly *Instance, name=Shell-1, part=ShellPart *End Instance *Nset, nset=All, instance=Shell-1 1, 2, 3, 4 *End Assembly *Material, name=Steel *Elastic 1000., 0.25 *Boundary All, 1, 6 *Step, name=Load, nlgeom=NO *Static 0.1, 1., 0.01, 1. *End Step )inp"; } Hdf5Handle OpenFile(const std::filesystem::path& path) { const hid_t file = H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); if (file < 0) { throw std::runtime_error{"Unable to open the CLI HDF5 artifact."}; } return Hdf5Handle{file, H5Fclose}; } Hdf5Handle OpenDataset(const hid_t file, const std::string& path) { const hid_t dataset = H5Dopen2(file, path.c_str(), H5P_DEFAULT); if (dataset < 0) { throw std::runtime_error{"Unable to open mandatory dataset: " + path}; } return Hdf5Handle{dataset, H5Dclose}; } std::vector 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()); if (space.Get() < 0 || rank < 0) { throw std::runtime_error{"Unable to inspect mandatory dataset dimensions."}; } std::vector dimensions(static_cast(rank)); if (rank > 0 && H5Sget_simple_extent_dims(space.Get(), dimensions.data(), nullptr) < 0) { throw std::runtime_error{"Unable to read mandatory dataset dimensions."}; } return dimensions; } std::vector 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(dimension); } const auto dataset = OpenDataset(file, path); std::vector values(count); if (!values.empty() && H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()) < 0) { throw std::runtime_error{"Unable to read mandatory numeric results."}; } return values; } 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}; if (attribute.Get() < 0 || type.Get() < 0 || H5Tget_class(type.Get()) != H5T_STRING || H5Tis_variable_str(type.Get()) <= 0) { throw std::runtime_error{"Expected a variable-length string attribute."}; } char* raw = nullptr; if (H5Aread(attribute.Get(), type.Get(), &raw) < 0 || raw == nullptr) { throw std::runtime_error{"Unable to read the HDF5 identity attribute."}; } const std::string value{raw}; (void)H5free_memory(raw); return value; } void ExpectMandatoryInventory(const hid_t file) { for (const char* path : {"/metadata", "/model/nodes", "/model/elements", "/steps/Step-1/frames/0/nodal/displacement", "/steps/Step-1/frames/0/nodal/reaction", "/steps/Step-1/frames/0/element/end_force_local", "/steps/Step-1/frames/0/element/section_resultant", "/steps/Step-1/frames/0/element/generalized_strain", "/steps/Step-1/frames/0/element/generalized_resultant", "/steps/Step-1/frames/0/element/stress_s11", "/diagnostics"}) { EXPECT_GT(H5Lexists(file, path, H5P_DEFAULT), 0) << path; } } void ExpectFesaHdf5Identity(const std::filesystem::path& output, const std::filesystem::path& input) { ASSERT_TRUE(std::filesystem::exists(output)); ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0); const auto file = OpenFile(output); ExpectMandatoryInventory(file.Get()); Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose}; ASSERT_GE(metadata.Get(), 0); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "feature_id"), "linear-static-3d-euler-beam"); const std::string normalized_input = std::filesystem::absolute(input).lexically_normal().generic_u8string(); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "source_input_identity") .find("path=" + normalized_input + ";content_identity="), 0U); } void ExpectShellHdf5Identity(const std::filesystem::path& output, const std::filesystem::path& input) { ASSERT_TRUE(std::filesystem::exists(output)); ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0); const auto file = OpenFile(output); Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose}; ASSERT_GE(metadata.Get(), 0); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "feature_id"), "linear-static-mitc4-shell"); EXPECT_GT(H5Lexists(file.Get(), "/steps/Step-1/frames/0/element/shell/generalized_strain", H5P_DEFAULT), 0); EXPECT_EQ(DatasetDimensions(file.Get(), "/steps/Step-1/frames/0/nodal/displacement"), (std::vector{4U, 6U})); const std::string normalized_input = std::filesystem::absolute(input).lexically_normal().generic_u8string(); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "source_input_identity") .find("path=" + normalized_input + ";content_identity="), 0U); } struct AppRun { int exit_code; std::string standard_error; }; AppRun RunApplication(const std::vector& arguments) { testing::internal::CaptureStderr(); try { const int exit_code = fesa::FesaApplication{}.Run(arguments); return {exit_code, testing::internal::GetCapturedStderr()}; } catch (...) { (void)testing::internal::GetCapturedStderr(); throw; } } void ExpectDiagnosticFieldOrder(const std::string& text) { ASSERT_FALSE(text.empty()); std::size_t cursor = 0U; for (const char* field : {"severity", "code", "file", "line", "keyword", "entity_identity", "message"}) { const auto position = text.find(field, cursor); ASSERT_NE(position, std::string::npos) << "Missing or out-of-order diagnostic field: " << field << "\nstderr:\n" << text; cursor = position + std::string{field}.size(); } } std::vector 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()}; } } // namespace TEST(LinearStaticCli, DefaultAndExplicitOutputProduceFesaHdf5) { TempDirectory directory{"paths"}; const auto input = directory.Path() / "model.inp"; WriteText(input, AxialDeck(true, false, false)); const auto default_output = directory.Path() / "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); } TEST(LinearStaticCli, ReturnsEveryExactExitCodeAndOrderedDiagnostic) { TempDirectory directory{"exit-codes"}; const auto valid_input = directory.Path() / "valid.inp"; const auto model_input = directory.Path() / "invalid-model.inp"; const auto solver_input = directory.Path() / "singular.inp"; WriteText(valid_input, AxialDeck(true, false, false)); 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 usage = RunApplication({}); const auto missing_input = directory.Path() / "missing.inp"; const auto input = RunApplication({missing_input.string()}); const auto repeated_output = 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")); 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"), std::string::npos); EXPECT_EQ(unknown_output_option.exit_code, 2); ExpectDiagnosticFieldOrder(unknown_output_option.standard_error); EXPECT_NE(unknown_output_option.standard_error.find("code=cli-usage"), std::string::npos); } 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)); 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); ExpectMandatoryInventory(plain_file.Get()); ExpectMandatoryInventory(requested_file.Get()); for (const char* suffix : {"/nodal/displacement", "/nodal/reaction", "/element/end_force_local", "/element/section_resultant", "/element/generalized_strain", "/element/generalized_resultant"}) { const std::string path = std::string{kStepRoot} + suffix; EXPECT_EQ(ReadDoubleDataset(requested_file.Get(), path), ReadDoubleDataset(plain_file.Get(), path)) << path; } EXPECT_EQ(DatasetDimensions(plain_file.Get(), "/diagnostics"), std::vector({0U})); const auto requested_diagnostics = DatasetDimensions(requested_file.Get(), "/diagnostics"); ASSERT_EQ(requested_diagnostics.size(), 1U); EXPECT_GT(requested_diagnostics[0U], 0U); } // MITC4-FLOW-001: shell input uses the unchanged application route and syntax. TEST(Mitc4ShellCli, WritesShellHdf5ThroughTheExistingApplicationRoute) { TempDirectory directory{"shell-route"}; const auto input = directory.Path() / "shell.inp"; const auto output = directory.Path() / "shell-results.h5"; WriteText(input, AllConstrainedShellDeck()); const auto result = RunApplication(ExplicitOutputArguments(input, output)); ASSERT_EQ(result.exit_code, 0) << result.standard_error; ExpectShellHdf5Identity(output, input); }