Skip to content

Commit 55fd562

Browse files
committed
Move recursion validation test to nocover
1 parent f98320f commit 55fd562

File tree

2 files changed

+81
-73
lines changed

2 files changed

+81
-73
lines changed

hypothesis-python/tests/cover/test_deadline.py

-73
Original file line numberDiff line numberDiff line change
@@ -155,76 +155,3 @@ def delay(phase, _info):
155155
test()
156156
finally:
157157
gc.callbacks.remove(delay)
158-
159-
160-
@pytest.mark.skipif(
161-
not hasattr(gc, "callbacks"), reason="PyPy has weird stack-limit behaviour"
162-
)
163-
def test_gc_hooks_do_not_cause_unraisable_recursionerror():
164-
# We were concerned in #3979 that we might see bad results from a RecursionError
165-
# inside the GC hook, if the stack was already deep and someone (e.g. Pytest)
166-
# had installed a sys.unraisablehook which raises that later.
167-
168-
NUM_CYCLES = 10_000
169-
170-
def probe_depth():
171-
try:
172-
return probe_depth() + 1
173-
except RecursionError:
174-
return 0
175-
176-
def at_depth(depth, fn):
177-
if depth <= 1:
178-
return fn()
179-
else:
180-
# Recurse towards requested depth
181-
return at_depth(depth - 1, fn)
182-
183-
def gen_cycles():
184-
# We may be at the recursion limit, no free stack frames. Generate lots
185-
# of reference cycles. Beware: there may not even be room for raising new
186-
# exceptions here, anything will end up as a RecursionError.
187-
for i in range(NUM_CYCLES):
188-
a = [None]
189-
b = [a]
190-
a[0] = b
191-
192-
def gen_cycles_at_depth(depth, *, gc_disable):
193-
try:
194-
if gc_disable:
195-
gc.disable()
196-
at_depth(depth, gen_cycles)
197-
if gc_disable:
198-
assert gc.collect() >= 2 * NUM_CYCLES
199-
else:
200-
assert gc.collect() < 2 * NUM_CYCLES # collection was triggered
201-
finally:
202-
gc.enable()
203-
204-
@given(st.booleans())
205-
def inner_test(_):
206-
max_depth = probe_depth() # would have to be called twice on PyPy
207-
208-
# Lower the limit to where we can successfully generate cycles
209-
while True:
210-
try:
211-
gen_cycles_at_depth(max_depth, gc_disable=True)
212-
except RecursionError:
213-
max_depth -= 1
214-
else:
215-
break
216-
217-
# Verify limits w/o any gc interfering
218-
gen_cycles_at_depth(max_depth - 1, gc_disable=True)
219-
# the previous line raises RecursionError on Pypy, while the next doesn't (!)
220-
gen_cycles_at_depth(max_depth, gc_disable=True)
221-
with pytest.raises(RecursionError):
222-
gen_cycles_at_depth(max_depth + 1, gc_disable=True)
223-
224-
# Check that the limit is unchanged with gc enabled
225-
gen_cycles_at_depth(max_depth - 1, gc_disable=False)
226-
gen_cycles_at_depth(max_depth, gc_disable=False)
227-
with pytest.raises(RecursionError):
228-
gen_cycles_at_depth(max_depth + 1, gc_disable=False)
229-
230-
inner_test()

hypothesis-python/tests/nocover/test_recursive.py

+81
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
99
# obtain one at https://mozilla.org/MPL/2.0/.
1010

11+
import gc
1112
import sys
1213
import threading
1314
import warnings
1415

1516
from hypothesis import HealthCheck, given, settings, strategies as st
17+
import pytest
1618

1719
from tests.common.debug import find_any, minimal
1820
from tests.common.utils import flaky
@@ -182,3 +184,82 @@ def test(data):
182184
def test_self_ref_regression(_):
183185
# See https://github.com/HypothesisWorks/hypothesis/issues/2794
184186
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

Comments
 (0)