#include "serif/utils/dependencies/dependencies.hpp" #include #include #include #include #include namespace { namespace dependencies = serif::utils::dependencies; struct Source {}; struct Unused {}; struct Result {}; struct OtherResult {}; struct Untrackable {}; struct View { const int& binding; const int& value; }; struct ViewTracking { /// Snapshot content without retaining the temporary view. int capture(const View& view) const { return view.value; } /// Vector storage may move or be copied without changing its value. bool matches(const int& captured, const View& view) const { return captured == view.value; } /// The persistent role, rather than its temporary wrapper, defines identity. const void* identity(const View& view) const { return &view.binding; } }; struct RevisionedSource { std::unique_ptr value; unsigned revision{0}; }; struct OwnedSnapshot { int& captures; int& comparisons; /// Exercise an owning snapshot that cannot itself be copied. std::unique_ptr capture(const RevisionedSource& source) const { ++captures; return std::make_unique(*source.value); } /// Compare the frozen content, independent of source revision counters. bool matches(const std::unique_ptr& captured, const RevisionedSource& source) const { ++comparisons; return *captured == *source.value; } }; } TEST_CASE("Dependency views can preserve underlying identity across temporary wrappers", "[unit][utils][dependencies]") { int binding = 1, other_binding = 1, value = 4, copied_value = 4; const auto context = dependencies::make_dependency_context(dependencies::own(View{binding, value}, ViewTracking{})); auto graph = dependencies::make_computation_graph(dependencies::make_computation>([](const View& view) { return view.value; })); auto cache = dependencies::make_computation_cache(std::move(graph), context); REQUIRE(cache.try_prepare(context)); const auto copied = dependencies::make_dependency_context(dependencies::own(View{binding, copied_value}, ViewTracking{})); auto report = cache.try_prepare(copied); REQUIRE(report); CHECK(report->reused_count() == 1); const auto rebound = dependencies::make_dependency_context(dependencies::own(View{other_binding, copied_value}, ViewTracking{})); report = cache.try_prepare(rebound); REQUIRE(report); CHECK(report->computed_count() == 1); copied_value = 8; REQUIRE(cache.try_prepare(rebound)); CHECK(cache.get() == 8); } TEST_CASE("Dependency value snapshots detect in-place container changes", "[unit][utils][dependencies]") { std::vector values{2, 3}; int calls = 0; auto context = dependencies::make_dependency_context(dependencies::borrow(values), dependencies::own(Untrackable{})); auto graph = dependencies::make_computation_graph(dependencies::make_computation>([&calls](const std::vector& input) { ++calls; return std::accumulate(input.begin(), input.end(), 0); })); auto cache = dependencies::make_computation_cache(std::move(graph), context); REQUIRE(cache.try_prepare(context)); CHECK(cache.get() == 5); values[1] = 7; REQUIRE(cache.try_prepare(context)); CHECK(cache.get() == 9); CHECK(calls == 2); values.push_back(4); REQUIRE(cache.try_prepare(context)); CHECK(cache.get() == 13); CHECK(calls == 3); REQUIRE(cache.try_prepare(context)); CHECK(calls == 3); } TEST_CASE("A dependency projection tracks noncopyable sources by revision", "[unit][utils][dependencies]") { RevisionedSource source{.value = std::make_unique(2)}; int calls = 0; auto context = dependencies::make_dependency_context(dependencies::borrow(source, dependencies::track_by([](const RevisionedSource& input) { return input.revision; }))); auto graph = dependencies::make_computation_graph(dependencies::make_computation>([&calls](const RevisionedSource& input) { ++calls; return *input.value; })); auto cache = dependencies::make_computation_cache(std::move(graph), context); REQUIRE(cache.try_prepare(context)); REQUIRE(cache.try_prepare(context)); CHECK(calls == 1); *source.value = 8; ++source.revision; REQUIRE(cache.try_prepare(context)); CHECK(calls == 2); CHECK(cache.get() == 8); } TEST_CASE("Required sources are inspected once and can use move-only snapshot state", "[unit][utils][dependencies]") { RevisionedSource source{.value = std::make_unique(2)}; int captures = 0; int comparisons = 0; auto context = dependencies::make_dependency_context(dependencies::borrow(source, OwnedSnapshot{.captures = captures, .comparisons = comparisons})); auto graph = dependencies::make_computation_graph( dependencies::make_computation>([](const RevisionedSource& input) { return *input.value; }), dependencies::make_computation>([](const RevisionedSource& input) { return 2 * *input.value; }) ); auto cache = dependencies::make_computation_cache(std::move(graph), context); REQUIRE(cache.try_prepare(context)); CHECK(captures == 1); CHECK(comparisons == 0); REQUIRE(cache.try_prepare(context)); CHECK(captures == 1); CHECK(comparisons == 1); *source.value = 9; REQUIRE(cache.try_prepare(context)); CHECK(captures == 2); CHECK(comparisons == 2); CHECK(cache.get() == 9); CHECK(cache.get() == 18); }