diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f96b23..48d9f3a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.25) project(async++) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +option(ASYNCPP_BUILD_TESTS ON) option(ENABLE_LLVM_COV OFF) option(ENABLE_LLVM_ADDRESS_SANITIZER OFF) option(ENABLE_LLVM_MEMORY_SANITIZER OFF) @@ -35,6 +36,12 @@ if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") endif() endif() +if (${ASYNCPP_BUILD_TESTS}) + add_compile_definitions(ASYNCPP_BUILD_TESTS=1) +endif() + add_subdirectory(include/async++) add_subdirectory(src) -add_subdirectory(test) \ No newline at end of file +if (${ASYNCPP_BUILD_TESTS}) + add_subdirectory(test) +endif() \ No newline at end of file diff --git a/include/async++/promise.hpp b/include/async++/promise.hpp index ce8f443..f753cc5 100644 --- a/include/async++/promise.hpp +++ b/include/async++/promise.hpp @@ -2,7 +2,9 @@ #include "awaitable.hpp" +#include #include +#include namespace asyncpp { @@ -91,4 +93,39 @@ struct result_promise { } }; + +namespace impl { + + class leak_checked_promise { + using snapshot_type = std::pair; + + public: +#ifdef ASYNCPP_BUILD_TESTS + leak_checked_promise() { num_alive.fetch_add(1, std::memory_order_relaxed); } + leak_checked_promise(const leak_checked_promise&) : leak_checked_promise() {} + leak_checked_promise(leak_checked_promise&&) : leak_checked_promise() {} + leak_checked_promise& operator=(const leak_checked_promise&) { return *this; } + leak_checked_promise& operator=(leak_checked_promise&&) { return *this; } + ~leak_checked_promise() { + num_alive.fetch_sub(1, std::memory_order_relaxed); + version.fetch_add(1, std::memory_order_relaxed); + } +#endif + + static snapshot_type snapshot() { + return { num_alive.load(std::memory_order_relaxed), version.load(std::memory_order_relaxed) }; + } + + static bool check(snapshot_type s) { + const auto current = snapshot(); + return current.first == s.first && current.second > s.second; + } + + private: + inline static std::atomic_intptr_t num_alive = 0; + inline static std::atomic_intptr_t version = 0; + }; + +} // namespace impl + } // namespace asyncpp \ No newline at end of file diff --git a/include/async++/shared_task.hpp b/include/async++/shared_task.hpp index a02422d..91ab58f 100644 --- a/include/async++/shared_task.hpp +++ b/include/async++/shared_task.hpp @@ -25,7 +25,7 @@ namespace impl_shared_task { }; template - struct promise : result_promise, resumable_promise, schedulable_promise { + struct promise : result_promise, resumable_promise, schedulable_promise, impl::leak_checked_promise { struct final_awaitable { constexpr bool await_ready() const noexcept { return false; } void await_suspend(std::coroutine_handle handle) noexcept { diff --git a/include/async++/stream.hpp b/include/async++/stream.hpp index b99143d..56dc54e 100644 --- a/include/async++/stream.hpp +++ b/include/async++/stream.hpp @@ -23,7 +23,7 @@ class stream; namespace impl_stream { template - struct promise : resumable_promise, schedulable_promise { + struct promise : resumable_promise, schedulable_promise, impl::leak_checked_promise { struct yield_awaitable { constexpr bool await_ready() const noexcept { return false; diff --git a/include/async++/task.hpp b/include/async++/task.hpp index c1f951b..c9f6a26 100644 --- a/include/async++/task.hpp +++ b/include/async++/task.hpp @@ -22,7 +22,7 @@ class task; namespace impl_task { template - struct promise : result_promise, resumable_promise, schedulable_promise { + struct promise : result_promise, resumable_promise, schedulable_promise, impl::leak_checked_promise { struct final_awaitable { constexpr bool await_ready() const noexcept { return false; } void await_suspend(std::coroutine_handle handle) const noexcept { diff --git a/test/helper_interleaving.hpp b/test/helper_interleaving.hpp index 40c675f..1088ec2 100644 --- a/test/helper_interleaving.hpp +++ b/test/helper_interleaving.hpp @@ -1,11 +1,12 @@ #pragma once -#include "helper_leak_tester.hpp" #include "helper_schedulers.hpp" #include #include +#include + #include using namespace asyncpp; @@ -24,17 +25,17 @@ auto run_dependent_tasks(MainTask main_task, main_result_t main_result; }; - const auto create_fixture = [&] { + auto create_fixture = [=, main_args = std::move(main_args), sub_args = std::move(sub_args)] { auto fixture_ = std::make_shared(); auto sub_result = launch(std::apply(sub_task, sub_args), fixture_->sub_sched); auto all_main_args = std::tuple_cat(std::forward_as_tuple(std::move(sub_result)), main_args); fixture_->main_result = launch(std::apply(main_task, std::move(all_main_args)), fixture_->main_sched); return fixture_; }; - const auto sub_thread = [](std::shared_ptr fixture_) { + auto sub_thread = [](std::shared_ptr fixture_) { fixture_->sub_sched.resume(); }; - const auto main_thread = [](std::shared_ptr fixture_) { + auto main_thread = [](std::shared_ptr fixture_) { fixture_->main_sched.resume(); if (!fixture_->main_result.ready()) { INTERLEAVED_ACQUIRE(fixture_->main_sched.wait()); @@ -43,9 +44,11 @@ auto run_dependent_tasks(MainTask main_task, join(fixture_->main_result); }; - return interleaving::run_all(std::function(create_fixture), - std::vector{ std::function(main_thread), std::function(sub_thread) }, - { "$main", "$sub" }); + return [create_fixture = std::move(create_fixture), sub_thread = std::move(sub_thread), main_thread = std::move(main_thread)] { + return interleaving::run_all(std::function(create_fixture), + std::vector{ std::function(main_thread), std::function(sub_thread) }, + { "$main", "$sub" }); + }; } @@ -58,33 +61,48 @@ auto run_abandoned_task(MainTask main_task, main_result_t main_result; }; - const auto create_fixture = [&] { + auto create_fixture = [main_task, main_args = std::move(main_args)] { auto fixture_ = std::make_shared(); fixture_->main_result = launch(std::apply(main_task, std::move(main_args)), fixture_->main_sched); return fixture_; }; - const auto exec_thread = [](std::shared_ptr fixture_) { + auto exec_thread = [](std::shared_ptr fixture_) { fixture_->main_sched.resume(); }; - const auto abandon_thread = [](std::shared_ptr fixture_) { + auto abandon_thread = [](std::shared_ptr fixture_) { fixture_->main_result = {}; }; - return interleaving::run_all(std::function(create_fixture), - std::vector{ std::function(abandon_thread), std::function(exec_thread) }, - { "$abandon", "$exec" }); + return [create_fixture = std::move(create_fixture), abandon_thread = std::move(abandon_thread), exec_thread = std::move(exec_thread)] { + return interleaving::run_all(std::function(create_fixture), + std::vector{ std::function(abandon_thread), std::function(exec_thread) }, + { "$abandon", "$exec" }); + }; } -template -void evaluate_interleavings(InterleavingGen interleaving_gen, std::optional tester = {}) { +template InterleavingGenFunc> +void evaluate_interleavings(InterleavingGenFunc interleaving_gen_func) { + size_t count = 0; + auto before = impl::leak_checked_promise::snapshot(); + for (const auto& interleaving : interleaving_gen_func()) { + INFO(count << "\n" + << (interleaving::interleaving_printer{ interleaving, true })); + REQUIRE(impl::leak_checked_promise::check(before)); + auto before = impl::leak_checked_promise::snapshot(); + ++count; + } + REQUIRE(count >= 3); +} + + +template InterleavingGen> +void evaluate_interleavings(InterleavingGen interleaving_gen) { size_t count = 0; for (const auto& interleaving : interleaving_gen) { + INFO(count << "\n" + << (interleaving::interleaving_printer{ interleaving, true })); ++count; - INFO((interleaving::interleaving_printer{ interleaving, true })); - if (tester) { - REQUIRE(tester.value()); - } } REQUIRE(count >= 3); } \ No newline at end of file diff --git a/test/helper_leak_tester.hpp b/test/helper_leak_tester.hpp deleted file mode 100644 index 2adf9f1..0000000 --- a/test/helper_leak_tester.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include - - -// Should be passed by value to a coroutine. -// Gets copied onto the coroutine stack and should get destructed -// when the coroutine promise is destroyed. -struct leak_tester { - leak_tester() : m_count(std::make_shared(0)) {} - operator bool() { return m_count.use_count() == 1; } - -private: - std::shared_ptr m_count; -}; \ No newline at end of file diff --git a/test/test_shared_task.cpp b/test/test_shared_task.cpp index d0a37ab..da84853 100644 --- a/test/test_shared_task.cpp +++ b/test/test_shared_task.cpp @@ -11,7 +11,7 @@ using namespace asyncpp; TEST_CASE("Shared task: interleaving co_await", "[Shared task]") { - static const auto sub_task = [](leak_tester tester) -> shared_task { + static const auto sub_task = []() -> shared_task { co_return 3; }; static const auto main_task = [](shared_task tsk) -> shared_task { @@ -19,28 +19,26 @@ TEST_CASE("Shared task: interleaving co_await", "[Shared task]") { co_return co_await tsk; }; - leak_tester tester; - auto interleavings = run_dependent_tasks(main_task, sub_task, std::tuple{}, std::tuple{ tester }); - evaluate_interleavings(std::move(interleavings), std::move(tester)); + auto interleavings = run_dependent_tasks(main_task, sub_task, std::tuple{}, std::tuple{}); + evaluate_interleavings(std::move(interleavings)); } TEST_CASE("Shared task: interleaving abandon", "[Shared task]") { - static const auto abandoned_task = [](leak_tester value) -> shared_task { co_return 3; }; + static const auto abandoned_task = []() -> shared_task { co_return 3; }; - leak_tester tester; - auto interleavings = run_abandoned_task(abandoned_task, std::tuple{ tester }); - evaluate_interleavings(std::move(interleavings), std::move(tester)); + auto interleavings = run_abandoned_task(abandoned_task, std::tuple{}); + evaluate_interleavings(std::move(interleavings)); } TEST_CASE("Shared task: abandon (not started)", "[Shared task]") { - static const auto coro = [](leak_tester tester) -> shared_task { + static const auto coro = []() -> shared_task { co_return; }; - leak_tester tester; - static_cast(coro(tester)); - REQUIRE(tester); + const auto before = impl::leak_checked_promise::snapshot(); + static_cast(coro()); + REQUIRE(impl::leak_checked_promise::check(before)); } diff --git a/test/test_stream.cpp b/test/test_stream.cpp index b13727b..e10eaf2 100644 --- a/test/test_stream.cpp +++ b/test/test_stream.cpp @@ -1,5 +1,3 @@ -#include "helper_leak_tester.hpp" - #include #include @@ -12,22 +10,22 @@ using namespace asyncpp; TEST_CASE("Stream: destroy", "[Task]") { - static const auto coro = [](leak_tester value) -> stream { co_yield 0; }; + static const auto coro = []() -> stream { co_yield 0; }; SECTION("no execution") { - leak_tester tester; + const auto before = impl::leak_checked_promise::snapshot(); { - auto s = coro(tester); + auto s = coro(); } - REQUIRE(tester); + REQUIRE(impl::leak_checked_promise::check(before)); } SECTION("synced") { - leak_tester tester; + const auto before = impl::leak_checked_promise::snapshot(); { - auto s = coro(tester); + auto s = coro(); join(s); } - REQUIRE(tester); + REQUIRE(impl::leak_checked_promise::check(before)); } } diff --git a/test/test_task.cpp b/test/test_task.cpp index fe14155..295d5e0 100644 --- a/test/test_task.cpp +++ b/test/test_task.cpp @@ -11,7 +11,7 @@ using namespace asyncpp; TEST_CASE("Task: interleaving co_await", "[Task]") { - static const auto sub_task = [](leak_tester tester) -> task { + static const auto sub_task = []() -> task { co_return 3; }; static const auto main_task = [](task tsk) -> task { @@ -19,28 +19,26 @@ TEST_CASE("Task: interleaving co_await", "[Task]") { co_return co_await tsk; }; - leak_tester tester; - auto interleavings = run_dependent_tasks(main_task, sub_task, std::tuple{}, std::tuple{ tester }); - evaluate_interleavings(std::move(interleavings), std::move(tester)); + auto interleavings = run_dependent_tasks(main_task, sub_task, std::tuple{}, std::tuple{}); + evaluate_interleavings(std::move(interleavings)); } TEST_CASE("Task: interleaving abandon", "[Task]") { - static const auto abandoned_task = [](leak_tester value) -> task { co_return 3; }; + static const auto abandoned_task = []() -> task { co_return 3; }; - leak_tester tester; - auto interleavings = run_abandoned_task(abandoned_task, std::tuple{ tester }); - evaluate_interleavings(std::move(interleavings), std::move(tester)); + auto interleavings = run_abandoned_task(abandoned_task, std::tuple{}); + evaluate_interleavings(std::move(interleavings)); } TEST_CASE("Task: abandon (not started)", "[Shared task]") { - static const auto coro = [](leak_tester tester) -> task { + static const auto coro = []() -> task { co_return; }; - leak_tester tester; - static_cast(coro(tester)); - REQUIRE(tester); + const auto before = impl::leak_checked_promise::snapshot(); + static_cast(coro()); + REQUIRE(impl::leak_checked_promise::check(before)); }