#include "fesa/results/results_writer.h" #include "fesa/results/prepared_results_writer.h" #include "fesa/results/dynamic_results_writer.h" #include "fesa/results/eigen_results_writer.h" #include "fesa/results/nonlinear_results_writer.h" #include "fesa/results/solid_results_writer.h" #include "fesa/results/thermal_results_writer.h" #include #include #include #include #include #include #include #include "fesa/analysis/analysis_model.h" #include "fesa/analysis/analysis_state.h" #include "fesa/core/diagnostic.h" #include "fesa/core/status.h" #include "fesa/model/domain.h" namespace { using WriteSignature = fesa::Status (fesa::ResultsWriter::*)( const std::filesystem::path&, const fesa::Domain&, const fesa::AnalysisState&, const std::vector&); static_assert(std::has_virtual_destructor_v); static_assert(std::is_abstract_v); static_assert( std::is_same_v); } // namespace namespace { // These adapters only observe borrowed values; no procedure, kernel, or file I/O. struct WriterCalls { std::vector paths; std::vector domains; std::vector*> diagnostics; std::vector families; fesa::Status Record(unsigned family, const std::filesystem::path& path, const fesa::Domain& domain, const std::vector& values) { paths.push_back(&path); domains.push_back(&domain); diagnostics.push_back(&values); families.push_back(family); return fesa::Status::Failure( fesa::FailureCategory::kOutput, {{fesa::Severity::kError, "candidate-close-failure", {"source.inp", 7U}, "WRITER", "0007", "Preserved writer failure."}}); } }; template class RecordingWriter final : public Writer { public: explicit RecordingWriter(WriterCalls& calls) : calls_{calls} {} fesa::Status Write(const std::filesystem::path& path, const fesa::Domain& domain, const State& state, const std::vector& diagnostics) override { received = &state; return calls_.Record(Family, path, domain, diagnostics); } const State* received{nullptr}; private: WriterCalls& calls_; }; class RecordingSolidWriter final : public fesa::SolidResultsWriter { public: explicit RecordingSolidWriter(WriterCalls& calls) : calls_{calls} {} fesa::Status Write(const std::filesystem::path& p, const fesa::Domain& d, const fesa::SolidStaticState& s, const std::vector& v) override { static_state = &s; return calls_.Record(5U, p, d, v); } fesa::Status Write(const std::filesystem::path& p, const fesa::Domain& d, const fesa::SolidFrequencyState& s, const std::vector& v) override { frequency_state = &s; return calls_.Record(6U, p, d, v); } fesa::Status Write(const std::filesystem::path& p, const fesa::Domain& d, const fesa::SolidDynamicAnalysisState& s, const std::vector& v) override { dynamic_state = &s; return calls_.Record(7U, p, d, v); } const fesa::SolidStaticState* static_state{nullptr}; const fesa::SolidFrequencyState* frequency_state{nullptr}; const fesa::SolidDynamicAnalysisState* dynamic_state{nullptr}; private: WriterCalls& calls_; }; // Empty values intentionally bypass physics: dispatch must preserve their type. std::vector EmptyOutcomes() { std::vector values; values.emplace_back(fesa::LinearStaticExecutionResult{ fesa::AnalysisState::Create(fesa::DofManager{}, {"source/step", 9U}), {}}); fesa::ModelDefinition definition{}; definition.steps = {{"value-only", {}, {}, 0.1, 1.0, 0.01, 1.0, {}}}; auto domain = fesa::Domain::Create(std::move(definition)); auto model = fesa::AnalysisModel::Create(domain.Value()); auto dofs = fesa::DofManager::Create(model.Value()); auto nonlinear = fesa::NonlinearAnalysisState::Create( domain.Value(), dofs.Value(), {}); if (!nonlinear.HasValue()) throw std::runtime_error{"Empty nonlinear value construction failed."}; values.emplace_back( fesa::NonlinearStaticExecutionResult{std::move(nonlinear.Value()), {}}); values.emplace_back(fesa::EigenAnalysisState{ fesa::FeastEigenResult{0U, {}, fesa::Matrix{0U, 0U}}}); values.emplace_back(fesa::DynamicExecutionResult{}); values.emplace_back(fesa::SolidStaticExecutionResult{}); values.emplace_back(fesa::SolidFrequencyState{ fesa::FeastEigenResult{0U, {}, fesa::Matrix{0U, 0U}}}); values.emplace_back(fesa::SolidDynamicExecutionResult{}); return values; } } // namespace TEST(PreparedResultsWriter, RoutesValueOutcomesAndPreservesWriterFailure) { auto domain = fesa::Domain::Create({}); ASSERT_TRUE(domain.HasValue()); const std::filesystem::path output{"relative/custom-result.h5"}; const std::vector diagnostics{ {fesa::Severity::kWarning, "original-warning", {"source.inp", 3U}, "NODE", "0003", "Original diagnostic."}}; WriterCalls calls; RecordingWriter linear{calls}; RecordingWriter nonlinear{calls}; RecordingWriter eigen{calls}; RecordingWriter dynamic{calls}; RecordingWriter thermal{calls}; RecordingSolidWriter solid{calls}; auto outcomes = EmptyOutcomes(); const std::array families{0U, 1U, 2U, 3U, 5U, 6U, 7U}; ASSERT_EQ(outcomes.size(), families.size()); for (std::size_t index = 0U; index < outcomes.size(); ++index) { const auto status = fesa::WritePreparedAnalysisResult( output, domain.Value(), outcomes[index], diagnostics, linear, nonlinear, eigen, &dynamic, &thermal, &solid); ASSERT_EQ(calls.families.size(), index + 1U); EXPECT_EQ(calls.families.back(), families[index]); EXPECT_EQ(calls.paths.back(), &output); EXPECT_EQ(calls.domains.back(), &domain.Value()); EXPECT_EQ(calls.diagnostics.back(), &diagnostics); EXPECT_EQ(status.Category(), fesa::FailureCategory::kOutput); ASSERT_EQ(status.Diagnostics().size(), 1U); EXPECT_EQ(status.Diagnostics()[0U].code, "candidate-close-failure"); EXPECT_EQ(status.Diagnostics()[0U].location.file, "source.inp"); EXPECT_EQ(status.Diagnostics()[0U].location.line, 7U); EXPECT_EQ(status.Diagnostics()[0U].keyword, "WRITER"); EXPECT_EQ(status.Diagnostics()[0U].entity_identity, "0007"); EXPECT_EQ(status.Diagnostics()[0U].message, "Preserved writer failure."); } EXPECT_EQ(linear.received, &std::get(outcomes[0U]).state); EXPECT_EQ(nonlinear.received, &std::get(outcomes[1U]).state); EXPECT_EQ(eigen.received, &std::get(outcomes[2U])); EXPECT_EQ(dynamic.received, &std::get(outcomes[3U]).state); EXPECT_EQ(thermal.received, nullptr); EXPECT_EQ(solid.static_state, &std::get(outcomes[4U]).state); EXPECT_EQ(solid.frequency_state, &std::get(outcomes[5U])); EXPECT_EQ(solid.dynamic_state, &std::get(outcomes[6U]).state); } TEST(PreparedResultsWriter, MissingOptionalAdapterInvokesNoOtherWriter) { auto domain = fesa::Domain::Create({}); ASSERT_TRUE(domain.HasValue()); WriterCalls calls; RecordingWriter linear{calls}; RecordingWriter nonlinear{calls}; RecordingWriter eigen{calls}; auto outcomes = EmptyOutcomes(); const std::array codes{ "missing-dynamic-writer", "missing-solid-writer", "missing-solid-writer", "missing-solid-writer"}; const std::array keywords{"DYNAMIC", "", "", ""}; for (std::size_t index = 3U; index < outcomes.size(); ++index) { const auto status = fesa::WritePreparedAnalysisResult( "explicit.h5", domain.Value(), outcomes[index], {}, linear, nonlinear, eigen); EXPECT_FALSE(status.IsOk()); EXPECT_EQ(status.Category(), fesa::FailureCategory::kOutput); ASSERT_EQ(status.Diagnostics().size(), 1U); EXPECT_EQ(status.Diagnostics()[0U].code, codes[index - 3U]); EXPECT_EQ(status.Diagnostics()[0U].keyword, keywords[index - 3U]); EXPECT_TRUE(calls.families.empty()); } }