Skip to content

Commit 6ec6077

Browse files
committed
use mocking for cancellation tests to avoid timing issues.
1 parent 3ba10cd commit 6ec6077

1 file changed

Lines changed: 46 additions & 28 deletions

File tree

tests/test_qthreadexec.py

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
import threading
77
import time
88
import weakref
9-
from concurrent.futures import CancelledError, TimeoutError
9+
from concurrent.futures import Future, TimeoutError
1010
from itertools import islice
11+
from unittest.mock import Mock, patch
1112

1213
import pytest
1314

@@ -124,28 +125,31 @@ def test_context(executor):
124125

125126

126127
@pytest.mark.parametrize("cancel", [True, False])
127-
def test_shutdown_cancel_futures(executor, cancel):
128+
def test_shutdown_cancel_futures(cancel):
128129
"""Test that shutdown with cancel_futures=True cancels all remaining futures in the queue."""
129130

130-
def task():
131-
time.sleep(0.01)
131+
# Create an executor with no workers so futures stay queued and never execute
132+
executor = qasync.QThreadExecutor(max_workers=0)
132133

133-
# Submit ten tasks to the executor
134-
futures = [executor.submit(task) for _ in range(10)]
135-
# shut it down
136-
executor.shutdown(cancel_futures=cancel)
134+
futures = [executor.submit(lambda: None) for _ in range(10)]
137135

138-
cancels = 0
139-
for future in futures:
140-
try:
141-
future.result(timeout=0.01)
142-
except CancelledError:
143-
cancels += 1
136+
# Shutdown with cancel_futures parameter
137+
executor.shutdown(wait=False, cancel_futures=cancel)
144138

145139
if cancel:
146-
assert cancels > 0
140+
# All futures should be cancelled since no workers consumed them
141+
cancelled_count = sum(1 for f in futures if f.cancelled())
142+
assert cancelled_count == 10, (
143+
f"Expected all 10 futures to be cancelled, got {cancelled_count}"
144+
)
147145
else:
148-
assert cancels == 0
146+
# No futures should be cancelled, they should still be pending
147+
cancelled_count = sum(1 for f in futures if f.cancelled())
148+
assert cancelled_count == 0, (
149+
f"Expected no futures to be cancelled, got {cancelled_count}"
150+
)
151+
152+
executor.shutdown(wait=True, cancel_futures=False)
149153

150154

151155
def test_map(executor):
@@ -232,17 +236,31 @@ def test_map_start(executor):
232236
assert list(m) == [(None, 0)]
233237

234238

235-
def test_map_close(executor):
239+
def test_map_close():
236240
"""Test that closing a running map cancels all remaining tasks."""
237-
results = []
238-
def func(x):
239-
nonlocal results
240-
time.sleep(0.05)
241-
results.append(x)
242-
return x
243-
m = executor.map(func, range(10))
244-
# must start the generator so that close() has any effect
245-
assert next(m) == 0
246-
m.close()
241+
242+
# Create an executor with no workers so we have full control
243+
executor = qasync.QThreadExecutor(max_workers=0)
244+
245+
# Create mock futures with proper result() method
246+
mock_futures = []
247+
for i in range(10):
248+
mock_future = Mock(spec=Future)
249+
mock_future.cancel = Mock(return_value=True)
250+
mock_future.result = Mock(return_value=i)
251+
mock_futures.append(mock_future)
252+
253+
# Mock submit to return our pre-created futures
254+
with patch.object(executor, "submit", side_effect=mock_futures):
255+
m = executor.map(lambda x: x, range(10))
256+
# must start the generator so that close() has any effect
257+
assert next(m) == 0
258+
m.close()
259+
260+
# All futures should have cancel() called:
261+
# - The first one via _result_or_cancel after next() consumed it
262+
# - The rest via the finally block when the generator is closed
263+
for i, f in enumerate(mock_futures):
264+
assert f.cancel.called, f"Future {i} should have been cancelled"
265+
247266
executor.shutdown(wait=True, cancel_futures=False)
248-
assert len(results) < 10, "Some tasks should have been cancelled"

0 commit comments

Comments
 (0)