Skip to content

Commit 8650ce1

Browse files
bmehta001Copilot
andcommitted
Guarantee async ops are destroyed before teardown in both self-join tests
Extract a shared DrainOperation helper that waits for the operation to be destroyed and aborts a stuck worker as a fallback, then hard-asserts it is gone. Both async regression tests now ensure the detached worker (and its curl_easy_cleanup) cannot outlive fixture teardown (m_client -> curl_global_cleanup) on either the success or timeout path, rather than returning while the worker might still run. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d2c69dd commit 8650ce1

1 file changed

Lines changed: 34 additions & 41 deletions

File tree

‎tests/unittests/HttpClientCurlTests.cpp‎

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,24 @@ class HttpClientCurlTests : public ::testing::Test
3030
const std::vector<uint8_t> m_body;
3131
};
3232

33+
// Ensure a detached async operation is fully destroyed before the test returns, so the
34+
// worker's curl_easy_cleanup cannot race fixture teardown (m_client -> curl_global_cleanup).
35+
// The .invalid host fails DNS in milliseconds, so this normally completes immediately; a
36+
// stuck worker is aborted as a fallback. Returns whether the operation was destroyed.
37+
static bool DrainOperation(const std::weak_ptr<CurlHttpOperation>& weakOp)
38+
{
39+
for (int i = 0; i < 500 && !weakOp.expired(); ++i)
40+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
41+
if (!weakOp.expired())
42+
{
43+
if (auto liveOp = weakOp.lock())
44+
liveOp->Abort();
45+
for (int i = 0; i < 500 && !weakOp.expired(); ++i)
46+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
47+
}
48+
return weakOp.expired();
49+
}
50+
3351
// --- SetSslVerification wiring ---
3452

3553
TEST_F(HttpClientCurlTests, SslVerification_DefaultsToTrue)
@@ -176,28 +194,15 @@ TEST_F(HttpClientCurlTests, SendAsync_DestroyOnWorkerThread_NoSelfJoin)
176194
callbackDone->set_value();
177195
});
178196

179-
if (done.wait_for(std::chrono::seconds(15)) != std::future_status::ready)
180-
{
181-
// The detached worker is unexpectedly still running (Send() against the
182-
// non-resolving host should fail within milliseconds). Signal it to abort, then
183-
// wait for the operation to actually be destroyed (weakOp expires once the
184-
// worker releases its self-reference) so the worker does not outlive this stack
185-
// frame / fixture teardown, which destroys m_client (curl_global_cleanup) and the
186-
// m_headers/m_body it may still be reading. Then fail.
187-
if (auto liveOp = weakOp.lock())
188-
liveOp->Abort();
189-
for (int i = 0; i < 500 && !weakOp.expired(); ++i)
190-
std::this_thread::sleep_for(std::chrono::milliseconds(10));
191-
FAIL() << "SendAsync did not complete within 15s";
192-
}
197+
const bool completed = (done.wait_for(std::chrono::seconds(15)) == std::future_status::ready);
193198

194-
// Success path: the callback set the promise, but the detached worker still holds
195-
// its self-reference until RunSendAndCallback returns. Wait (bounded) for the
196-
// operation to be destroyed so its curl_easy_cleanup cannot race with fixture
197-
// teardown (m_client -> curl_global_cleanup).
198-
for (int i = 0; i < 500 && !weakOp.expired(); ++i)
199-
std::this_thread::sleep_for(std::chrono::milliseconds(10));
200-
EXPECT_TRUE(weakOp.expired());
199+
// Make sure the operation is destroyed before this test returns, regardless of
200+
// whether the send completed: the callback sets the promise while the detached
201+
// worker still holds its self-reference, so the worker (and its curl_easy_cleanup)
202+
// can outlive this frame and race fixture teardown (m_client -> curl_global_cleanup).
203+
// Drain (with an abort fallback) so the operation is gone first.
204+
ASSERT_TRUE(DrainOperation(weakOp)) << "operation still alive after abort; worker may outlive teardown";
205+
EXPECT_TRUE(completed) << "SendAsync did not complete within 15s";
201206
}
202207

203208
// A stack-constructed operation is not owned by a shared_ptr, so shared_from_this()
@@ -262,27 +267,15 @@ TEST_F(HttpClientCurlTests, SendAsync_NoOnDestroyDispatchAfterCompletion)
262267
callbackDone->set_value();
263268
});
264269

265-
if (done.wait_for(std::chrono::seconds(15)) != std::future_status::ready)
266-
{
267-
// The detached worker is unexpectedly still running (Send() against the
268-
// non-resolving host should fail within milliseconds). Abort it and wait so
269-
// it does not outlive this stack frame, which owns the m_headers / m_body the
270-
// worker may still read. (cb is heap-owned and captured by the worker, so it
271-
// stays alive on its own.) Then fail.
272-
if (auto liveOp = weakOp.lock())
273-
liveOp->Abort();
274-
for (int i = 0; i < 500 && !weakOp.expired(); ++i)
275-
std::this_thread::sleep_for(std::chrono::milliseconds(10));
276-
FAIL() << "SendAsync did not complete within 15s";
277-
}
270+
const bool completed = (done.wait_for(std::chrono::seconds(15)) == std::future_status::ready);
278271

279-
// The operation is destroyed once the worker returns and releases its
280-
// self-reference; wait for that, then let the destructor body finish so a
281-
// missing guard (which would increment the counter inside ~CurlHttpOperation)
282-
// is observed rather than raced past.
283-
for (int i = 0; i < 500 && !weakOp.expired(); ++i)
284-
std::this_thread::sleep_for(std::chrono::milliseconds(10));
285-
ASSERT_TRUE(weakOp.expired());
272+
// Ensure the operation is destroyed before this test returns so the worker cannot
273+
// outlive fixture teardown (m_client -> curl_global_cleanup); cb is heap-owned and
274+
// captured by the worker, so it stays alive on its own. Abort a stuck worker.
275+
ASSERT_TRUE(DrainOperation(weakOp)) << "operation still alive after abort; worker may outlive teardown";
276+
ASSERT_TRUE(completed) << "SendAsync did not complete within 15s";
277+
// Let the destructor body finish so a missing OnDestroy guard (which would increment
278+
// the counter inside ~CurlHttpOperation) is observed rather than raced past.
286279
std::this_thread::sleep_for(std::chrono::milliseconds(50));
287280

288281
EXPECT_EQ(cb->onDestroyAfterComplete.load(), 0);

0 commit comments

Comments
 (0)