Files
Emily Boudreaux cb20faeb3d 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.
2026-11-09 13:53:17 -05:00

204 lines
7.6 KiB
C++

#include <expected>
#include <memory>
#include <string>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include "serif/utils/dependencies/dependencies.hpp"
namespace
{
namespace dependencies = serif::utils::dependencies;
struct Source
{
};
struct Result
{
};
struct OtherResult
{
};
struct FinalResult
{
};
} // namespace
TEST_CASE("Shared dependency results survive refresh rejection invalidation and cache destruction", "[unit][utils][dependencies]")
{
std::shared_ptr<const std::vector<int>> first;
std::shared_ptr<const std::vector<int>> second;
std::weak_ptr<const std::vector<int>> released;
{
int value = 4;
auto context = dependencies::make_dependency_context(dependencies::borrow<Source>(value));
auto graph = dependencies::make_computation_graph(dependencies::make_computation<Result, dependencies::InputList<Source>>(
[](const int& input) -> std::expected<std::vector<int>, std::string>
{
if (input < 0)
{
return std::unexpected("negative trial");
}
return std::vector<int>{input, input + 1};
}));
auto cache = dependencies::make_computation_cache<std::string>(std::move(graph), context);
STATIC_CHECK(std::same_as<decltype(cache.share<Result>()), std::shared_ptr<const std::vector<int>>>);
CHECK_THROWS(cache.share<Result>());
REQUIRE(cache.try_prepare(context));
first = cache.share<Result>();
CHECK((*first == std::vector<int>{4, 5}));
REQUIRE(cache.try_prepare(context));
CHECK(cache.share<Result>() == first);
value = 9;
REQUIRE(cache.try_prepare(context));
second = cache.share<Result>();
CHECK(second != first);
CHECK((*first == std::vector<int>{4, 5}));
CHECK((*second == std::vector<int>{9, 10}));
value = -1;
CHECK_FALSE(cache.try_prepare(context));
CHECK_FALSE(cache.ready());
CHECK_THROWS(cache.share<Result>());
CHECK((*first == std::vector<int>{4, 5}));
CHECK((*second == std::vector<int>{9, 10}));
value = 9;
REQUIRE(cache.try_prepare(context));
CHECK(cache.share<Result>() == second);
cache.invalidate();
CHECK_THROWS(cache.share<Result>());
CHECK((*second == std::vector<int>{9, 10}));
REQUIRE(cache.try_prepare(context));
released = cache.share<Result>();
CHECK_FALSE(released.expired());
}
CHECK(released.expired());
CHECK((*first == std::vector<int>{4, 5}));
CHECK((*second == std::vector<int>{9, 10}));
released = first;
first.reset();
CHECK(released.expired());
}
TEST_CASE("Only currently requested dependency results can be shared while retained snapshots stay valid", "[unit][utils][dependencies]")
{
const auto context = dependencies::make_dependency_context(dependencies::own<Source>(3));
auto graph = dependencies::make_computation_graph(
dependencies::make_computation<Result, dependencies::InputList<Source>>([](const int& value)
{
return value;
}),
dependencies::make_computation<OtherResult, dependencies::InputList<Source>>([](const int& value)
{
return 2 * value;
}));
auto cache = dependencies::make_computation_cache(std::move(graph), context);
REQUIRE(cache.try_prepare(context));
const auto previous = cache.share<OtherResult>();
REQUIRE(cache.try_prepare(context, dependencies::KeyList<Result>{}));
CHECK_THROWS(cache.share<OtherResult>());
CHECK(*previous == 6);
CHECK(*cache.share<Result>() == 3);
auto moved = std::move(cache);
CHECK_THROWS(cache.share<Result>());
CHECK(*moved.share<Result>() == 3);
CHECK(*previous == 6);
}
TEST_CASE("Owning computations retain the exact move-only prerequisite across cache generations and owner lifetimes", "[unit][utils][dependencies]")
{
using Snapshot = std::shared_ptr<const std::unique_ptr<int>>;
Snapshot first;
Snapshot second;
std::weak_ptr<const std::unique_ptr<int>> unretained;
{
int value = 4;
auto context = dependencies::make_dependency_context(dependencies::borrow<Source>(value));
auto graph = dependencies::make_computation_graph(
dependencies::make_owning_computation<OtherResult, dependencies::InputList<Result>>([](Snapshot prerequisite)
{
return prerequisite;
}),
dependencies::make_computation<Result, dependencies::InputList<Source>>([](const int input)
{
return std::make_unique<int>(input);
}),
dependencies::make_computation<FinalResult, dependencies::InputList<OtherResult>>([](const Snapshot& input)
{
return **input + 1;
}));
auto cache = dependencies::make_computation_cache(std::move(graph), context);
REQUIRE(cache.try_prepare(context));
first = cache.get<OtherResult>();
CHECK(first == cache.share<Result>());
CHECK(cache.get<FinalResult>() == 5);
const auto reused = cache.try_prepare(context);
REQUIRE(reused);
CHECK(reused->reused_count() == 3);
CHECK(cache.get<OtherResult>() == first);
value = 9;
REQUIRE(cache.try_prepare(context));
second = cache.get<OtherResult>();
CHECK(second == cache.share<Result>());
CHECK(second != first);
CHECK(**first == 4);
CHECK(**second == 9);
CHECK(cache.get<FinalResult>() == 10);
cache.invalidate();
CHECK(**first == 4);
CHECK(**second == 9);
REQUIRE(cache.try_prepare(context));
unretained = cache.share<Result>();
CHECK_FALSE(unretained.expired());
}
CHECK(unretained.expired());
CHECK(**first == 4);
CHECK(**second == 9);
}
TEST_CASE("Rejected owning computations release candidate prerequisites and preserve the accepted generation", "[unit][utils][dependencies]")
{
using Snapshot = std::shared_ptr<const std::unique_ptr<int>>;
int value = 4;
std::weak_ptr<const std::unique_ptr<int>> rejected;
auto context = dependencies::make_dependency_context(dependencies::borrow<Source>(value));
auto graph = dependencies::make_computation_graph(
dependencies::make_computation<Result, dependencies::InputList<Source>>([](const int input)
{
return std::make_unique<int>(input);
}),
dependencies::make_owning_computation<OtherResult, dependencies::InputList<Result>>([&rejected](Snapshot prerequisite) -> std::expected<Snapshot, std::string>
{
if (**prerequisite < 0)
{
rejected = prerequisite;
return std::unexpected("negative candidate");
}
return prerequisite;
}));
auto cache = dependencies::make_computation_cache(std::move(graph), context);
REQUIRE(cache.try_prepare(context));
const Snapshot accepted = cache.get<OtherResult>();
value = -1;
const auto failure = cache.try_prepare(context);
REQUIRE_FALSE(failure);
CHECK(failure.error().error == "negative candidate");
CHECK(rejected.expired());
CHECK_FALSE(cache.ready());
CHECK(**accepted == 4);
value = 4;
const auto recovered = cache.try_prepare(context);
REQUIRE(recovered);
CHECK(recovered->reused_count() == 2);
CHECK(cache.get<OtherResult>() == accepted);
const auto rebuilt = cache.try_prepare(context, dependencies::KeyList<OtherResult>{}, dependencies::PreparationMode::rebuild);
REQUIRE(rebuilt);
CHECK(rebuilt->computed_count() == 2);
CHECK(cache.get<OtherResult>() != accepted);
CHECK(**accepted == 4);
}