From 42cced50fb3dcdf24b3188eaa786b412b133d8bd Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:56:58 +0000 Subject: [PATCH] feat: Migrate coroutine tests from Boost.Coroutine to C++20 coroutines Migrate Coroutine_test and JobQueue_test from Boost.Coroutine to C++20 std::coroutine using CoroTask/CoroTaskRunner: - Coroutine_test: Replace Coro-based coroutine tests with CoroTask equivalents using co_await runner->yieldAndPost(). - JobQueue_test: Replace Coro suspend/resume patterns with CoroTask equivalents, use pointer-by-value captures in coroutine lambdas to avoid dangling reference issues. --- src/test/core/Coroutine_test.cpp | 65 +++++++++++++++++----------- src/test/core/JobQueue_test.cpp | 74 +++++++++++++++++--------------- 2 files changed, 80 insertions(+), 59 deletions(-) diff --git a/src/test/core/Coroutine_test.cpp b/src/test/core/Coroutine_test.cpp index 73dc3c2f8f..af187dc1cc 100644 --- a/src/test/core/Coroutine_test.cpp +++ b/src/test/core/Coroutine_test.cpp @@ -40,6 +40,11 @@ public: } }; + // NOTE: All coroutine lambdas passed to postCoroTask use explicit + // pointer-by-value captures instead of [&] to work around a GCC 14 + // bug where reference captures in coroutine lambdas are corrupted + // in the coroutine frame. + void correct_order() { @@ -54,13 +59,15 @@ public: })); gate g1, g2; - std::shared_ptr c; - env.app().getJobQueue().postCoro(jtCLIENT, "CoroTest", [&](auto const& cr) { - c = cr; - g1.signal(); - c->yield(); - g2.signal(); - }); + std::shared_ptr c; + env.app().getJobQueue().postCoroTask( + jtCLIENT, "CoroTest", [cp = &c, g1p = &g1, g2p = &g2](auto runner) -> CoroTask { + *cp = runner; + g1p->signal(); + co_await runner->suspend(); + g2p->signal(); + co_return; + }); BEAST_EXPECT(g1.wait_for(5s)); c->join(); c->post(); @@ -81,11 +88,17 @@ public: })); gate g; - env.app().getJobQueue().postCoro(jtCLIENT, "CoroTest", [&](auto const& c) { - c->post(); - c->yield(); - g.signal(); - }); + env.app().getJobQueue().postCoroTask( + jtCLIENT, "CoroTest", [gp = &g](auto runner) -> CoroTask { + // Schedule a resume before suspending. The posted job + // cannot actually call resume() until the current resume() + // releases CoroTaskRunner::mutex_, which only happens after + // the coroutine suspends at co_await. + runner->post(); + co_await runner->suspend(); + gp->signal(); + co_return; + }); BEAST_EXPECT(g.wait_for(5s)); } @@ -101,7 +114,7 @@ public: auto& jq = env.app().getJobQueue(); static int const N = 4; - std::array, N> a; + std::array, N> a; LocalValue lv(-1); BEAST_EXPECT(*lv == -1); @@ -118,19 +131,23 @@ public: for (int i = 0; i < N; ++i) { - jq.postCoro(jtCLIENT, "CoroTest", [&, id = i](auto const& c) { - a[id] = c; - g.signal(); - c->yield(); + jq.postCoroTask( + jtCLIENT, + "CoroTest", + [this, ap = &a, gp = &g, lvp = &lv, id = i](auto runner) -> CoroTask { + (*ap)[id] = runner; + gp->signal(); + co_await runner->suspend(); - this->BEAST_EXPECT(*lv == -1); - *lv = id; - this->BEAST_EXPECT(*lv == id); - g.signal(); - c->yield(); + this->BEAST_EXPECT(**lvp == -1); + **lvp = id; + this->BEAST_EXPECT(**lvp == id); + gp->signal(); + co_await runner->suspend(); - this->BEAST_EXPECT(*lv == id); - }); + this->BEAST_EXPECT(**lvp == id); + co_return; + }); BEAST_EXPECT(g.wait_for(5s)); a[i]->join(); } diff --git a/src/test/core/JobQueue_test.cpp b/src/test/core/JobQueue_test.cpp index 13142c299f..470041fdee 100644 --- a/src/test/core/JobQueue_test.cpp +++ b/src/test/core/JobQueue_test.cpp @@ -43,87 +43,91 @@ class JobQueue_test : public beast::unit_test::suite } } + // NOTE: All coroutine lambdas passed to postCoroTask use explicit + // pointer-by-value captures instead of [&] to work around a GCC 14 + // bug where reference captures in coroutine lambdas are corrupted + // in the coroutine frame. + void - testPostCoro() + testPostCoroTask() { jtx::Env env{*this}; JobQueue& jQueue = env.app().getJobQueue(); { - // Test repeated post()s until the Coro completes. + // Test repeated post()s until the coroutine completes. std::atomic yieldCount{0}; - auto const coro = jQueue.postCoro( - jtCLIENT, - "PostCoroTest1", - [&yieldCount](std::shared_ptr const& coroCopy) { - while (++yieldCount < 4) - coroCopy->yield(); + auto const runner = jQueue.postCoroTask( + jtCLIENT, "PostCoroTest1", [ycp = &yieldCount](auto runner) -> CoroTask { + while (++(*ycp) < 4) + co_await runner->suspend(); + co_return; }); - BEAST_EXPECT(coro != nullptr); + BEAST_EXPECT(runner != nullptr); // Wait for the Job to run and yield. while (yieldCount == 0) ; - // Now re-post until the Coro says it is done. + // Now re-post until the CoroTaskRunner says it is done. int old = yieldCount; - while (coro->runnable()) + while (runner->runnable()) { - BEAST_EXPECT(coro->post()); + BEAST_EXPECT(runner->post()); while (old == yieldCount) { } - coro->join(); + runner->join(); BEAST_EXPECT(++old == yieldCount); } BEAST_EXPECT(yieldCount == 4); } { - // Test repeated resume()s until the Coro completes. + // Test repeated resume()s until the coroutine completes. int yieldCount{0}; - auto const coro = jQueue.postCoro( - jtCLIENT, - "PostCoroTest2", - [&yieldCount](std::shared_ptr const& coroCopy) { - while (++yieldCount < 4) - coroCopy->yield(); + auto const runner = jQueue.postCoroTask( + jtCLIENT, "PostCoroTest2", [ycp = &yieldCount](auto runner) -> CoroTask { + while (++(*ycp) < 4) + co_await runner->suspend(); + co_return; }); - if (!coro) + if (!runner) { - // There's no good reason we should not get a Coro, but we + // There's no good reason we should not get a runner, but we // can't continue without one. BEAST_EXPECT(false); return; } // Wait for the Job to run and yield. - coro->join(); + runner->join(); - // Now resume until the Coro says it is done. + // Now resume until the CoroTaskRunner says it is done. int old = yieldCount; - while (coro->runnable()) + while (runner->runnable()) { - coro->resume(); // Resume runs synchronously on this thread. + runner->resume(); // Resume runs synchronously on this thread. BEAST_EXPECT(++old == yieldCount); } BEAST_EXPECT(yieldCount == 4); } { // If the JobQueue is stopped, we should no - // longer be able to add a Coro (and calling postCoro() should - // return false). + // longer be able to post a coroutine (and calling postCoroTask() + // should return nullptr). using namespace std::chrono_literals; jQueue.stop(); - // The Coro should never run, so having the Coro access this + // The coroutine should never run, so having it access this // unprotected variable on the stack should be completely safe. // Not recommended for the faint of heart... - bool unprotected = false; - auto const coro = jQueue.postCoro( - jtCLIENT, "PostCoroTest3", [&unprotected](std::shared_ptr const&) { - unprotected = false; + bool unprotected; + auto const runner = jQueue.postCoroTask( + jtCLIENT, "PostCoroTest3", [up = &unprotected](auto) -> CoroTask { + *up = false; + co_return; }); - BEAST_EXPECT(coro == nullptr); + BEAST_EXPECT(runner == nullptr); } } @@ -132,7 +136,7 @@ public: run() override { testAddJob(); - testPostCoro(); + testPostCoroTask(); } };