#include #include #include #include #include #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> first; std::shared_ptr> second; std::weak_ptr> released; { int value = 4; auto context = dependencies::make_dependency_context(dependencies::borrow(value)); auto graph = dependencies::make_computation_graph(dependencies::make_computation>( [](const int& input) -> std::expected, std::string> { if (input < 0) { return std::unexpected("negative trial"); } return std::vector{input, input + 1}; })); auto cache = dependencies::make_computation_cache(std::move(graph), context); STATIC_CHECK(std::same_as()), std::shared_ptr>>); CHECK_THROWS(cache.share()); REQUIRE(cache.try_prepare(context)); first = cache.share(); CHECK((*first == std::vector{4, 5})); REQUIRE(cache.try_prepare(context)); CHECK(cache.share() == first); value = 9; REQUIRE(cache.try_prepare(context)); second = cache.share(); CHECK(second != first); CHECK((*first == std::vector{4, 5})); CHECK((*second == std::vector{9, 10})); value = -1; CHECK_FALSE(cache.try_prepare(context)); CHECK_FALSE(cache.ready()); CHECK_THROWS(cache.share()); CHECK((*first == std::vector{4, 5})); CHECK((*second == std::vector{9, 10})); value = 9; REQUIRE(cache.try_prepare(context)); CHECK(cache.share() == second); cache.invalidate(); CHECK_THROWS(cache.share()); CHECK((*second == std::vector{9, 10})); REQUIRE(cache.try_prepare(context)); released = cache.share(); CHECK_FALSE(released.expired()); } CHECK(released.expired()); CHECK((*first == std::vector{4, 5})); CHECK((*second == std::vector{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(3)); auto graph = dependencies::make_computation_graph( dependencies::make_computation>([](const int& value) { return value; }), dependencies::make_computation>([](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(); REQUIRE(cache.try_prepare(context, dependencies::KeyList{})); CHECK_THROWS(cache.share()); CHECK(*previous == 6); CHECK(*cache.share() == 3); auto moved = std::move(cache); CHECK_THROWS(cache.share()); CHECK(*moved.share() == 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>; Snapshot first; Snapshot second; std::weak_ptr> unretained; { int value = 4; auto context = dependencies::make_dependency_context(dependencies::borrow(value)); auto graph = dependencies::make_computation_graph( dependencies::make_owning_computation>([](Snapshot prerequisite) { return prerequisite; }), dependencies::make_computation>([](const int input) { return std::make_unique(input); }), dependencies::make_computation>([](const Snapshot& input) { return **input + 1; })); auto cache = dependencies::make_computation_cache(std::move(graph), context); REQUIRE(cache.try_prepare(context)); first = cache.get(); CHECK(first == cache.share()); CHECK(cache.get() == 5); const auto reused = cache.try_prepare(context); REQUIRE(reused); CHECK(reused->reused_count() == 3); CHECK(cache.get() == first); value = 9; REQUIRE(cache.try_prepare(context)); second = cache.get(); CHECK(second == cache.share()); CHECK(second != first); CHECK(**first == 4); CHECK(**second == 9); CHECK(cache.get() == 10); cache.invalidate(); CHECK(**first == 4); CHECK(**second == 9); REQUIRE(cache.try_prepare(context)); unretained = cache.share(); 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>; int value = 4; std::weak_ptr> rejected; auto context = dependencies::make_dependency_context(dependencies::borrow(value)); auto graph = dependencies::make_computation_graph( dependencies::make_computation>([](const int input) { return std::make_unique(input); }), dependencies::make_owning_computation>([&rejected](Snapshot prerequisite) -> std::expected { 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(); 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() == accepted); const auto rebuilt = cache.try_prepare(context, dependencies::KeyList{}, dependencies::PreparationMode::rebuild); REQUIRE(rebuilt); CHECK(rebuilt->computed_count() == 2); CHECK(cache.get() != accepted); CHECK(**accepted == 4); }