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.
This commit is contained in:
2026-11-09 13:53:17 -05:00
parent 74e55a6859
commit cb20faeb3d
202 changed files with 35505 additions and 953 deletions

View File

@@ -0,0 +1,189 @@
#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);
}