@@ -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
3553TEST_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