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.
204 lines
7.6 KiB
C++
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);
|
|
}
|