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,193 @@
#include "serif/utils/error/errors.hpp"
#include "serif/utils/dependencies/dependencies.hpp"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <expected>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>
namespace {
namespace dependencies = serif::utils::dependencies;
struct Source {};
struct Intermediate {};
struct Result {};
struct ThrowingSnapshot {
/// Simulate a policy failing while it tries to capture replacement state.
int capture(const int& value) const {
if (value < 0) {
serif::utils::errors::serif_error<std::runtime_error, serif::utils::errors::SERiFErrorCode::INVALID_ARGUMENT>("source snapshot unavailable");
}
return value;
}
/// Compare only successfully captured states.
bool matches(const int& captured, const int& value) const { return captured == value; }
};
}
TEST_CASE("Rejected dependency preparation publishes neither candidate results nor snapshots", "[unit][utils][dependencies]") {
int source = 2;
int intermediate_calls = 0;
int result_calls = 0;
auto context = dependencies::make_dependency_context(dependencies::borrow<Source>(source));
auto graph = dependencies::make_computation_graph(
dependencies::make_computation<Result, dependencies::InputList<Intermediate>>([&result_calls](int intermediate) -> std::expected<int, std::string> {
++result_calls;
if (intermediate < 0) { return std::unexpected("input must be nonnegative"); }
return intermediate + 1;
}),
dependencies::make_computation<Intermediate, dependencies::InputList<Source>>([&intermediate_calls](int input) { ++intermediate_calls; return 2 * input; })
);
auto cache = dependencies::make_computation_cache(std::move(graph), context);
REQUIRE(cache.try_prepare(context));
CHECK(cache.get<Result>() == 5);
source = -3;
auto rejected = cache.try_prepare(context);
REQUIRE_FALSE(rejected);
CHECK(rejected.error().computation_index == 0);
CHECK(rejected.error().error == "input must be nonnegative");
CHECK_FALSE(cache.ready());
CHECK_FALSE(cache.contains<Intermediate>());
CHECK_THROWS_AS(cache.get<Result>(), std::logic_error);
CHECK(cache.report().computed_count() == 0);
CHECK(cache.report().reused_count() == 0);
CHECK(intermediate_calls == 2);
CHECK(result_calls == 2);
// Retrying rejected inputs must rebuild the prerequisite: its candidate was never committed.
REQUIRE_FALSE(cache.try_prepare(context));
CHECK(intermediate_calls == 3);
CHECK(result_calls == 3);
// The last accepted cache is still usable when the source returns to its accepted state.
source = 2;
const auto recovered = cache.try_prepare(context);
REQUIRE(recovered);
CHECK(recovered->reused_count() == 2);
CHECK(cache.get<Result>() == 5);
CHECK(intermediate_calls == 3);
CHECK(result_calls == 3);
source = 4;
REQUIRE(cache.try_prepare(context));
CHECK(cache.get<Result>() == 9);
CHECK(intermediate_calls == 4);
CHECK(result_calls == 4);
}
TEST_CASE("Dependency computations can return move-only typed errors", "[unit][utils][dependencies]") {
using Error = std::unique_ptr<int>;
int source = -1;
auto context = dependencies::make_dependency_context(dependencies::borrow<Source>(source));
auto graph = dependencies::make_computation_graph(dependencies::make_computation<Result, dependencies::InputList<Source>>([](int value) -> std::expected<int, Error> {
if (value < 0) { return std::unexpected(std::make_unique<int>(value)); }
return value;
}));
auto cache = dependencies::make_computation_cache<Error>(std::move(graph), context);
const auto rejected = cache.try_prepare(context);
REQUIRE_FALSE(rejected);
CHECK(*rejected.error().error == -1);
source = 3;
REQUIRE(cache.try_prepare(context));
CHECK(cache.get<Result>() == 3);
}
TEST_CASE("Exceptions leave dependency results unavailable without replacing the accepted cache", "[unit][utils][dependencies]") {
int source = 2;
int calls = 0;
auto context = dependencies::make_dependency_context(dependencies::borrow<Source>(source, ThrowingSnapshot{}));
auto graph = dependencies::make_computation_graph(dependencies::make_computation<Result, dependencies::InputList<Source>>([&calls](int value) {
++calls;
if (value == 3) {
serif::utils::errors::serif_error<std::runtime_error, serif::utils::errors::SERiFErrorCode::INVALID_ARGUMENT>("calculation failed");
}
return value;
}));
auto cache = dependencies::make_computation_cache(std::move(graph), context);
REQUIRE(cache.try_prepare(context));
CHECK(calls == 1);
SECTION("A source policy throws before evaluation") {
source = -1;
CHECK_THROWS_WITH(cache.try_prepare(context), Catch::Matchers::ContainsSubstring("source snapshot unavailable"));
CHECK(calls == 1);
}
SECTION("The calculation throws after snapshot capture") {
source = 3;
CHECK_THROWS_WITH(cache.try_prepare(context), Catch::Matchers::ContainsSubstring("calculation failed"));
CHECK(calls == 2);
}
CHECK_FALSE(cache.ready());
CHECK_THROWS_AS(cache.get<Result>(), std::logic_error);
source = 2;
const auto recovered = cache.try_prepare(context);
REQUIRE(recovered);
CHECK(recovered->reused_count() == 1);
CHECK(cache.get<Result>() == 2);
}
TEST_CASE("Preparation counts distinguish successful requests from rejected attempts and survive cache moves", "[unit][utils][dependencies]") {
int source = 2;
int evaluations = 0;
const auto context = dependencies::make_dependency_context(dependencies::borrow<Source>(source));
const auto computation = dependencies::make_computation<Result, dependencies::InputList<Source>>(
[&evaluations](int input) -> std::expected<int, std::string> {
++evaluations;
if (input < 0) {
return std::unexpected("input must be nonnegative");
}
return 2 * input;
}
);
auto cache = dependencies::make_computation_cache(context, computation);
CHECK(cache.preparation_count() == 0);
const auto computed = cache.try_prepare(context);
REQUIRE(computed);
CHECK(computed->status<Result>() == dependencies::PreparationStatus::computed);
CHECK(cache.preparation_count() == 1);
CHECK(evaluations == 1);
const auto reused = cache.try_prepare(context);
REQUIRE(reused);
CHECK(reused->status<Result>() == dependencies::PreparationStatus::reused);
CHECK(cache.preparation_count() == 2);
CHECK(evaluations == 1);
source = -1;
const auto rejected = cache.try_prepare(context);
REQUIRE_FALSE(rejected);
CHECK_FALSE(cache.ready());
CHECK(cache.preparation_count() == 2);
CHECK(evaluations == 2);
cache.invalidate();
CHECK_FALSE(cache.ready());
CHECK(cache.preparation_count() == 2);
source = 2;
const auto recovered = cache.try_prepare(context);
REQUIRE(recovered);
CHECK(recovered->status<Result>() == dependencies::PreparationStatus::computed);
CHECK(cache.preparation_count() == 3);
CHECK(evaluations == 3);
auto moved = std::move(cache);
CHECK(moved.ready());
CHECK(moved.preparation_count() == 3);
CHECK(moved.get<Result>() == 4);
CHECK_FALSE(cache.ready());
CHECK(cache.preparation_count() == 0);
const auto moved_report = moved.try_prepare(context);
REQUIRE(moved_report);
CHECK(moved_report->status<Result>() == dependencies::PreparationStatus::reused);
CHECK(moved.preparation_count() == 4);
CHECK(evaluations == 3);
}