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.
190 lines
7.6 KiB
C++
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);
|
|
}
|