|
2 | 2 | # © 2014 Mark Harviston <mark.harviston@gmail.com> |
3 | 3 | # © 2014 Arve Knudsen <arve.knudsen@gmail.com> |
4 | 4 | # BSD License |
| 5 | +import threading |
| 6 | +import weakref |
| 7 | +import logging |
5 | 8 |
|
6 | 9 | import pytest |
| 10 | + |
7 | 11 | import qasync |
8 | 12 |
|
9 | 13 |
|
| 14 | +_TestObject = type("_TestObject", (object,), {}) |
| 15 | + |
| 16 | + |
| 17 | +@pytest.fixture(autouse=True) |
| 18 | +def disable_executor_logging(): |
| 19 | + """ |
| 20 | + When running under pytest, leftover LogRecord objects |
| 21 | + keep references to objects in the scope that logging was called in. |
| 22 | + To avoid issues with tests targeting stale references, |
| 23 | + we disable logging for QThreadExecutor and _QThreadWorker classes. |
| 24 | + """ |
| 25 | + for cls in (qasync.QThreadExecutor, qasync._QThreadWorker): |
| 26 | + logger_name = cls.__qualname__ |
| 27 | + if cls.__module__ is not None: |
| 28 | + logger_name = f"{cls.__module__}.{logger_name}" |
| 29 | + logger = logging.getLogger(logger_name) |
| 30 | + logger.addHandler(logging.NullHandler()) |
| 31 | + logger.propagate = False |
| 32 | + |
| 33 | + |
10 | 34 | @pytest.fixture |
11 | 35 | def executor(request): |
12 | 36 | exe = qasync.QThreadExecutor(5) |
@@ -48,3 +72,36 @@ def rec(a, *args, **kwargs): |
48 | 72 | for f in fs: |
49 | 73 | with pytest.raises(RecursionError): |
50 | 74 | f.result() |
| 75 | + |
| 76 | + |
| 77 | +def test_no_stale_reference_as_argument(executor): |
| 78 | + test_obj = _TestObject() |
| 79 | + test_obj_collected = threading.Event() |
| 80 | + |
| 81 | + # Reference to weakref has to be kept for callback to work |
| 82 | + _ = weakref.ref(test_obj, lambda *_: test_obj_collected.set()) |
| 83 | + # Submit object as argument to the executor |
| 84 | + future = executor.submit(lambda *_: None, test_obj) |
| 85 | + del test_obj |
| 86 | + # Wait for future to resolve |
| 87 | + future.result() |
| 88 | + |
| 89 | + collected = test_obj_collected.wait(timeout=1) |
| 90 | + assert ( |
| 91 | + collected is True |
| 92 | + ), "Stale reference to executor argument not collected within timeout." |
| 93 | + |
| 94 | + |
| 95 | +def test_no_stale_reference_as_result(executor): |
| 96 | + # Get object as result out of executor |
| 97 | + test_obj = executor.submit(lambda: _TestObject()).result() |
| 98 | + test_obj_collected = threading.Event() |
| 99 | + |
| 100 | + # Reference to weakref has to be kept for callback to work |
| 101 | + _ = weakref.ref(test_obj, lambda *_: test_obj_collected.set()) |
| 102 | + del test_obj |
| 103 | + |
| 104 | + collected = test_obj_collected.wait(timeout=1) |
| 105 | + assert ( |
| 106 | + collected is True |
| 107 | + ), "Stale reference to executor result not collected within timeout." |
0 commit comments