blob: 30860c21d0662796fe033349a47ece70053d71da [file]
/*
* SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION &
* AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
#include "../coroutine.hpp"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
namespace requester
{
/**
* An awaiter that suspends a coroutine indefinitely (never resumes it
* automatically). Used to create a coroutine that is suspended mid-flight
* so we can test what happens when its Coroutine RAII wrapper is destroyed
* before the coroutine completes.
*/
struct SuspendForever
{
bool await_ready() const noexcept
{
return false;
}
void await_suspend(std::coroutine_handle<>) const noexcept {}
void await_resume() const noexcept {}
};
/** Coroutine that suspends itself and never resumes. */
static Coroutine suspendedCoroutine()
{
co_await SuspendForever{};
co_return 0;
}
/** Coroutine that completes immediately without suspending. */
static Coroutine completedCoroutine()
{
co_return 0;
}
/**
* Test: destructor must destroy a suspended (not-done) coroutine handle.
*
* Regression test for the bug where ~Coroutine() used
* if (handle && handle.done())
* which skipped handle.destroy() for suspended coroutines, leaking the
* heap-allocated coroutine frame. Valgrind reports this as "definitely
* lost" bytes when the old (buggy) condition is used.
*
* The fix — removing the handle.done() guard — is safe because:
* - NsmDevice::~NsmDevice() calls task.detach() before ~Coroutine() runs,
* transferring ownership to the event loop (handle set to nullptr).
* - deviceTask holds shared_ptr<NsmDevice> by value, so ~NsmDevice() is
* only entered when use_count drops to zero, which cannot happen while
* the coroutine frame is live.
*/
#ifndef COVERAGE_DISABLE_COROUTINES
TEST(CoroutineDestructorTest, DestroySuspendedHandle_NoMemoryLeak)
{
{
auto coro = suspendedCoroutine();
// Coroutine is suspended at co_await SuspendForever{} — handle is
// valid and handle.done() == false.
EXPECT_FALSE(coro.done());
// ~Coroutine() must call handle.destroy() here regardless of done().
}
// If the destructor used `if (handle && handle.done())`, valgrind would
// report the coroutine frame as definitely lost at this point.
}
#endif // COVERAGE_DISABLE_COROUTINES
/**
* Test: destructor must also destroy a completed coroutine handle.
*
* Sanity-check that the fix does not break the normal (already-done) path.
*/
TEST(CoroutineDestructorTest, DestroyCompletedHandle_NoMemoryLeak)
{
{
auto coro = completedCoroutine();
EXPECT_TRUE(coro.done());
// ~Coroutine() destroys the handle here as well.
}
}
/**
* Test: move assignment must destroy the old handle before replacing it.
*
* Verifies that operator=(Coroutine&&) calls handle.destroy() on the
* previously held (suspended) handle so it is not leaked.
*
* Not applicable in COVERAGE_DISABLE_COROUTINES mode: co_await is a no-op
* so suspendedCoroutine() completes immediately and there is no suspended
* handle to destroy. The coroutine.hpp move-assignment has a separate
* #else branch for coverage mode that transfers promise.data instead.
*/
#ifndef COVERAGE_DISABLE_COROUTINES
TEST(CoroutineDestructorTest, MoveAssignment_DestroysOldSuspendedHandle)
#else
TEST(CoroutineDestructorTest,
DISABLED_MoveAssignment_DestroysOldSuspendedHandle)
#endif
{
#ifdef COVERAGE_DISABLE_COROUTINES
// In coverage mode co_await is a no-op, so suspendedCoroutine() completes
// immediately and done() returns true. The test is only meaningful with
// real coroutines.
GTEST_SKIP() << "Skipped: coroutine suspension not available in coverage";
#endif
auto coro = suspendedCoroutine();
EXPECT_FALSE(coro.done());
// Move-assign a new (completed) coroutine; the old suspended handle must
// be destroyed and must not leak.
coro = completedCoroutine();
EXPECT_TRUE(coro.done());
}
/**
* Tests for Coroutine::assign().
*
* Regression coverage for bug 5749651: assign() called with a task that has
* no co_await (completes immediately) caused a double-destroy crash:
* 1. assign() called co.handle.destroy() on the done frame.
* 2. co.handle was not nulled → dangling pointer.
* 3. ~Coroutine() checked handle.done() on freed memory (UB).
* 4. If UB returned true → second handle.destroy() → SIGABRT (double-free).
* Fix: co.handle = nullptr after every explicit destroy inside assign().
*/
// Immediate task (no co_await): verifies no double-free (bug 5749651).
// If assign() omits `co.handle = nullptr` after destroy, this crashes with
// SIGABRT under ASAN/valgrind or sporadically in production.
TEST(CoroutineAssignTest, ImmediateTask_NoCrashAndHandleNull)
{
std::coroutine_handle<> handle{};
bool result = Coroutine::assign(handle, []() -> Coroutine { co_return 0; });
EXPECT_TRUE(result);
// Immediately-done frame is destroyed inside assign(); handle stays null.
EXPECT_EQ(handle, nullptr);
}
// Two consecutive immediate tasks: no accumulated state between calls.
TEST(CoroutineAssignTest, ImmediateTaskTwice_BothSucceed)
{
std::coroutine_handle<> handle{};
EXPECT_TRUE(Coroutine::assign(handle, []() -> Coroutine { co_return 0; }));
EXPECT_EQ(handle, nullptr);
EXPECT_TRUE(Coroutine::assign(handle, []() -> Coroutine { co_return 0; }));
EXPECT_EQ(handle, nullptr);
}
#ifndef COVERAGE_DISABLE_COROUTINES
// Suspended task: handle transferred to caller, not done.
TEST(CoroutineAssignTest, SuspendedTask_HandleSetAndNotDone)
{
std::coroutine_handle<> handle{};
bool result = Coroutine::assign(handle, []() -> Coroutine {
co_await SuspendForever{};
co_return 0;
});
EXPECT_TRUE(result);
EXPECT_NE(handle, nullptr);
EXPECT_FALSE(handle.done());
handle.destroy();
}
// assign() while a coroutine is still suspended must return false and leave
// the existing handle untouched.
TEST(CoroutineAssignTest, AssignWhileRunning_ReturnsFalse)
{
std::coroutine_handle<> handle{};
Coroutine::assign(handle, []() -> Coroutine {
co_await SuspendForever{};
co_return 0;
});
ASSERT_NE(handle, nullptr);
ASSERT_FALSE(handle.done());
bool result = Coroutine::assign(handle, []() -> Coroutine { co_return 0; });
EXPECT_FALSE(result);
// Original handle must be unchanged.
EXPECT_NE(handle, nullptr);
EXPECT_FALSE(handle.done());
handle.destroy();
}
#endif // COVERAGE_DISABLE_COROUTINES
} // namespace requester