#include #include #include #include #include #include #include #include #include #include #include #include "fesa/app/fesa_application.h" #define NOMINMAX #include #include "fesa/analysis/linear_frequency_procedure.h" #include "fesa/analysis/linear_static_procedure.h" #include "fesa/analysis/newton_iteration_log.h" #include "fesa/analysis/nonlinear_static_procedure.h" #include "fesa/analysis/prepared_analysis.h" #include "fesa/app/output_paths.h" #include "fesa/assembly/parallel_for.h" #include "fesa/io/hdf5/eigen_hdf5_results_writer.h" #include "fesa/io/hdf5/hdf5_results_writer.h" #include "fesa/io/hdf5/nonlinear_hdf5_results_writer.h" #include "fesa/solvers/linear/mkl_pardiso_solver.h" #include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h" namespace { class HdfHandle { public: HdfHandle(hid_t value, herr_t (*close)(hid_t)) : value_(value), close_(close) { if (value < 0) throw std::runtime_error{"Cannot open HDF test object."}; } ~HdfHandle() { close_(value_); } HdfHandle(const HdfHandle&) = delete; HdfHandle& operator=(const HdfHandle&) = delete; hid_t Get() const { return value_; } private: hid_t value_; herr_t (*close_)(hid_t); }; std::vector Shape(hid_t file, const std::string& path) { HdfHandle data{H5Dopen2(file, path.c_str(), H5P_DEFAULT), H5Dclose}; HdfHandle space{H5Dget_space(data.Get()), H5Sclose}; const int rank = H5Sget_simple_extent_ndims(space.Get()); if (rank < 0) throw std::runtime_error{"Invalid HDF test shape."}; std::vector shape(static_cast(rank)); EXPECT_GE(H5Sget_simple_extent_dims(space.Get(), shape.data(), nullptr), 0); return shape; } std::vector Doubles(hid_t file, const std::string& path) { std::size_t count = 1; for (auto extent : Shape(file, path)) count *= static_cast(extent); std::vector result(count); HdfHandle data{H5Dopen2(file, path.c_str(), H5P_DEFAULT), H5Dclose}; EXPECT_GE(H5Dread(data.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, result.data()), 0); return result; } std::string StringAttribute(hid_t object, const char* name) { HdfHandle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose}; HdfHandle type{H5Aget_type(attribute.Get()), H5Tclose}; char* value = nullptr; EXPECT_GE(H5Aread(attribute.Get(), type.Get(), &value), 0); const std::string result = value == nullptr ? "" : value; if (value != nullptr) H5free_memory(value); return result; } struct FrameRow { std::uint64_t frame_index; std::uint64_t increment_index; double time; double time_increment; std::uint8_t is_initial; }; std::vector Frames(hid_t file) { const std::string path = "/steps/Step-1/accepted_frames"; const auto shape = Shape(file, path); if (shape.size() != 1) throw std::runtime_error{"Invalid frame shape."}; std::vector frames(static_cast(shape[0])); HdfHandle type{H5Tcreate(H5T_COMPOUND, sizeof(FrameRow)), H5Tclose}; EXPECT_GE(H5Tinsert(type.Get(), "frame_index", HOFFSET(FrameRow, frame_index), H5T_NATIVE_UINT64), 0); EXPECT_GE(H5Tinsert(type.Get(), "increment_index", HOFFSET(FrameRow, increment_index), H5T_NATIVE_UINT64), 0); EXPECT_GE( H5Tinsert(type.Get(), "time", HOFFSET(FrameRow, time), H5T_NATIVE_DOUBLE), 0); EXPECT_GE(H5Tinsert(type.Get(), "time_increment", HOFFSET(FrameRow, time_increment), H5T_NATIVE_DOUBLE), 0); EXPECT_GE(H5Tinsert(type.Get(), "is_initial", HOFFSET(FrameRow, is_initial), H5T_NATIVE_UINT8), 0); HdfHandle data{H5Dopen2(file, path.c_str(), H5P_DEFAULT), H5Dclose}; EXPECT_GE(H5Dread(data.Get(), type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, frames.data()), 0); return frames; } std::string ReadBytes(const std::filesystem::path& path) { std::ifstream input{path, std::ios::binary}; return {std::istreambuf_iterator{input}, std::istreambuf_iterator{}}; } std::string Replace(std::string text, const std::string& from, const std::string& to) { const auto position = text.find(from); if (position == std::string::npos) throw std::runtime_error{"Missing fixture text."}; text.replace(position, from.size(), to); return text; } struct AppRun { int exit_code; std::string error; }; AppRun RunApplication(const std::filesystem::path& input) { testing::internal::CaptureStderr(); const int result = fesa::FesaApplication{}.Run({input.string()}); return {result, testing::internal::GetCapturedStderr()}; } class ThermalApplicationTest : public testing::Test { protected: void SetUp() override { static std::atomic counter{0}; do { directory_ = std::filesystem::temp_directory_path() / ("fesa-thermal-app-" + std::to_string(std::random_device{}()) + "-" + std::to_string(counter.fetch_add(1))); } while (!std::filesystem::create_directory(directory_)); } void TearDown() override { std::error_code error; std::filesystem::remove_all(directory_, error); } std::filesystem::path Path(const std::string& name) const { return directory_ / name; } void Write(const std::string& name, const std::string& text) { std::ofstream output{Path(name), std::ios::binary}; output << text; ASSERT_TRUE(output.good()); } std::filesystem::path directory_; }; std::string Deck(const std::string& controls = ".3,.7", const std::string& initial = "-2") { return R"inp(*Part,name=P *Node 1,0,0,0 2,1,0,0 3,1,1,0 4,0,1,0 *Element,type=DS4 1,1,2,3,4 *Elset,elset=E 1 *Shell Section,elset=E,material=M 1,5 *End Part *Assembly,name=A *Instance,name=I,part=P *End Instance *Nset,nset=All,instance=I 1,2,3,4 *End Assembly *Material,name=M *Density 1 *Conductivity 1 *Specific Heat 1 *Initial Conditions,type=TEMPERATURE All,)inp" + initial + R"inp( *Step,name=Raw Heat,inc=10 *Heat Transfer )inp" + controls + R"inp( *Output,field,variable=PRESELECT *End Step )inp"; } TEST_F(ThermalApplicationTest, ExecutesThermalInputThroughAtomicOutput) { Write("Heat.Model.INP", Deck()); const auto run = RunApplication(Path("Heat.Model.INP")); ASSERT_EQ(run.exit_code, 0) << run.error; EXPECT_GT(H5Fis_hdf5(Path("Heat.Model_results.h5").string().c_str()), 0); EXPECT_EQ(std::filesystem::file_size(Path("Heat.Model_log.txt")), 0U); HdfHandle file{H5Fopen(Path("Heat.Model_results.h5").string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), H5Fclose}; HdfHandle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose}; EXPECT_EQ(StringAttribute(metadata.Get(), "source_step_name"), "Raw Heat"); EXPECT_EQ(StringAttribute(metadata.Get(), "feature_id"), "shell-transient-heat-transfer"); EXPECT_EQ(StringAttribute(metadata.Get(), "analysis_procedure"), "uncoupled-transient-heat-transfer"); EXPECT_EQ(StringAttribute(metadata.Get(), "source_input_identity") .find("path=" + Path("Heat.Model.INP").generic_u8string() + ";content_identity="), 0U); const auto frames = Frames(file.Get()); ASSERT_EQ(frames.size(), 4U); for (std::size_t index = 0; index < frames.size(); ++index) { const auto& frame = frames[index]; EXPECT_EQ(frame.frame_index, index); EXPECT_EQ(frame.increment_index, index); EXPECT_EQ(frame.is_initial, index == 0 ? 1 : 0); EXPECT_DOUBLE_EQ(frame.time, index == 3 ? .7 : static_cast(index) * .3); EXPECT_DOUBLE_EQ(frame.time_increment, index == 0 ? 0 : index == 3 ? std::fma(-2., .3, .7) : .3); const auto root = "/steps/Step-1/frames/" + std::to_string(index); EXPECT_EQ(Shape(file.Get(), root + "/nodal/temperature"), (std::vector{4, 5})); for (double value : Doubles(file.Get(), root + "/nodal/temperature")) { EXPECT_TRUE(std::isfinite(value)); EXPECT_LT(value, 0.); if (index == 0) EXPECT_DOUBLE_EQ(value, -2.); } if (index == 0) { EXPECT_EQ( H5Lexists(file.Get(), (root + "/nodal/reaction_heat_rate").c_str(), H5P_DEFAULT), 0); EXPECT_EQ(H5Lexists(file.Get(), (root + "/element").c_str(), H5P_DEFAULT), 0); } else { EXPECT_EQ(Shape(file.Get(), root + "/nodal/reaction_heat_rate"), (std::vector{0})); EXPECT_EQ(Shape(file.Get(), root + "/element/heat_flux"), (std::vector{1, 4, 6, 3})); } } EXPECT_EQ(Shape(file.Get(), "/diagnostics"), (std::vector{1})); HdfHandle diagnostics{H5Dopen2(file.Get(), "/diagnostics", H5P_DEFAULT), H5Dclose}; HdfHandle string_type{H5Tcopy(H5T_C_S1), H5Tclose}; ASSERT_GE(H5Tset_size(string_type.Get(), H5T_VARIABLE), 0); ASSERT_GE(H5Tset_cset(string_type.Get(), H5T_CSET_UTF8), 0); HdfHandle row_type{H5Tcreate(H5T_COMPOUND, sizeof(char*)), H5Tclose}; ASSERT_GE(H5Tinsert(row_type.Get(), "code", 0, string_type.Get()), 0); char* code = nullptr; ASSERT_GE(H5Dread(diagnostics.Get(), row_type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, &code), 0); EXPECT_STREQ(code, "ignored-input-keyword"); H5free_memory(code); } TEST_F(ThermalApplicationTest, EndpointCloseUsesActualDeltaAndLargeIncrementUsesPeriod) { for (const auto& controls : {".3,.9", "2,.7"}) { SCOPED_TRACE(controls); Write("model.inp", Deck(controls, "0")); const auto run = RunApplication(Path("model.inp")); ASSERT_EQ(run.exit_code, 0) << run.error; HdfHandle file{H5Fopen(Path("model_results.h5").string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), H5Fclose}; const auto frames = Frames(file.Get()); if (std::string{controls} == ".3,.9") { ASSERT_EQ(frames.size(), 4U); EXPECT_DOUBLE_EQ(frames.back().time, .9); EXPECT_DOUBLE_EQ(frames.back().time_increment, .3); EXPECT_NE(frames.back().time_increment, frames[3].time - frames[2].time); } else { ASSERT_EQ(frames.size(), 2U); EXPECT_DOUBLE_EQ(frames.back().time, .7); EXPECT_DOUBLE_EQ(frames.back().time_increment, .7); } } } TEST_F(ThermalApplicationTest, RawInitialPrecedesPrescribedBoundaryProjection) { auto deck = Replace(Deck(".1,.2"), "*Output,field", "*Boundary\nAll,11,11,1\nAll,15,15,1\n*Output,field"); Write("model.inp", deck); const auto run = RunApplication(Path("model.inp")); ASSERT_EQ(run.exit_code, 0) << run.error; HdfHandle file{H5Fopen(Path("model_results.h5").string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), H5Fclose}; for (double value : Doubles(file.Get(), "/steps/Step-1/frames/0/nodal/temperature")) EXPECT_DOUBLE_EQ(value, -2); for (int frame = 1; frame <= 2; ++frame) { const auto root = "/steps/Step-1/frames/" + std::to_string(frame); const auto temperature = Doubles(file.Get(), root + "/nodal/temperature"); for (std::size_t node = 0; node < 4; ++node) { EXPECT_DOUBLE_EQ(temperature[5 * node], 1); EXPECT_DOUBLE_EQ(temperature[5 * node + 4], 1); } EXPECT_EQ(Shape(file.Get(), root + "/nodal/reaction_heat_rate"), (std::vector{8})); } } TEST_F(ThermalApplicationTest, InputAndSolverFailuresPreservePreviousFinal) { const std::string previous{"previous final\0bytes", 20}; for (const auto& test : std::vector>{ {Replace(Deck(), "*Output,field", "*Boundary\nAll,12,12,1\n*Output,field"), 3}, {Deck("1.e-300,1.e-300", "1.e200"), 5}}) { Write("model.inp", test.first); Write("model_results.h5", previous); const auto run = RunApplication(Path("model.inp")); EXPECT_EQ(run.exit_code, test.second) << run.error; EXPECT_FALSE(run.error.empty()); EXPECT_EQ(ReadBytes(Path("model_results.h5")), previous); EXPECT_EQ(std::filesystem::file_size(Path("model_log.txt")), 0U); } } TEST_F(ThermalApplicationTest, BusyLeasePrecedesParsingAndPreservesBothOutputs) { Write("model.inp", "deliberately invalid input"); Write("model_results.h5", "previous final"); Write("model_log.txt", "previous log"); fesa::DerivedOutputPaths paths{Path("model.inp"), Path("model_results.h5"), Path("model_log.txt")}; auto lease = fesa::AcquireOutputWriteLease(paths); ASSERT_TRUE(lease.HasValue()); const auto run = RunApplication(paths.input_path); EXPECT_EQ(run.exit_code, 6); EXPECT_NE(run.error.find("code=output-busy"), std::string::npos); EXPECT_EQ(ReadBytes(paths.results_path), "previous final"); EXPECT_EQ(ReadBytes(paths.log_path), "previous log"); } TEST_F(ThermalApplicationTest, RealFinalizeFailurePreservesFinalAndRemovesCandidate) { Write("model.inp", Deck(".1,.1", "0")); Write("model_results.h5", "previous final"); // Deny replacement of this final while permitting reads. The output lease is // separate; the candidate must reach the actual atomic finalize operation. const HANDLE locked = CreateFileW(Path("model_results.h5").c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr); ASSERT_NE(locked, INVALID_HANDLE_VALUE); const auto run = RunApplication(Path("model.inp")); EXPECT_TRUE(CloseHandle(locked)); EXPECT_EQ(run.exit_code, 6) << run.error; EXPECT_EQ(ReadBytes(Path("model_results.h5")), "previous final"); EXPECT_EQ(std::filesystem::file_size(Path("model_log.txt")), 0U); for (const auto& entry : std::filesystem::directory_iterator{directory_}) { EXPECT_EQ(entry.path().filename().string().find(".tmp."), std::string::npos); } } TEST_F(ThermalApplicationTest, MissingAdaptersHaveSpecificFailureCategories) { Write("model.inp", Deck(".1,.1", "0")); auto prepared = fesa::PrepareAnalysisInput(Path("model.inp")); ASSERT_TRUE(prepared.HasValue()); ASSERT_EQ(prepared.Value().domain.Steps().Size(), 0U); fesa::SerialParallelFor parallel; fesa::MklPardisoSolver solver; fesa::MklPardisoTangentSolver tangent; fesa::LinearStaticProcedureExecutor linear{parallel, solver}; fesa::NonlinearStaticProcedureExecutor nonlinear{parallel, tangent}; fesa::LinearFrequencyProcedureExecutor frequency; fesa::TransientHeatProcedureExecutor thermal{parallel, solver}; fesa::NewtonIterationLogWriter log; ASSERT_TRUE(log.Open(Path("model_log.txt")).IsOk()); auto missing = fesa::ExecutePreparedAnalysis(prepared.Value().domain, linear, nonlinear, frequency, log); ASSERT_FALSE(missing.HasValue()); EXPECT_EQ(missing.GetStatus().Category(), fesa::FailureCategory::kSolver); ASSERT_EQ(missing.GetStatus().Diagnostics().size(), 1U); EXPECT_EQ(missing.GetStatus().Diagnostics()[0].code, "missing-thermal-executor"); auto outcome = fesa::ExecutePreparedAnalysis(prepared.Value().domain, linear, nonlinear, frequency, log, nullptr, &thermal); ASSERT_TRUE(outcome.HasValue()); ASSERT_TRUE( std::holds_alternative(outcome.Value())); ASSERT_TRUE(log.Close().IsOk()); fesa::Hdf5ResultsWriter linear_writer; fesa::NonlinearHdf5ResultsWriter nonlinear_writer; fesa::EigenHdf5ResultsWriter eigen_writer; const fesa::DerivedOutputPaths paths{ Path("model.inp"), Path("model_results.h5"), Path("model_log.txt")}; Write("model_results.h5", "previous final"); const auto status = fesa::FinalizePreparedAnalysisResult( paths, prepared.Value().domain, outcome.Value(), prepared.Value().diagnostics, linear_writer, nonlinear_writer, eigen_writer); EXPECT_EQ(status.Category(), fesa::FailureCategory::kOutput); ASSERT_EQ(status.Diagnostics().size(), 1U); EXPECT_EQ(status.Diagnostics()[0].code, "missing-thermal-writer"); EXPECT_EQ(ReadBytes(paths.results_path), "previous final"); } TEST_F(ThermalApplicationTest, ExistingStructuralStaticNonlinearAndHhtRoutesRemainAvailable) { auto base = Replace(Deck(), "type=DS4", "type=S4"); base = Replace(base, "*Conductivity\n1\n*Specific Heat\n1", "*Elastic\n1000,.25"); base.erase(base.find("*Initial Conditions")); base += "*Boundary\nAll,1,6\n"; for (const auto& step : {"*Step,name=Structural,nlgeom=NO\n*Static\n.1,.1,.01,.1\n*End Step\n", "*Step,name=Structural,nlgeom=YES\n*Static\n.1,1.,.01,.1\n*End Step\n", "*Step,name=Structural,nlgeom=YES,inc=10\n*Dynamic,direct\n.1,.2\n*End " "Step\n"}) { SCOPED_TRACE(step); const bool dynamic = std::string{step}.find("*Dynamic") != std::string::npos; Write("model.inp", (dynamic ? base : Replace(base, "*Density\n1\n", "")) + step); const auto run = RunApplication(Path("model.inp")); ASSERT_EQ(run.exit_code, 0) << run.error; EXPECT_GT(H5Fis_hdf5(Path("model_results.h5").string().c_str()), 0); } } TEST_F(ThermalApplicationTest, NativeExecutableAcceptsThermalInputAndRejectsInteriorBoundary) { std::vector module(32768); const DWORD length = GetModuleFileNameW(nullptr, module.data(), static_cast(module.size())); ASSERT_GT(length, 0U); ASSERT_LT(length, module.size()); const auto executable = std::filesystem::path{std::wstring{module.data(), length}} .parent_path() .parent_path() / "src/fesa/fesa.exe"; ASSERT_TRUE(std::filesystem::exists(executable)); std::string previous; for (const bool valid : {true, false}) { Write("native.inp", valid ? Deck(".1,.1", "0") : Replace(Deck(), "*Output,field", "*Boundary\nAll,12,12,1\n*Output,field")); std::wstring command = L"\"" + executable.wstring() + L"\" \"" + Path("native.inp").wstring() + L"\""; STARTUPINFOW startup{}; startup.cb = sizeof(startup); PROCESS_INFORMATION process{}; ASSERT_TRUE(CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &startup, &process)); const DWORD wait = WaitForSingleObject(process.hProcess, 60000); if (wait != WAIT_OBJECT_0) TerminateProcess(process.hProcess, 99); DWORD exit = 99; EXPECT_TRUE(GetExitCodeProcess(process.hProcess, &exit)); CloseHandle(process.hThread); CloseHandle(process.hProcess); ASSERT_EQ(wait, WAIT_OBJECT_0); EXPECT_EQ(exit, valid ? 0U : 3U); EXPECT_GT(H5Fis_hdf5(Path("native_results.h5").string().c_str()), 0); if (valid) { previous = ReadBytes(Path("native_results.h5")); } else { EXPECT_EQ(ReadBytes(Path("native_results.h5")), previous); } } } } // namespace