#include "fesa/app/fesa_application.hpp" #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 zeroLength, const bool outputRequests) { const std::string secondNode = zeroLength ? "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 = outputRequests ? 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"} + secondNode + 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 normalizedInput = std::filesystem::absolute(input).lexically_normal().generic_u8string(); EXPECT_EQ( readStringAttribute(metadata.get(), "source_input_identity").find( "path=" + normalizedInput + ";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 normalizedInput = std::filesystem::absolute(input).lexically_normal().generic_u8string(); EXPECT_EQ( readStringAttribute(metadata.get(), "source_input_identity").find( "path=" + normalizedInput + ";content_identity="), 0U); } struct AppRun { int exitCode; std::string standardError; }; AppRun runApplication(const std::vector& arguments) { testing::internal::CaptureStderr(); try { const int exitCode = fesa::FesaApplication{}.run(arguments); return {exitCode, 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 defaultOutput = directory.path() / "results.h5"; { CurrentDirectoryGuard currentDirectory{directory.path()}; const auto result = runApplication({input.string()}); ASSERT_EQ(result.exitCode, 0) << result.standardError; } expectFesaHdf5Identity(defaultOutput, input); const auto explicitOutput = directory.path() / "named-output.h5"; const auto result = runApplication( explicitOutputArguments(input, explicitOutput)); ASSERT_EQ(result.exitCode, 0) << result.standardError; expectFesaHdf5Identity(explicitOutput, input); } TEST(LinearStaticCli, ReturnsEveryExactExitCodeAndOrderedDiagnostic) { TempDirectory directory{"exit-codes"}; const auto validInput = directory.path() / "valid.inp"; const auto modelInput = directory.path() / "invalid-model.inp"; const auto solverInput = directory.path() / "singular.inp"; writeText(validInput, axialDeck(true, false, false)); writeText(modelInput, axialDeck(true, true, false)); writeText(solverInput, axialDeck(false, false, false)); const auto success = runApplication(explicitOutputArguments( validInput, directory.path() / "success.h5")); const auto usage = runApplication({}); const auto missingInput = directory.path() / "missing.inp"; const auto input = runApplication({missingInput.string()}); const auto repeatedOutput = runApplication( {missingInput.string(), "--output", "--output"}); const auto unknownOutputOption = runApplication( {missingInput.string(), "--output", "--bogus"}); const auto model = runApplication(explicitOutputArguments( modelInput, directory.path() / "model-failure.h5")); const auto solver = runApplication(explicitOutputArguments( solverInput, directory.path() / "solver-failure.h5")); const auto output = runApplication(explicitOutputArguments( validInput, directory.path() / "nonexistent-parent" / "results.h5")); EXPECT_EQ(success.exitCode, 0) << success.standardError; EXPECT_EQ(usage.exitCode, 2); EXPECT_EQ(input.exitCode, 3); EXPECT_EQ(model.exitCode, 4); EXPECT_EQ(solver.exitCode, 5); EXPECT_EQ(output.exitCode, 6); expectDiagnosticFieldOrder(usage.standardError); expectDiagnosticFieldOrder(input.standardError); expectDiagnosticFieldOrder(model.standardError); expectDiagnosticFieldOrder(solver.standardError); expectDiagnosticFieldOrder(output.standardError); EXPECT_EQ(repeatedOutput.exitCode, 2); expectDiagnosticFieldOrder(repeatedOutput.standardError); EXPECT_NE(repeatedOutput.standardError.find("code=cli-usage"), std::string::npos); EXPECT_EQ(unknownOutputOption.exitCode, 2); expectDiagnosticFieldOrder(unknownOutputOption.standardError); EXPECT_NE(unknownOutputOption.standardError.find("code=cli-usage"), std::string::npos); } TEST(LinearStaticCli, OutputRequestsDoNotFilterMandatoryResults) { TempDirectory directory{"output-requests"}; const auto plainInput = directory.path() / "plain.inp"; const auto requestedInput = directory.path() / "requested.inp"; const auto plainOutput = directory.path() / "plain.h5"; const auto requestedOutput = directory.path() / "requested.h5"; writeText(plainInput, axialDeck(true, false, false)); writeText(requestedInput, axialDeck(true, false, true)); const auto plain = runApplication( explicitOutputArguments(plainInput, plainOutput)); const auto requested = runApplication( explicitOutputArguments(requestedInput, requestedOutput)); ASSERT_EQ(plain.exitCode, 0) << plain.standardError; ASSERT_EQ(requested.exitCode, 0) << requested.standardError; const auto plainFile = openFile(plainOutput); const auto requestedFile = openFile(requestedOutput); expectMandatoryInventory(plainFile.get()); expectMandatoryInventory(requestedFile.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(requestedFile.get(), path), readDoubleDataset(plainFile.get(), path)) << path; } EXPECT_EQ(datasetDimensions(plainFile.get(), "/diagnostics"), std::vector({0U})); const auto requestedDiagnostics = datasetDimensions(requestedFile.get(), "/diagnostics"); ASSERT_EQ(requestedDiagnostics.size(), 1U); EXPECT_GT(requestedDiagnostics[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.exitCode, 0) << result.standardError; expectShellHdf5Identity(output, input); }