Files
MeanField_SERiF/tests/utils/dependencies/mixed_errors.cpp
Emily Boudreaux cb20faeb3d test(tests): added test suite
The test suite is extensive. Note that LLMs were used to aid in the development of the test suite. All LLM generated code was manually reviewed.
2026-11-09 13:53:17 -05:00

190 lines
7.6 KiB
C++

#include <expected>
#include <memory>
#include <stdexcept>
#include <string>
#include <variant>
#include <catch2/catch_test_macros.hpp>
#include "serif/utils/dependencies/computation_cache.hpp"
namespace {
namespace dependencies = serif::utils::dependencies;
struct Source {};
struct CheckedInput {};
struct PhysicsResult {};
struct Output {};
/** @brief Preserve the original invalid argument without converting all failures into text. */
struct InputFailure {
int value;
std::string message;
};
/** @brief Preserve downstream numerical diagnostics independently of input validation. */
struct PhysicsFailure {
double value;
int sample;
std::string message;
};
/** @brief A cache error may own diagnostic data that cannot be copied. */
struct OwnedFailure {
std::unique_ptr<int> value;
};
}
TEST_CASE("Explicit error unions preserve each computation's structured failure and transactional recovery", "[unit][utils][dependencies]") {
using Failure = std::variant<InputFailure, PhysicsFailure>;
int input = 2;
int input_calls = 0;
int physics_calls = 0;
int output_calls = 0;
const auto context = dependencies::make_dependency_context(dependencies::borrow<Source>(input));
const auto checked = dependencies::make_computation<CheckedInput, dependencies::InputList<Source>>(
[&](int value) -> std::expected<double, InputFailure> {
++input_calls;
if (value < 0) {
return std::unexpected(InputFailure{.value = value, .message = "The input must be nonnegative."});
}
return 2.0 * value;
}
);
const auto physics = dependencies::make_computation<PhysicsResult, dependencies::InputList<CheckedInput>>(
[&](double value) -> std::expected<double, PhysicsFailure> {
++physics_calls;
if (value > 10.0) {
return std::unexpected(PhysicsFailure{.value = value, .sample = 3, .message = "The physical state exceeds the admissible range."});
}
return value + 0.5;
}
);
const auto output = dependencies::make_computation<Output, dependencies::InputList<PhysicsResult>>(
[&](double value) {
++output_calls;
return 3.0 * value;
}
);
auto cache = dependencies::make_computation_cache<Failure>(context, checked, physics, output);
REQUIRE(cache.try_prepare(context));
CHECK(cache.get<Output>() == 13.5);
CHECK(cache.preparation_count() == 1);
input = -1;
const auto rejected_input = cache.try_prepare(context);
REQUIRE_FALSE(rejected_input);
CHECK(rejected_input.error().computation_index == 0);
const InputFailure* input_failure = std::get_if<InputFailure>(&rejected_input.error().error);
REQUIRE(input_failure != nullptr);
CHECK(input_failure->value == -1);
CHECK(input_failure->message == "The input must be nonnegative.");
CHECK_FALSE(cache.ready());
CHECK(cache.preparation_count() == 1);
CHECK(input_calls == 2);
CHECK(physics_calls == 1);
CHECK(output_calls == 1);
CHECK_THROWS_AS(cache.get<CheckedInput>(), std::logic_error);
CHECK_THROWS_AS(cache.get<PhysicsResult>(), std::logic_error);
CHECK_THROWS_AS(cache.get<Output>(), std::logic_error);
input = 3;
const auto recovered_input = cache.try_prepare(context);
REQUIRE(recovered_input);
CHECK(recovered_input->computed_count() == 3);
CHECK(cache.get<Output>() == 19.5);
CHECK(cache.preparation_count() == 2);
input = 6;
const auto rejected_physics = cache.try_prepare(context);
REQUIRE_FALSE(rejected_physics);
CHECK(rejected_physics.error().computation_index == 1);
const PhysicsFailure* physics_failure = std::get_if<PhysicsFailure>(&rejected_physics.error().error);
REQUIRE(physics_failure != nullptr);
CHECK(physics_failure->value == 12.0);
CHECK(physics_failure->sample == 3);
CHECK(physics_failure->message == "The physical state exceeds the admissible range.");
CHECK_FALSE(cache.ready());
CHECK(cache.preparation_count() == 2);
CHECK(input_calls == 4);
CHECK(physics_calls == 3);
CHECK(output_calls == 2);
CHECK_THROWS_AS(cache.get<CheckedInput>(), std::logic_error);
CHECK_THROWS_AS(cache.get<Output>(), std::logic_error);
// The rejected candidate must not replace the last accepted source or intermediate result.
input = 3;
const auto recovered_physics = cache.try_prepare(context);
REQUIRE(recovered_physics);
CHECK(recovered_physics->computed_count() == 0);
CHECK(cache.get<CheckedInput>() == 6.0);
CHECK(cache.get<PhysicsResult>() == 6.5);
CHECK(cache.get<Output>() == 19.5);
CHECK(input_calls == 4);
CHECK(physics_calls == 3);
CHECK(output_calls == 2);
}
TEST_CASE("Requested results avoid unrelated errors in a graph with an explicit failure union", "[unit][utils][dependencies]") {
using Failure = std::variant<InputFailure, PhysicsFailure>;
int input = 7;
int physics_calls = 0;
const auto context = dependencies::make_dependency_context(dependencies::borrow<Source>(input));
const auto checked = dependencies::make_computation<CheckedInput, dependencies::InputList<Source>>(
[](int value) -> std::expected<int, InputFailure> {
return value;
}
);
const auto physics = dependencies::make_computation<PhysicsResult, dependencies::InputList<CheckedInput>>(
[&](int value) -> std::expected<int, PhysicsFailure> {
++physics_calls;
return std::unexpected(PhysicsFailure{.value = static_cast<double>(value), .sample = 2, .message = "This downstream result is deliberately unavailable."});
}
);
auto cache = dependencies::make_computation_cache<Failure>(context, checked, physics);
const auto requested = cache.try_prepare(context, dependencies::RequestedResults<CheckedInput>{});
REQUIRE(requested);
CHECK(requested->computed_count() == 1);
CHECK(requested->status<PhysicsResult>() == dependencies::PreparationStatus::not_required);
CHECK(physics_calls == 0);
CHECK(cache.get<CheckedInput>() == 7);
CHECK_FALSE(cache.contains<PhysicsResult>());
const auto rejected = cache.try_prepare(context);
REQUIRE_FALSE(rejected);
CHECK(std::holds_alternative<PhysicsFailure>(rejected.error().error));
CHECK_FALSE(cache.ready());
REQUIRE(cache.try_prepare(context, dependencies::RequestedResults<CheckedInput>{}));
CHECK(cache.get<CheckedInput>() == 7);
CHECK(physics_calls == 1);
}
TEST_CASE("Explicit failure unions transfer move-only diagnostic payloads without copying", "[unit][utils][dependencies]") {
using Failure = std::variant<InputFailure, OwnedFailure>;
int input = -4;
const auto context = dependencies::make_dependency_context(dependencies::borrow<Source>(input));
const auto computation = dependencies::make_computation<Output, dependencies::InputList<Source>>(
[](int value) -> std::expected<int, OwnedFailure> {
if (value < 0) {
return std::unexpected(OwnedFailure{.value = std::make_unique<int>(value)});
}
return value;
}
);
auto cache = dependencies::make_computation_cache<Failure>(context, computation);
const auto rejected = cache.try_prepare(context);
REQUIRE_FALSE(rejected);
const OwnedFailure* failure = std::get_if<OwnedFailure>(&rejected.error().error);
REQUIRE(failure != nullptr);
REQUIRE(failure->value != nullptr);
CHECK(*failure->value == -4);
CHECK_FALSE(cache.ready());
input = 4;
REQUIRE(cache.try_prepare(context));
CHECK(cache.get<Output>() == 4);
CHECK(*failure->value == -4);
}