|
8 | 8 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can
|
9 | 9 | # obtain one at https://mozilla.org/MPL/2.0/.
|
10 | 10 |
|
| 11 | +import gc |
11 | 12 | import sys
|
12 | 13 | import threading
|
13 | 14 | import warnings
|
14 | 15 |
|
15 | 16 | from hypothesis import HealthCheck, given, settings, strategies as st
|
| 17 | +import pytest |
16 | 18 |
|
17 | 19 | from tests.common.debug import find_any, minimal
|
18 | 20 | from tests.common.utils import flaky
|
@@ -182,3 +184,82 @@ def test(data):
|
182 | 184 | def test_self_ref_regression(_):
|
183 | 185 | # See https://github.com/HypothesisWorks/hypothesis/issues/2794
|
184 | 186 | pass
|
| 187 | + |
| 188 | + |
| 189 | +@flaky(min_passes=1, max_runs=2) |
| 190 | +def test_gc_hooks_do_not_cause_unraisable_recursionerror(): |
| 191 | + # We were concerned in #3979 that we might see bad results from a RecursionError |
| 192 | + # inside the GC hook, if the stack was already deep and someone (e.g. Pytest) |
| 193 | + # had installed a sys.unraisablehook which raises that later. |
| 194 | + |
| 195 | + # This test is potentially flaky, because the stack usage of a function is not |
| 196 | + # constant. Regardless, if the test passes just once that's sufficient proof that |
| 197 | + # it's not the GC (or accounting of it) that is at fault. |
| 198 | + |
| 199 | + NUM_CYCLES = 10_000 |
| 200 | + |
| 201 | + def probe_depth(): |
| 202 | + try: |
| 203 | + return probe_depth() + 1 |
| 204 | + except RecursionError: |
| 205 | + return 0 |
| 206 | + |
| 207 | + def at_depth(depth, fn): |
| 208 | + if depth <= 1: |
| 209 | + return fn() |
| 210 | + else: |
| 211 | + # Recurse towards requested depth |
| 212 | + return at_depth(depth - 1, fn) |
| 213 | + |
| 214 | + def gen_cycles(): |
| 215 | + # We may be at the recursion limit, no free stack frames. Generate lots |
| 216 | + # of reference cycles, to trigger GC (if enabled). Beware: there may not |
| 217 | + # even be room for raising new exceptions here, anything will end up as |
| 218 | + # a RecursionError. |
| 219 | + for i in range(NUM_CYCLES): |
| 220 | + a = [None] |
| 221 | + b = [a] |
| 222 | + a[0] = b |
| 223 | + |
| 224 | + def gen_cycles_at_depth(depth, *, gc_disable): |
| 225 | + try: |
| 226 | + if gc_disable: |
| 227 | + gc.disable() |
| 228 | + at_depth(depth, gen_cycles) |
| 229 | + dead_objects = gc.collect() |
| 230 | + if dead_objects is not None: # == None on PyPy |
| 231 | + if gc_disable: |
| 232 | + assert dead_objects >= 2 * NUM_CYCLES |
| 233 | + else: |
| 234 | + assert dead_objects < 2 * NUM_CYCLES # collection was triggered |
| 235 | + finally: |
| 236 | + gc.enable() |
| 237 | + |
| 238 | + @given(st.booleans()) |
| 239 | + def inner_test(_): |
| 240 | + max_depth = probe_depth() |
| 241 | + max_depth = probe_depth() # Executing probe twice de-flakes PyPy |
| 242 | + |
| 243 | + while True: |
| 244 | + # Lower the limit to where we can successfully generate cycles |
| 245 | + try: |
| 246 | + gen_cycles_at_depth(max_depth, gc_disable=True) |
| 247 | + except RecursionError: |
| 248 | + max_depth -= 1 |
| 249 | + else: |
| 250 | + break |
| 251 | + |
| 252 | + # Verify limits w/o any gc interfering |
| 253 | + |
| 254 | + # gen_cycles_at_depth(max_depth - 1, gc_disable=True) # RecursionError on PyPy (!) |
| 255 | + gen_cycles_at_depth(max_depth, gc_disable=True) |
| 256 | + with pytest.raises(RecursionError): |
| 257 | + gen_cycles_at_depth(max_depth + 1, gc_disable=True) |
| 258 | + |
| 259 | + # Check that the limit is unchanged with gc enabled |
| 260 | + |
| 261 | + gen_cycles_at_depth(max_depth, gc_disable=False) |
| 262 | + with pytest.raises(RecursionError): |
| 263 | + gen_cycles_at_depth(max_depth + 1, gc_disable=False) |
| 264 | + |
| 265 | + inner_test() |
0 commit comments