|
51 | 51 | import importlib.util |
52 | 52 | import io |
53 | 53 | import os |
| 54 | +import sys |
| 55 | +import time |
54 | 56 | import unittest |
| 57 | +from typing import TYPE_CHECKING |
55 | 58 | from unittest import mock |
56 | 59 |
|
57 | 60 | from tests._support import sample_path |
58 | 61 |
|
| 62 | +if TYPE_CHECKING: |
| 63 | + from typing import Callable |
| 64 | + |
59 | 65 | #: Test tier (#1958): depends on garbage collection, weak references or object |
60 | 66 | #: finalisation. |
61 | 67 | __tier__ = 'ALL' |
|
67 | 73 | #: Linux; the :func:`builtins.open` measurement below covers the rest. |
68 | 74 | HAS_PROC_FD = os.path.isdir('/proc/self/fd') |
69 | 75 |
|
| 76 | +#: Seconds :func:`collect_until` keeps collecting for a finaliser after the first |
| 77 | +#: :func:`gc.collect`. CPython runs the finaliser before ``collect`` returns, so it |
| 78 | +#: gets no grace. GraalPy's ``collect`` forces a full Java GC, sleeps 15 ms "just |
| 79 | +#: trying to give the GC more time" and then runs only the weak reference |
| 80 | +#: callbacks queued by then (graalpython ``GcModuleBuiltins.javaCollect``, at |
| 81 | +#: graal-25.4.4), so on a loaded runner the finaliser can run after it returns |
| 82 | +#: (#1963). A finaliser that never runs still fails once the grace is spent. |
| 83 | +FINALISER_GRACE = 10.0 if sys.implementation.name == 'graalpy' else 0.0 |
| 84 | + |
| 85 | + |
| 86 | +def collect_until(done: 'Callable[[], bool]', grace: float = FINALISER_GRACE) -> bool: |
| 87 | + """Collect garbage until ``done()`` holds or ``grace`` seconds have passed. |
| 88 | +
|
| 89 | + Args: |
| 90 | + done: Zero-argument callable, checked after each collection. |
| 91 | + grace: Seconds to keep collecting after the first collection. |
| 92 | +
|
| 93 | + Returns: |
| 94 | + Whether ``done()`` held before the grace ran out. |
| 95 | +
|
| 96 | + """ |
| 97 | + deadline = time.monotonic() + grace |
| 98 | + while True: |
| 99 | + gc.collect() |
| 100 | + if done(): |
| 101 | + return True |
| 102 | + if time.monotonic() >= deadline: |
| 103 | + return False |
| 104 | + time.sleep(0.05) |
| 105 | + |
70 | 106 |
|
71 | 107 | def fd_count(target: str) -> int: |
72 | 108 | """How many of this process's open descriptors resolve to ``target``. |
@@ -230,7 +266,7 @@ def test_extractor_abandoned_before_eof_still_releases_its_handle(self) -> None: |
230 | 266 | 'open at this point') |
231 | 267 |
|
232 | 268 | del extractor |
233 | | - gc.collect() |
| 269 | + collect_until(lambda: recorder.still_open == 0) |
234 | 270 |
|
235 | 271 | with self.subTest(measurement='builtins.open handles still open'): |
236 | 272 | self.assertEqual(recorder.still_open, 0, |
@@ -398,11 +434,30 @@ def test_finaliser_alone_releases_an_abandoned_handle(self) -> None: |
398 | 434 | self.assertEqual(recorder.still_open, 1) |
399 | 435 |
|
400 | 436 | del extractor |
401 | | - gc.collect() |
| 437 | + collect_until(lambda: recorder.still_open == 0) |
402 | 438 |
|
403 | 439 | self.assertEqual(recorder.still_open, 0, |
404 | 440 | 'without __del__ the handle outlived the extractor') |
405 | 441 |
|
| 442 | + def test_collect_until_waits_out_a_finaliser_that_runs_late(self) -> None: |
| 443 | + """A finaliser run after :func:`gc.collect` returns, as GraalPy's can be (:issue:`1963`). |
| 444 | +
|
| 445 | + The stand-in finaliser runs on the third collection. With no grace the |
| 446 | + helper reports it as never run, the shape of the GraalPy leg's failure; with a |
| 447 | + grace it waits it out. |
| 448 | +
|
| 449 | + """ |
| 450 | + collections = [] |
| 451 | + |
| 452 | + def late() -> bool: |
| 453 | + collections.append(None) |
| 454 | + return len(collections) >= 3 |
| 455 | + |
| 456 | + self.assertFalse(collect_until(late, grace=0.0)) |
| 457 | + collections.clear() |
| 458 | + self.assertTrue(collect_until(late, grace=5.0)) |
| 459 | + self.assertEqual(len(collections), 3) |
| 460 | + |
406 | 461 | def test_finished_extraction_detaches_its_finaliser(self) -> None: |
407 | 462 | """``_cleanup`` and the context manager unregister it, so nothing closes twice.""" |
408 | 463 | import pcapkit |
|
0 commit comments