feat(cpp-object-oriented-modular-refactoring): step 20 - analysis-hierarchy
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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<std::string>& 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<std::string> 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<std::string>{
|
||||
"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<fesa::Analysis> analysis =
|
||||
std::make_unique<fesa::LinearStaticAnalysis>(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<std::string> adapter_events;
|
||||
fesa::SerialParallelFor serial;
|
||||
SpyLinearSolver solver{adapter_events};
|
||||
SpyResultsWriter writer{adapter_events};
|
||||
std::unique_ptr<fesa::Analysis> analysis =
|
||||
std::make_unique<fesa::LinearStaticAnalysis>(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<std::string>{"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<fesa::Analysis> analysis =
|
||||
std::make_unique<fesa::LinearStaticAnalysis>(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<fesa::Analysis> analysis =
|
||||
std::make_unique<fesa::LinearStaticAnalysis>(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<fesa::Analysis> analysis =
|
||||
std::make_unique<fesa::LinearStaticAnalysis>(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<std::string> 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<fesa::Analysis> singular_analysis =
|
||||
std::make_unique<fesa::LinearStaticAnalysis>(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<fesa::Analysis> constrained_analysis =
|
||||
std::make_unique<fesa::LinearStaticAnalysis>(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<std::string> adapter_events;
|
||||
SpyResultsWriter writer{adapter_events};
|
||||
fesa::LinearStaticAnalysis analysis{serial, solver, writer};
|
||||
std::unique_ptr<fesa::Analysis> analysis =
|
||||
std::make_unique<fesa::LinearStaticAnalysis>(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);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#include "fesa/analysis/analysis.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
using RunSignature =
|
||||
fesa::Status (fesa::Analysis::*)(const fesa::AnalysisRequest&);
|
||||
|
||||
static_assert(std::has_virtual_destructor_v<fesa::Analysis>);
|
||||
static_assert(std::is_abstract_v<fesa::Analysis>);
|
||||
static_assert(std::is_same_v<decltype(&fesa::Analysis::Run), RunSignature>);
|
||||
|
||||
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<StatusAnalysis>(destroyed, failure);
|
||||
StatusAnalysis* const concrete_observer = concrete.get();
|
||||
std::unique_ptr<fesa::Analysis> 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);
|
||||
}
|
||||
Reference in New Issue
Block a user