#include "serif/utils/dependencies/dependencies.hpp" #include #include #include #include #include #include #include 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 a{1.0, 2.0, 3.0}; double scale = 2.0; double offset = 0.5; std::array calls{}; const auto context = dependencies::make_dependency_context( dependencies::borrow(a), dependencies::borrow(scale), dependencies::borrow(offset) ); const auto multiply = dependencies::make_computation>( [&calls](const std::array& input, double factor) { ++calls[0]; return std::array{factor * input[0], factor * input[1], factor * input[2]}; } ); const auto sum = dependencies::make_computation>( [&calls](const std::array& 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{0, 0}); auto report = cache.try_prepare(context, dependencies::RequestedResults{}); REQUIRE(report); CHECK(report->computed_count() == 2); CHECK(cache.get() == std::array{2.0, 4.0, 6.0}); CHECK(cache.get() == 12.5); CHECK(calls == std::array{1, 1}); report = cache.try_prepare(context); REQUIRE(report); CHECK(report->reused_count() == 2); CHECK(calls == std::array{1, 1}); offset = 1.5; report = cache.try_prepare(context); REQUIRE(report); CHECK(report->status() == dependencies::PreparationStatus::reused); CHECK(report->status() == dependencies::PreparationStatus::computed); CHECK(cache.get() == 13.5); CHECK(calls == std::array{1, 2}); scale = 3.0; report = cache.try_prepare(context); REQUIRE(report); CHECK(report->computed_count() == 2); CHECK(cache.get() == 19.5); CHECK(calls == std::array{2, 3}); } TEST_CASE("Direct cache construction transfers computations owning noncopyable state", "[unit][utils][dependencies]") { const auto context = dependencies::make_dependency_context(dependencies::own(4)); auto computation = dependencies::make_computation>( [offset = std::make_unique(3)](int input) { return input + *offset; } ); auto cache = dependencies::make_computation_cache(context, std::move(computation)); REQUIRE(cache.try_prepare(context)); CHECK(cache.get() == 7); const auto report = cache.try_prepare(context); REQUIRE(report); CHECK(report->status() == dependencies::PreparationStatus::reused); } TEST_CASE("A dependency diamond prepares shared work once in prerequisite order", "[unit][utils][dependencies]") { int source = 2; std::array calls{}; auto context = dependencies::make_dependency_context(dependencies::borrow(source)); auto graph = dependencies::make_computation_graph( dependencies::make_computation>([&calls](int left, int right) { ++calls[3]; return left + right; }), dependencies::make_computation>([&calls](int shared) { ++calls[2]; return 3 * shared; }), dependencies::make_computation>([&calls](int input) { ++calls[0]; return 2 * input; }), dependencies::make_computation>([&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(), std::logic_error); auto report = cache.try_prepare(context, dependencies::RequestedResults{}); REQUIRE(report); CHECK(report->computed_count() == 4); CHECK(report->source_changed()); CHECK(cache.get() == 17); CHECK(calls == std::array{1, 1, 1, 1}); report = cache.try_prepare(context); REQUIRE(report); CHECK(report->reused_count() == 4); CHECK_FALSE(report->source_changed()); CHECK(calls == std::array{1, 1, 1, 1}); source = 3; report = cache.try_prepare(context); REQUIRE(report); CHECK(report->computed_count() == 4); CHECK(cache.get() == 25); CHECK(calls == std::array{2, 2, 2, 2}); report = cache.try_prepare(context, dependencies::RequestedResults{}, dependencies::PreparationMode::rebuild); REQUIRE(report); CHECK(report->computed_count() == 4); CHECK(calls == std::array{3, 3, 3, 3}); cache.invalidate(); CHECK_FALSE(cache.ready()); CHECK_THROWS_AS(cache.get(), std::logic_error); report = cache.try_prepare(context); REQUIRE(report); CHECK(report->computed_count() == 4); CHECK(calls == std::array{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 calls{}; auto context = dependencies::make_dependency_context(dependencies::borrow(source), dependencies::borrow(other_source)); auto graph = dependencies::make_computation_graph( dependencies::make_computation>([&calls](int left, int right) { ++calls[2]; return left + right; }), dependencies::make_computation>([&calls](int input) { ++calls[0]; return 2 * input; }), dependencies::make_computation>([&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() == 10); source = 4; auto report = cache.try_prepare(context, dependencies::RequestedResults{}); REQUIRE(report); CHECK(calls == std::array{2, 1, 1}); CHECK(report->status() == dependencies::PreparationStatus::not_required); CHECK_FALSE(cache.contains()); CHECK_THROWS_AS(cache.get(), std::logic_error); report = cache.try_prepare(context, dependencies::RequestedResults{}); REQUIRE(report); CHECK(cache.get() == 14); CHECK(report->computed_count() == 1); CHECK(report->reused_count() == 2); CHECK(calls == std::array{2, 1, 2}); other_source = 5; report = cache.try_prepare(context, dependencies::RequestedResults{}); REQUIRE(report); CHECK_FALSE(report->source_changed()); // This source was outside the request and was not inspected. CHECK(report->reused_count() == 1); report = cache.try_prepare(context, dependencies::RequestedResults{}); REQUIRE(report); CHECK(report->source_changed()); CHECK(cache.get() == 10); report = cache.try_prepare(context); REQUIRE(report); CHECK(report->computed_count() == 1); CHECK(cache.get() == 18); CHECK(calls == std::array{2, 2, 3}); report = cache.try_prepare(context, dependencies::RequestedResults<>{}); REQUIRE(report); CHECK(cache.ready()); CHECK_FALSE(cache.contains()); 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>([&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() == 42); REQUIRE(cache.try_prepare(context, dependencies::RequestedResults{}, 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 source = 4; int calls = 0; const auto context = dependencies::make_dependency_context(dependencies::borrow(source)); const auto computation = dependencies::make_computation>( [&calls](int input) -> std::expected { ++calls; if (input < 0) { return std::unexpected("negative input"); } return std::make_unique(input); } ); auto cache = dependencies::make_computation_cache(context, computation); REQUIRE(cache.try_prepare(context)); const auto first = cache.share(); CHECK(**first == 4); const auto reused = cache.try_prepare(context); REQUIRE(reused); CHECK(reused->status() == dependencies::PreparationStatus::reused); CHECK(cache.share() == first); CHECK(calls == 1); const auto forced = cache.try_prepare(context, dependencies::RequestedResults{}, dependencies::PreparationMode::rebuild); REQUIRE(forced); CHECK(forced->status() == dependencies::PreparationStatus::computed); const auto accepted = cache.share(); 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()); CHECK(calls == 3); CHECK(**accepted == 4); CHECK(**first == 4); source = 4; const auto recovered = cache.try_prepare(context); REQUIRE(recovered); CHECK(recovered->status() == dependencies::PreparationStatus::reused); CHECK(cache.share() == accepted); CHECK(calls == 3); }