From be35d00f49558c997dbf6faec824e06b2504dcf0 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Sun, 16 Aug 2026 11:33:08 +0900 Subject: [PATCH] feat(cpp-object-oriented-modular-refactoring): step 20 - analysis-hierarchy --- include/fesa/analysis/analysis.h | 30 +++++ .../fesa/analysis/linear_static_analysis.h | 78 ++++-------- src/fesa/analysis/linear_static_analysis.cpp | 53 +++++--- src/fesa/app/fesa_application.cpp | 7 +- .../essential_constraint_policy.cpp | 10 -- tests/CMakeLists.txt | 1 + .../analysis/linear_static_analysis_test.cpp | 116 ++++++++---------- tests/unit/analysis/analysis_test.cpp | 67 ++++++++++ 8 files changed, 214 insertions(+), 148 deletions(-) create mode 100644 include/fesa/analysis/analysis.h create mode 100644 tests/unit/analysis/analysis_test.cpp diff --git a/include/fesa/analysis/analysis.h b/include/fesa/analysis/analysis.h new file mode 100644 index 0000000..8a11b03 --- /dev/null +++ b/include/fesa/analysis/analysis.h @@ -0,0 +1,30 @@ +#ifndef FESA_ANALYSIS_ANALYSIS_H_ +#define FESA_ANALYSIS_ANALYSIS_H_ + +#include + +#include "fesa/core/status.h" + +namespace fesa { + +/// @brief Carries input and authoritative output paths for one analysis run. +struct AnalysisRequest { + std::filesystem::path input_path; + std::filesystem::path output_path; +}; + +/// @brief Defines the minimal execution contract shared by analysis procedures. +class Analysis { + public: + virtual ~Analysis() = default; + + /// @brief Executes one procedure for the supplied input and output paths. + /// @param request Input and authoritative output paths for this run. + /// @return The concrete procedure result without changing its failure + /// category. + virtual Status Run(const AnalysisRequest& request) = 0; +}; + +} // namespace fesa + +#endif // FESA_ANALYSIS_ANALYSIS_H_ diff --git a/include/fesa/analysis/linear_static_analysis.h b/include/fesa/analysis/linear_static_analysis.h index a915d06..8911bde 100644 --- a/include/fesa/analysis/linear_static_analysis.h +++ b/include/fesa/analysis/linear_static_analysis.h @@ -1,14 +1,14 @@ #ifndef FESA_ANALYSIS_LINEAR_STATIC_ANALYSIS_H_ #define FESA_ANALYSIS_LINEAR_STATIC_ANALYSIS_H_ -#include #include #include +#include "fesa/analysis/analysis.h" #include "fesa/analysis/analysis_model.h" #include "fesa/analysis/analysis_state.h" #include "fesa/constraints/essential_constraint_policy.h" -#include "fesa/core/status.h" +#include "fesa/elements/element.h" #include "fesa/fem/dof_manager.h" #include "fesa/math/sparse_matrix.h" #include "fesa/math/vector.h" @@ -20,41 +20,6 @@ class LinearSolver; class ParallelFor; class ResultsWriter; -/// @brief Carries input and authoritative output paths for one analysis run. -struct AnalysisRequest { - std::filesystem::path input_path; - std::filesystem::path output_path; -}; - -/// @brief Executes the current approved V0 analysis lifecycle. -class Analysis { - public: - virtual ~Analysis() = default; - - /// @brief Runs the eight current lifecycle stages in their approved order. - /// @param request Input and output paths for this run. - /// @return The first stage failure or successful result finalization. - Status Run(const AnalysisRequest& request); - - protected: - /// @brief Initializes owned input and Domain state for a run. - virtual Status Initialize(const AnalysisRequest& request) = 0; - /// @brief Builds the non-owning active-model view. - virtual Status BuildAnalysisModel() = 0; - /// @brief Builds stable DOF numbering and the sparse structural pattern. - virtual Status BuildDofMapAndSparsePattern() = 0; - /// @brief Assembles full stiffness and stable constraint partitions. - virtual Status AssembleAndPartitionStiffness() = 0; - /// @brief Factorizes Kff before any load assembly. - virtual Status Factorize() = 0; - /// @brief Assembles loads and forms Ff-Kfc*dc without solving. - virtual Status AssembleLoadsAndEffectiveRhs() = 0; - /// @brief Substitutes the retained factorization and reconstructs full d. - virtual Status SubstituteAndReconstruct() = 0; - /// @brief Recovers a complete candidate and writes authoritative results. - virtual Status RecoverAndWriteResults() = 0; -}; - /// @brief Orchestrates the single-step linear-static procedure. /// @note Factorization, substitution, recovery, and writing remain separately /// observable through injected backend boundaries. @@ -66,31 +31,36 @@ class LinearStaticAnalysis final : public Analysis { LinearSolver& linear_solver, ResultsWriter& results_writer); - protected: - /// @copydoc Analysis::Initialize - Status Initialize(const AnalysisRequest& request) override; - /// @copydoc Analysis::BuildAnalysisModel - Status BuildAnalysisModel() override; - /// @copydoc Analysis::BuildDofMapAndSparsePattern - Status BuildDofMapAndSparsePattern() override; - /// @copydoc Analysis::AssembleAndPartitionStiffness - Status AssembleAndPartitionStiffness() override; - /// @copydoc Analysis::Factorize - Status Factorize() override; - /// @copydoc Analysis::AssembleLoadsAndEffectiveRhs - Status AssembleLoadsAndEffectiveRhs() override; - /// @copydoc Analysis::SubstituteAndReconstruct - Status SubstituteAndReconstruct() override; - /// @copydoc Analysis::RecoverAndWriteResults - Status RecoverAndWriteResults() override; + /// @brief Runs the approved eight-stage linear-static lifecycle. + /// @return The first stage failure or successful result finalization. + Status Run(const AnalysisRequest& request) override; private: + /// @brief Initializes owned input and Domain state for a run candidate. + Status InitializeCandidate(const AnalysisRequest& request); + /// @brief Builds the non-owning active-model view. + Status BuildAnalysisModel(); + /// @brief Creates runtime elements, stable DOFs, and the sparse pattern. + Status BuildDofMapAndSparsePattern(); + /// @brief Assembles full stiffness and stable constraint partitions. + Status AssembleAndPartitionStiffness(); + /// @brief Factorizes Kff before any load assembly. + Status FactorizeFreeSystem(); + /// @brief Assembles loads and forms Ff-Kfc*dc without solving. + Status AssembleLoadsAndEffectiveRhs(); + /// @brief Substitutes the retained factorization and reconstructs full d. + Status SubstituteAndReconstruct(); + /// @brief Recovers a complete candidate before writing authoritative output. + Status RecoverAndWrite(); + const ParallelFor& parallel_for_; LinearSolver& linear_solver_; ResultsWriter& results_writer_; AnalysisRequest request_; std::unique_ptr domain_; std::unique_ptr model_; + std::vector> elements_; + ElementView element_view_; std::unique_ptr dofs_; std::unique_ptr state_; std::unique_ptr full_stiffness_; diff --git a/src/fesa/analysis/linear_static_analysis.cpp b/src/fesa/analysis/linear_static_analysis.cpp index 6d0c0c1..80a6524 100644 --- a/src/fesa/analysis/linear_static_analysis.cpp +++ b/src/fesa/analysis/linear_static_analysis.cpp @@ -5,6 +5,7 @@ #include "fesa/assembly/load_assembler.h" #include "fesa/assembly/parallel_for.h" #include "fesa/assembly/sparse_assembler.h" +#include "fesa/elements/element_factory.h" #include "fesa/io/abaqus/domain_mapper.h" #include "fesa/io/abaqus/input_reader.h" #include "fesa/results/result_recovery.h" @@ -13,8 +14,8 @@ namespace fesa { -Status Analysis::Run(const AnalysisRequest& request) { - Status status = Initialize(request); +Status LinearStaticAnalysis::Run(const AnalysisRequest& request) { + Status status = InitializeCandidate(request); if (!status.IsOk()) { return status; } @@ -30,7 +31,7 @@ Status Analysis::Run(const AnalysisRequest& request) { if (!status.IsOk()) { return status; } - status = Factorize(); + status = FactorizeFreeSystem(); if (!status.IsOk()) { return status; } @@ -42,7 +43,7 @@ Status Analysis::Run(const AnalysisRequest& request) { if (!status.IsOk()) { return status; } - return RecoverAndWriteResults(); + return RecoverAndWrite(); } LinearStaticAnalysis::LinearStaticAnalysis(const ParallelFor& parallel_for, @@ -52,7 +53,8 @@ LinearStaticAnalysis::LinearStaticAnalysis(const ParallelFor& parallel_for, linear_solver_{linear_solver}, results_writer_{results_writer} {} -Status LinearStaticAnalysis::Initialize(const AnalysisRequest& request) { +Status LinearStaticAnalysis::InitializeCandidate( + const AnalysisRequest& request) { // Clear dependent objects in reverse ownership order so a reused analysis // never exposes a view into a Domain from an earlier run. effective_rhs_.reset(); @@ -60,6 +62,8 @@ Status LinearStaticAnalysis::Initialize(const AnalysisRequest& request) { full_stiffness_.reset(); state_.reset(); dofs_.reset(); + element_view_.clear(); + elements_.clear(); model_.reset(); domain_.reset(); diagnostics_.clear(); @@ -90,11 +94,29 @@ Status LinearStaticAnalysis::BuildAnalysisModel() { } Status LinearStaticAnalysis::BuildDofMapAndSparsePattern() { - auto dofs = DofManager::Create(*model_); - if (!dofs.HasValue()) { - return dofs.GetStatus(); + std::vector> elements; + ElementView element_view; + elements.reserve(model_->ActiveElements().size()); + element_view.reserve(model_->ActiveElements().size()); + const ElementFactory factory; + for (const EntityIndex element_index : model_->ActiveElements()) { + auto element = factory.Create(domain_->Elements()[element_index], *domain_); + if (!element.HasValue()) { + return element.GetStatus(); + } + element_view.emplace_back(*element.Value()); + elements.push_back(std::move(element.Value())); } - dofs_ = std::make_unique(std::move(dofs.Value())); + + DofManager dofs; + const Status dof_status = dofs.Build(*model_, element_view); + if (!dof_status.IsOk()) { + return dof_status; + } + + elements_ = std::move(elements); + element_view_ = std::move(element_view); + dofs_ = std::make_unique(std::move(dofs)); state_ = std::make_unique( AnalysisState::Create(*dofs_, {"Step-1", 0U})); return Status::Ok(); @@ -102,7 +124,7 @@ Status LinearStaticAnalysis::BuildDofMapAndSparsePattern() { Status LinearStaticAnalysis::AssembleAndPartitionStiffness() { auto stiffness = - SparseAssembler::AssembleStiffness(*model_, *dofs_, parallel_for_); + SparseAssembler::Assemble(element_view_, *dofs_, parallel_for_); if (!stiffness.HasValue()) { return stiffness.GetStatus(); } @@ -119,8 +141,8 @@ Status LinearStaticAnalysis::AssembleAndPartitionStiffness() { return Status::Ok(); } -Status LinearStaticAnalysis::Factorize() { - // This call intentionally precedes all load assembly in Analysis::Run. +Status LinearStaticAnalysis::FactorizeFreeSystem() { + // This call intentionally precedes all load assembly in Run. return linear_solver_.Factorize(partitioned_stiffness_->k_ff); } @@ -154,9 +176,10 @@ Status LinearStaticAnalysis::SubstituteAndReconstruct() { return Status::Ok(); } -Status LinearStaticAnalysis::RecoverAndWriteResults() { - const Status recovery_status = - ResultRecovery::Recover(*model_, *dofs_, *full_stiffness_, *state_); +Status LinearStaticAnalysis::RecoverAndWrite() { + const Status recovery_status = ResultRecovery::Recover( + *model_, element_view_, *dofs_, *full_stiffness_, state_->Displacement(), + state_->ExternalForce(), *state_); if (!recovery_status.IsOk()) { return recovery_status; } diff --git a/src/fesa/app/fesa_application.cpp b/src/fesa/app/fesa_application.cpp index dd6614a..72a3950 100644 --- a/src/fesa/app/fesa_application.cpp +++ b/src/fesa/app/fesa_application.cpp @@ -2,9 +2,11 @@ #include #include +#include #include #include +#include "fesa/analysis/analysis.h" #include "fesa/analysis/linear_static_analysis.h" #include "fesa/assembly/parallel_for.h" #include "fesa/core/diagnostic.h" @@ -91,8 +93,9 @@ int FesaApplication::Run(const std::vector& arguments) { TbbParallelFor parallel_for; MklPardisoSolver linear_solver; Hdf5ResultsWriter results_writer; - LinearStaticAnalysis analysis{parallel_for, linear_solver, results_writer}; - const Status status = analysis.Run(request); + std::unique_ptr analysis = std::make_unique( + parallel_for, linear_solver, results_writer); + const Status status = analysis->Run(request); if (status.IsOk()) { return kSuccessExitCode; } diff --git a/src/fesa/constraints/essential_constraint_policy.cpp b/src/fesa/constraints/essential_constraint_policy.cpp index cf3487d..5903d0d 100644 --- a/src/fesa/constraints/essential_constraint_policy.cpp +++ b/src/fesa/constraints/essential_constraint_policy.cpp @@ -57,13 +57,6 @@ Result ExtractBlock(const SparseMatrix& full, std::move(contributions), pattern); } -void RequireDofOrder(const DofManager& dof_manager) { - if (!dof_manager.ValidateInvariants().IsOk()) { - throw std::invalid_argument{ - "DofManager constraint dimensions or order are invalid."}; - } -} - } // namespace Result EssentialConstraintPolicy::Partition( @@ -114,7 +107,6 @@ Result EssentialConstraintPolicy::Partition( Vector EssentialConstraintPolicy::GatherFree( const Vector& full_values, const DofManager& dof_manager) const { - RequireDofOrder(dof_manager); if (full_values.Size() != dof_manager.FullDofCount()) { throw std::invalid_argument{ "Full vector size must match the DofManager full dimension."}; @@ -129,7 +121,6 @@ Vector EssentialConstraintPolicy::GatherFree( Vector EssentialConstraintPolicy::GatherConstrained( const Vector& full_values, const DofManager& dof_manager) const { - RequireDofOrder(dof_manager); if (full_values.Size() != dof_manager.FullDofCount()) { throw std::invalid_argument{ "Full vector size must match the DofManager full dimension."}; @@ -145,7 +136,6 @@ Vector EssentialConstraintPolicy::GatherConstrained( Vector EssentialConstraintPolicy::ReconstructFull( const Vector& free_values, const Vector& constrained_values, const DofManager& dof_manager) const { - RequireDofOrder(dof_manager); if (free_values.Size() != dof_manager.FreeDofCount() || constrained_values.Size() != dof_manager.ConstrainedDofCount()) { throw std::invalid_argument{ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4ac90cd..4a56e13 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,6 +3,7 @@ include(GoogleTest) add_executable( fesa_unit_tests unit/build_info_test.cpp + unit/analysis/analysis_test.cpp unit/analysis/analysis_model_test.cpp unit/analysis/analysis_state_test.cpp unit/assembly/parallel_for_test.cpp diff --git a/tests/integration/analysis/linear_static_analysis_test.cpp b/tests/integration/analysis/linear_static_analysis_test.cpp index f593601..a738210 100644 --- a/tests/integration/analysis/linear_static_analysis_test.cpp +++ b/tests/integration/analysis/linear_static_analysis_test.cpp @@ -14,6 +14,7 @@ #include #include +#include "fesa/analysis/analysis.h" #include "fesa/assembly/parallel_for.h" #include "fesa/results/results_writer.h" #include "fesa/solvers/linear/mkl_pardiso_solver.h" @@ -152,45 +153,6 @@ N4, 1, 6 )inp"); } -// The pure Template Method spy makes the eight public lifecycle hooks -// observable without coupling the ordering assertion to any solver backend. -class SpyAnalysis final : public fesa::Analysis { - public: - const std::vector& Events() const noexcept { return events_; } - - protected: - fesa::Status Initialize(const fesa::AnalysisRequest&) override { - return Record("initialize"); - } - fesa::Status BuildAnalysisModel() override { - return Record("build-analysis-model"); - } - fesa::Status BuildDofMapAndSparsePattern() override { - return Record("build-dof-map-and-sparse-pattern"); - } - fesa::Status AssembleAndPartitionStiffness() override { - return Record("assemble-and-partition-stiffness"); - } - fesa::Status Factorize() override { return Record("factorize"); } - fesa::Status AssembleLoadsAndEffectiveRhs() override { - return Record("assemble-loads-and-effective-rhs"); - } - fesa::Status SubstituteAndReconstruct() override { - return Record("substitute-and-reconstruct"); - } - fesa::Status RecoverAndWriteResults() override { - return Record("recover-and-write-results"); - } - - private: - fesa::Status Record(const char* event) { - events_.emplace_back(event); - return fesa::Status::Ok(); - } - - std::vector events_; -}; - // The solver spy records only the adapter-boundary operations. In particular, // solve() cannot conceal a second factorization call. class SpyLinearSolver final : public fesa::LinearSolver { @@ -342,17 +304,6 @@ class CapturingResultsWriter final : public fesa::ResultsWriter { } // namespace TEST(LinearStaticCli, FactorizesBeforeLoadAndSolvesWithoutRefactorization) { - SpyAnalysis lifecycle; - const fesa::AnalysisRequest empty_request{}; - ASSERT_TRUE(lifecycle.Run(empty_request).IsOk()); - EXPECT_EQ(lifecycle.Events(), - (std::vector{ - "initialize", "build-analysis-model", - "build-dof-map-and-sparse-pattern", - "assemble-and-partition-stiffness", "factorize", - "assemble-loads-and-effective-rhs", - "substitute-and-reconstruct", "recover-and-write-results"})); - TempDirectory directory{"order"}; const auto input = directory.Path() / "order.inp"; const auto output = directory.Path() / "results.h5"; @@ -362,9 +313,10 @@ TEST(LinearStaticCli, FactorizesBeforeLoadAndSolvesWithoutRefactorization) { fesa::SerialParallelFor serial; SpyLinearSolver solver{adapter_events}; SpyResultsWriter writer{adapter_events}; - fesa::LinearStaticAnalysis analysis{serial, solver, writer}; + std::unique_ptr analysis = + std::make_unique(serial, solver, writer); - ASSERT_TRUE(analysis.Run({input, output}).IsOk()); + ASSERT_TRUE(analysis->Run({input, output}).IsOk()); EXPECT_EQ(solver.FactorizeCalls(), 1); EXPECT_EQ(solver.SolveCalls(), 1); EXPECT_EQ(writer.WriteCalls(), 1); @@ -373,6 +325,30 @@ TEST(LinearStaticCli, FactorizesBeforeLoadAndSolvesWithoutRefactorization) { "writer-write"})); } +TEST(LinearStaticCli, FactorizesBeforeRejectingAnInvalidLoad) { + TempDirectory directory{"factorize-before-load"}; + const auto input = directory.Path() / "invalid-load-shell.inp"; + const auto output = directory.Path() / "must-not-exist.h5"; + WriteText(input, ShellDeck("*Boundary\nAll, 1, 6\n", "*Cload\nN1, 6, 1.0\n")); + + std::vector adapter_events; + fesa::SerialParallelFor serial; + SpyLinearSolver solver{adapter_events}; + SpyResultsWriter writer{adapter_events}; + std::unique_ptr analysis = + std::make_unique(serial, solver, writer); + + const fesa::Status status = analysis->Run({input, output}); + ASSERT_FALSE(status.IsOk()); + EXPECT_EQ(status.Category(), fesa::FailureCategory::kModel); + ASSERT_EQ(status.Diagnostics().size(), 1U); + EXPECT_EQ(status.Diagnostics()[0U].code, "unsupported-drilling-load"); + EXPECT_EQ(solver.FactorizeCalls(), 1); + EXPECT_EQ(solver.SolveCalls(), 0); + EXPECT_EQ(writer.WriteCalls(), 0); + EXPECT_EQ(adapter_events, (std::vector{"solver-factorize"})); +} + TEST(LinearStaticCli, RealPipelineHandlesAnalyticalAndNonzeroPrescription) { TempDirectory directory{"analytical"}; const auto input = directory.Path() / "prescribed-axial.inp"; @@ -382,9 +358,10 @@ TEST(LinearStaticCli, RealPipelineHandlesAnalyticalAndNonzeroPrescription) { fesa::SerialParallelFor serial; fesa::MklPardisoSolver solver; CapturingResultsWriter writer; - fesa::LinearStaticAnalysis analysis{serial, solver, writer}; + std::unique_ptr analysis = + std::make_unique(serial, solver, writer); - const auto status = analysis.Run({input, output}); + const auto status = analysis->Run({input, output}); ASSERT_TRUE(status.IsOk()); EXPECT_EQ(writer.OutputPath(), output); EXPECT_EQ(writer.NodeCount(), 2U); @@ -419,9 +396,10 @@ TEST(Mitc4ShellCli, UsesExistingLifecycleAndExactlyOneFactorization) { fesa::SerialParallelFor serial; SpyLinearSolver solver{adapter_events}; SpyResultsWriter writer{adapter_events}; - fesa::LinearStaticAnalysis analysis{serial, solver, writer}; + std::unique_ptr analysis = + std::make_unique(serial, solver, writer); - ASSERT_TRUE(analysis.Run({input, output}).IsOk()); + ASSERT_TRUE(analysis->Run({input, output}).IsOk()); EXPECT_EQ(solver.FactorizeCalls(), 1); EXPECT_EQ(solver.SolveCalls(), 1); EXPECT_EQ(writer.WriteCalls(), 1); @@ -440,9 +418,10 @@ TEST(Mitc4ShellCli, AppliesKfcForNonzeroPrescribedDisplacement) { fesa::SerialParallelFor serial; RecordingMklSolver solver; CapturingResultsWriter writer; - fesa::LinearStaticAnalysis analysis{serial, solver, writer}; + std::unique_ptr analysis = + std::make_unique(serial, solver, writer); - const auto status = analysis.Run({input, output}); + const auto status = analysis->Run({input, output}); for (const auto& diagnostic : status.Diagnostics()) { EXPECT_TRUE(status.IsOk()) << diagnostic.code << ": " << diagnostic.message; } @@ -479,19 +458,21 @@ TEST(Mitc4ShellCli, RejectsSingularAndAcceptsZeroByZeroFreeSystem) { fesa::MklPardisoSolver singular_solver; std::vector singular_events; SpyResultsWriter singular_writer{singular_events}; - fesa::LinearStaticAnalysis singular_analysis{serial, singular_solver, - singular_writer}; - const auto singular = - singular_analysis.Run({singular_input, directory.Path() / "singular.h5"}); + std::unique_ptr singular_analysis = + std::make_unique(serial, singular_solver, + singular_writer); + const auto singular = singular_analysis->Run( + {singular_input, directory.Path() / "singular.h5"}); ASSERT_FALSE(singular.IsOk()); EXPECT_EQ(singular.Category(), fesa::FailureCategory::kSolver); EXPECT_EQ(singular_writer.WriteCalls(), 0); RecordingMklSolver constrained_solver; CapturingResultsWriter constrained_writer; - fesa::LinearStaticAnalysis constrained_analysis{serial, constrained_solver, - constrained_writer}; - const auto constrained = constrained_analysis.Run( + std::unique_ptr constrained_analysis = + std::make_unique(serial, constrained_solver, + constrained_writer); + const auto constrained = constrained_analysis->Run( {constrained_input, directory.Path() / "constrained.h5"}); ASSERT_TRUE(constrained.IsOk()); EXPECT_EQ(constrained_solver.FactorizeCalls(), 1); @@ -511,9 +492,10 @@ TEST(Mitc4ShellCli, DoesNotWriteAnInvalidRecoveryCandidate) { NonfiniteLinearSolver solver; std::vector adapter_events; SpyResultsWriter writer{adapter_events}; - fesa::LinearStaticAnalysis analysis{serial, solver, writer}; + std::unique_ptr analysis = + std::make_unique(serial, solver, writer); const auto status = - analysis.Run({input, directory.Path() / "must-not-exist.h5"}); + analysis->Run({input, directory.Path() / "must-not-exist.h5"}); ASSERT_FALSE(status.IsOk()); EXPECT_EQ(status.Category(), fesa::FailureCategory::kModel); diff --git a/tests/unit/analysis/analysis_test.cpp b/tests/unit/analysis/analysis_test.cpp new file mode 100644 index 0000000..72f5bf8 --- /dev/null +++ b/tests/unit/analysis/analysis_test.cpp @@ -0,0 +1,67 @@ +#include "fesa/analysis/analysis.h" + +#include + +#include +#include +#include +#include + +namespace { + +using RunSignature = + fesa::Status (fesa::Analysis::*)(const fesa::AnalysisRequest&); + +static_assert(std::has_virtual_destructor_v); +static_assert(std::is_abstract_v); +static_assert(std::is_same_v); + +class StatusAnalysis final : public fesa::Analysis { + public: + StatusAnalysis(bool& destroyed, fesa::Status result) + : destroyed_{destroyed}, result_{std::move(result)} {} + + ~StatusAnalysis() override { destroyed_ = true; } + + fesa::Status Run(const fesa::AnalysisRequest& request) override { + request_ = request; + return result_; + } + + const fesa::AnalysisRequest& Request() const noexcept { return request_; } + + private: + bool& destroyed_; + fesa::Status result_; + fesa::AnalysisRequest request_; +}; + +} // namespace + +TEST(Analysis, PropagatesStatusAndDestroysThroughMinimalAnalysis) { + bool destroyed = false; + const fesa::AnalysisRequest request{"model.inp", "results.h5"}; + const fesa::Status failure = fesa::Status::Failure( + fesa::FailureCategory::kSolver, + {{fesa::Severity::kError, + "analysis-test-failure", + {}, + "ANALYSIS", + "", + "The concrete procedure failure must propagate unchanged."}}); + + auto concrete = std::make_unique(destroyed, failure); + StatusAnalysis* const concrete_observer = concrete.get(); + std::unique_ptr analysis = std::move(concrete); + + const fesa::Status status = analysis->Run(request); + ASSERT_FALSE(status.IsOk()); + EXPECT_EQ(status.Category(), fesa::FailureCategory::kSolver); + ASSERT_EQ(status.Diagnostics().size(), 1U); + EXPECT_EQ(status.Diagnostics()[0U].code, "analysis-test-failure"); + EXPECT_EQ(concrete_observer->Request().input_path, request.input_path); + EXPECT_EQ(concrete_observer->Request().output_path, request.output_path); + + analysis.reset(); + EXPECT_TRUE(destroyed); +}