#include #include #include #include #include #include #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 value; }; } TEST_CASE("Explicit error unions preserve each computation's structured failure and transactional recovery", "[unit][utils][dependencies]") { using Failure = std::variant; int input = 2; int input_calls = 0; int physics_calls = 0; int output_calls = 0; const auto context = dependencies::make_dependency_context(dependencies::borrow(input)); const auto checked = dependencies::make_computation>( [&](int value) -> std::expected { ++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>( [&](double value) -> std::expected { ++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>( [&](double value) { ++output_calls; return 3.0 * value; } ); auto cache = dependencies::make_computation_cache(context, checked, physics, output); REQUIRE(cache.try_prepare(context)); CHECK(cache.get() == 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(&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(), std::logic_error); CHECK_THROWS_AS(cache.get(), std::logic_error); CHECK_THROWS_AS(cache.get(), 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() == 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(&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(), std::logic_error); CHECK_THROWS_AS(cache.get(), 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() == 6.0); CHECK(cache.get() == 6.5); CHECK(cache.get() == 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; int input = 7; int physics_calls = 0; const auto context = dependencies::make_dependency_context(dependencies::borrow(input)); const auto checked = dependencies::make_computation>( [](int value) -> std::expected { return value; } ); const auto physics = dependencies::make_computation>( [&](int value) -> std::expected { ++physics_calls; return std::unexpected(PhysicsFailure{.value = static_cast(value), .sample = 2, .message = "This downstream result is deliberately unavailable."}); } ); auto cache = dependencies::make_computation_cache(context, checked, physics); const auto requested = cache.try_prepare(context, dependencies::RequestedResults{}); REQUIRE(requested); CHECK(requested->computed_count() == 1); CHECK(requested->status() == dependencies::PreparationStatus::not_required); CHECK(physics_calls == 0); CHECK(cache.get() == 7); CHECK_FALSE(cache.contains()); const auto rejected = cache.try_prepare(context); REQUIRE_FALSE(rejected); CHECK(std::holds_alternative(rejected.error().error)); CHECK_FALSE(cache.ready()); REQUIRE(cache.try_prepare(context, dependencies::RequestedResults{})); CHECK(cache.get() == 7); CHECK(physics_calls == 1); } TEST_CASE("Explicit failure unions transfer move-only diagnostic payloads without copying", "[unit][utils][dependencies]") { using Failure = std::variant; int input = -4; const auto context = dependencies::make_dependency_context(dependencies::borrow(input)); const auto computation = dependencies::make_computation>( [](int value) -> std::expected { if (value < 0) { return std::unexpected(OwnedFailure{.value = std::make_unique(value)}); } return value; } ); auto cache = dependencies::make_computation_cache(context, computation); const auto rejected = cache.try_prepare(context); REQUIRE_FALSE(rejected); const OwnedFailure* failure = std::get_if(&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() == 4); CHECK(*failure->value == -4); }