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,260 @@
#include "serif/utils/dependencies/dependencies.hpp"
#include <catch2/catch_test_macros.hpp>
#include <array>
#include <expected>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>
namespace {
namespace dependencies = serif::utils::dependencies;
struct Source {};
struct OtherSource {};
struct Shared {};
struct Left {};
struct Right {};
struct Total {};
}
TEST_CASE("Named computations build a cache directly with general and custom keys", "[unit][utils][dependencies]") {
namespace keys = dependencies::keys;
struct Offset { double unused; };
std::array<double, 3> a{1.0, 2.0, 3.0};
double scale = 2.0;
double offset = 0.5;
std::array<int, 2> calls{};
const auto context = dependencies::make_dependency_context(
dependencies::borrow<keys::Input>(a), dependencies::borrow<keys::Scale>(scale), dependencies::borrow<Offset>(offset)
);
const auto multiply = dependencies::make_computation<keys::Output, dependencies::InputList<keys::Input, keys::Scale>>(
[&calls](const std::array<double, 3>& input, double factor) {
++calls[0];
return std::array<double, 3>{factor * input[0], factor * input[1], factor * input[2]};
}
);
const auto sum = dependencies::make_computation<Total, dependencies::InputList<keys::Output, Offset>>(
[&calls](const std::array<double, 3>& input, double shift) {
++calls[1];
return input[0] + input[1] + input[2] + shift;
}
);
// No explicit graph or ownership transfer is needed; prerequisites may be supplied last.
auto cache = dependencies::make_computation_cache(context, sum, multiply);
CHECK_FALSE(cache.ready());
CHECK(calls == std::array<int, 2>{0, 0});
auto report = cache.try_prepare(context, dependencies::RequestedResults<Total>{});
REQUIRE(report);
CHECK(report->computed_count() == 2);
CHECK(cache.get<keys::Output>() == std::array<double, 3>{2.0, 4.0, 6.0});
CHECK(cache.get<Total>() == 12.5);
CHECK(calls == std::array<int, 2>{1, 1});
report = cache.try_prepare(context);
REQUIRE(report);
CHECK(report->reused_count() == 2);
CHECK(calls == std::array<int, 2>{1, 1});
offset = 1.5;
report = cache.try_prepare(context);
REQUIRE(report);
CHECK(report->status<keys::Output>() == dependencies::PreparationStatus::reused);
CHECK(report->status<Total>() == dependencies::PreparationStatus::computed);
CHECK(cache.get<Total>() == 13.5);
CHECK(calls == std::array<int, 2>{1, 2});
scale = 3.0;
report = cache.try_prepare(context);
REQUIRE(report);
CHECK(report->computed_count() == 2);
CHECK(cache.get<Total>() == 19.5);
CHECK(calls == std::array<int, 2>{2, 3});
}
TEST_CASE("Direct cache construction transfers computations owning noncopyable state", "[unit][utils][dependencies]") {
const auto context = dependencies::make_dependency_context(dependencies::own<Source>(4));
auto computation = dependencies::make_computation<Total, dependencies::InputList<Source>>(
[offset = std::make_unique<int>(3)](int input) { return input + *offset; }
);
auto cache = dependencies::make_computation_cache(context, std::move(computation));
REQUIRE(cache.try_prepare(context));
CHECK(cache.get<Total>() == 7);
const auto report = cache.try_prepare(context);
REQUIRE(report);
CHECK(report->status<Total>() == dependencies::PreparationStatus::reused);
}
TEST_CASE("A dependency diamond prepares shared work once in prerequisite order", "[unit][utils][dependencies]") {
int source = 2;
std::array<int, 4> calls{};
auto context = dependencies::make_dependency_context(dependencies::borrow<Source>(source));
auto graph = dependencies::make_computation_graph(
dependencies::make_computation<Total, dependencies::InputList<Left, Right>>([&calls](int left, int right) { ++calls[3]; return left + right; }),
dependencies::make_computation<Right, dependencies::InputList<Shared>>([&calls](int shared) { ++calls[2]; return 3 * shared; }),
dependencies::make_computation<Shared, dependencies::InputList<Source>>([&calls](int input) { ++calls[0]; return 2 * input; }),
dependencies::make_computation<Left, dependencies::InputList<Shared>>([&calls](int shared) { ++calls[1]; return shared + 1; })
);
auto cache = dependencies::make_computation_cache(graph, context);
CHECK_FALSE(cache.ready());
CHECK_THROWS_AS(cache.get<Total>(), std::logic_error);
auto report = cache.try_prepare(context, dependencies::RequestedResults<Total>{});
REQUIRE(report);
CHECK(report->computed_count() == 4);
CHECK(report->source_changed<Source>());
CHECK(cache.get<Total>() == 17);
CHECK(calls == std::array<int, 4>{1, 1, 1, 1});
report = cache.try_prepare(context);
REQUIRE(report);
CHECK(report->reused_count() == 4);
CHECK_FALSE(report->source_changed<Source>());
CHECK(calls == std::array<int, 4>{1, 1, 1, 1});
source = 3;
report = cache.try_prepare(context);
REQUIRE(report);
CHECK(report->computed_count() == 4);
CHECK(cache.get<Total>() == 25);
CHECK(calls == std::array<int, 4>{2, 2, 2, 2});
report = cache.try_prepare(context, dependencies::RequestedResults<Total>{}, dependencies::PreparationMode::rebuild);
REQUIRE(report);
CHECK(report->computed_count() == 4);
CHECK(calls == std::array<int, 4>{3, 3, 3, 3});
cache.invalidate();
CHECK_FALSE(cache.ready());
CHECK_THROWS_AS(cache.get<Left>(), std::logic_error);
report = cache.try_prepare(context);
REQUIRE(report);
CHECK(report->computed_count() == 4);
CHECK(calls == std::array<int, 4>{4, 4, 4, 4});
}
TEST_CASE("Partial dependency requests retain caches and propagate later changes", "[unit][utils][dependencies]") {
int source = 2;
int other_source = 3;
std::array<int, 3> calls{};
auto context = dependencies::make_dependency_context(dependencies::borrow<Source>(source), dependencies::borrow<OtherSource>(other_source));
auto graph = dependencies::make_computation_graph(
dependencies::make_computation<Total, dependencies::InputList<Left, Right>>([&calls](int left, int right) { ++calls[2]; return left + right; }),
dependencies::make_computation<Left, dependencies::InputList<Source>>([&calls](int input) { ++calls[0]; return 2 * input; }),
dependencies::make_computation<Right, dependencies::InputList<OtherSource>>([&calls](int input) { ++calls[1]; return 2 * input; })
);
auto cache = dependencies::make_computation_cache(std::move(graph), context);
REQUIRE(cache.try_prepare(context));
CHECK(cache.get<Total>() == 10);
source = 4;
auto report = cache.try_prepare(context, dependencies::RequestedResults<Left>{});
REQUIRE(report);
CHECK(calls == std::array<int, 3>{2, 1, 1});
CHECK(report->status<Right>() == dependencies::PreparationStatus::not_required);
CHECK_FALSE(cache.contains<Total>());
CHECK_THROWS_AS(cache.get<Right>(), std::logic_error);
report = cache.try_prepare(context, dependencies::RequestedResults<Total>{});
REQUIRE(report);
CHECK(cache.get<Total>() == 14);
CHECK(report->computed_count() == 1);
CHECK(report->reused_count() == 2);
CHECK(calls == std::array<int, 3>{2, 1, 2});
other_source = 5;
report = cache.try_prepare(context, dependencies::RequestedResults<Left>{});
REQUIRE(report);
CHECK_FALSE(report->source_changed<OtherSource>()); // This source was outside the request and was not inspected.
CHECK(report->reused_count() == 1);
report = cache.try_prepare(context, dependencies::RequestedResults<Right>{});
REQUIRE(report);
CHECK(report->source_changed<OtherSource>());
CHECK(cache.get<Right>() == 10);
report = cache.try_prepare(context);
REQUIRE(report);
CHECK(report->computed_count() == 1);
CHECK(cache.get<Total>() == 18);
CHECK(calls == std::array<int, 3>{2, 2, 3});
report = cache.try_prepare(context, dependencies::RequestedResults<>{});
REQUIRE(report);
CHECK(cache.ready());
CHECK_FALSE(cache.contains<Total>());
CHECK(report->computed_count() == 0);
CHECK(report->reused_count() == 0);
}
TEST_CASE("Empty dependency graphs and constant calculations need no sources", "[unit][utils][dependencies]") {
const auto context = dependencies::make_dependency_context();
auto empty = dependencies::make_computation_cache(dependencies::make_computation_graph(), context);
const auto empty_report = empty.try_prepare(context);
REQUIRE(empty_report);
CHECK(empty.ready());
CHECK(empty_report->computed_count() == 0);
int calls = 0;
auto graph = dependencies::make_computation_graph(dependencies::make_computation<Total, dependencies::InputList<>>([&calls] { ++calls; return 42; }));
auto cache = dependencies::make_computation_cache(std::move(graph), context);
REQUIRE(cache.try_prepare(context));
REQUIRE(cache.try_prepare(context));
CHECK(calls == 1);
CHECK(cache.get<Total>() == 42);
REQUIRE(cache.try_prepare(context, dependencies::RequestedResults<Total>{}, dependencies::PreparationMode::rebuild));
CHECK(calls == 2);
}
TEST_CASE("Move-only results retain accepted generations across rebuilds and rejection", "[unit][utils][dependencies]") {
using Value = std::unique_ptr<int>;
int source = 4;
int calls = 0;
const auto context = dependencies::make_dependency_context(dependencies::borrow<Source>(source));
const auto computation = dependencies::make_computation<Total, dependencies::InputList<Source>>(
[&calls](int input) -> std::expected<Value, std::string> {
++calls;
if (input < 0) {
return std::unexpected("negative input");
}
return std::make_unique<int>(input);
}
);
auto cache = dependencies::make_computation_cache(context, computation);
REQUIRE(cache.try_prepare(context));
const auto first = cache.share<Total>();
CHECK(**first == 4);
const auto reused = cache.try_prepare(context);
REQUIRE(reused);
CHECK(reused->status<Total>() == dependencies::PreparationStatus::reused);
CHECK(cache.share<Total>() == first);
CHECK(calls == 1);
const auto forced = cache.try_prepare(context, dependencies::RequestedResults<Total>{}, dependencies::PreparationMode::rebuild);
REQUIRE(forced);
CHECK(forced->status<Total>() == dependencies::PreparationStatus::computed);
const auto accepted = cache.share<Total>();
CHECK(accepted != first);
CHECK(calls == 2);
CHECK(**accepted == 4);
CHECK(**first == 4);
source = -1;
const auto rejected = cache.try_prepare(context);
REQUIRE_FALSE(rejected);
CHECK(rejected.error().error == "negative input");
CHECK_FALSE(cache.contains<Total>());
CHECK(calls == 3);
CHECK(**accepted == 4);
CHECK(**first == 4);
source = 4;
const auto recovered = cache.try_prepare(context);
REQUIRE(recovered);
CHECK(recovered->status<Total>() == dependencies::PreparationStatus::reused);
CHECK(cache.share<Total>() == accepted);
CHECK(calls == 3);
}