@@ -161,59 +161,71 @@ def delay(phase, _info):
161
161
gc .callbacks .remove (delay )
162
162
163
163
164
- @pytest .mark .skipif (not hasattr (gc , "callbacks" ), reason = "CPython specific " )
165
- def test_gc_hooks_do_not_fail_due_to_recursionerror ():
164
+ @pytest .mark .skipif (not hasattr (gc , "callbacks" ), reason = "PyPy has weird stack-limit behaviour " )
165
+ def test_gc_hooks_do_not_cause_unraisable_recursionerror ():
166
166
# We were concerned in #3979 that we might see bad results from a RecursionError
167
167
# inside the GC hook, if the stack was already deep and someone (e.g. Pytest)
168
- # had installed a sys.unraisablehook which raises that later. Even if there's
169
- # no such hook, we'd get the measured time wrong, so we set that to NaN.
168
+ # had installed a sys.unraisablehook which raises that later.
169
+
170
+ NUM_CYCLES = 10_000
170
171
171
172
def probe_depth ():
172
173
try :
173
174
return probe_depth () + 1
174
175
except RecursionError :
175
176
return 0
176
177
177
- collections = 0
178
-
179
- def collect_at (depth ):
180
- if depth == 0 :
181
- # We are at the recursion limit, no free stack frames. Generate reference
182
- # cycles until a background collection is observed. Beware: there is not
183
- # even room for raising new exceptions here.
184
- orig_collections = collections
185
- n_iter = 0
186
- while n_iter < 100_000 :
187
- if collections > orig_collections :
188
- return True # successfully triggered collection at stack limit
189
- a = [None ]
190
- b = [a ]
191
- a [0 ] = b
192
- n_iter += 1
193
- return False
178
+ def at_depth (depth , fn ):
179
+ if depth <= 1 :
180
+ return fn ()
194
181
else :
195
- return collect_at (depth - 1 )
182
+ # Recurse towards requested depth
183
+ return at_depth (depth - 1 , fn )
184
+
185
+ def gen_cycles ():
186
+ # We may be at the recursion limit, no free stack frames. Generate lots
187
+ # of reference cycles. Beware: there may not even be room for raising new
188
+ # exceptions here, anything will end up as a RecursionError.
189
+ for i in range (NUM_CYCLES ):
190
+ a = [None ]
191
+ b = [a ]
192
+ a [0 ] = b
193
+
194
+ def gen_cycles_at_depth (depth , * , gc_disable ):
195
+ try :
196
+ if gc_disable :
197
+ gc .disable ()
198
+ at_depth (depth , gen_cycles )
199
+ if gc_disable :
200
+ assert gc .collect () >= 2 * NUM_CYCLES
201
+ else :
202
+ assert gc .collect () < 2 * NUM_CYCLES # collection was triggered
203
+ finally :
204
+ gc .enable ()
196
205
197
206
@given (st .booleans ())
198
207
def inner_test (_ ):
199
- max_depth = probe_depth ()
200
- assert collect_at (max_depth )
208
+ max_depth = probe_depth () # would have to be called twice on PyPy
209
+
210
+ # Lower the limit to where we can successfully generate cycles
211
+ while True :
212
+ try :
213
+ gen_cycles_at_depth (max_depth , gc_disable = True )
214
+ except RecursionError :
215
+ max_depth -= 1
216
+ else :
217
+ break
201
218
202
- observations = []
203
- try :
204
- TESTCASE_CALLBACKS .append (observations .append )
205
- def gc_count_collections (* _ ):
206
- nonlocal collections
207
- collections += 1
208
- gc .callbacks .append (gc_count_collections )
209
- inner_test ()
210
- finally :
211
- gc .callbacks .remove (gc_count_collections )
212
- popped = TESTCASE_CALLBACKS .pop ()
213
- assert popped == observations .append , (popped , observations .append )
219
+ # Verify limits w/o any gc interfering
220
+ gen_cycles_at_depth (max_depth - 1 , gc_disable = True ) # this line raises RecursionError on Pypy
221
+ gen_cycles_at_depth (max_depth , gc_disable = True ) # while this line doesn't
222
+ with pytest .raises (RecursionError ):
223
+ gen_cycles_at_depth (max_depth + 1 , gc_disable = True )
214
224
215
- timings = [t .get ("timing" , {}).get ("overall:gc" , 0.0 ) for t in observations ]
216
- assert any (math .isnan (v ) for v in timings ), timings
225
+ # Check that the limit is unchanged with gc enabled
226
+ gen_cycles_at_depth (max_depth - 1 , gc_disable = False )
227
+ gen_cycles_at_depth (max_depth , gc_disable = False )
228
+ with pytest .raises (RecursionError ):
229
+ gen_cycles_at_depth (max_depth + 1 , gc_disable = False )
217
230
218
- gc .collect () # should clear any NaN state
219
- assert not math .isnan (junkdrawer .gc_cumulative_time ())
231
+ inner_test ()
0 commit comments