1import gc 2import re 3import sys 4import textwrap 5import threading 6import types 7import unittest 8import weakref 9 10from test import support 11from test.support import threading_helper 12from test.support.script_helper import assert_python_ok 13 14 15class ClearTest(unittest.TestCase): 16 """ 17 Tests for frame.clear(). 18 """ 19 20 def inner(self, x=5, **kwargs): 21 1/0 22 23 def outer(self, **kwargs): 24 try: 25 self.inner(**kwargs) 26 except ZeroDivisionError as e: 27 exc = e 28 return exc 29 30 def clear_traceback_frames(self, tb): 31 """ 32 Clear all frames in a traceback. 33 """ 34 while tb is not None: 35 tb.tb_frame.clear() 36 tb = tb.tb_next 37 38 def test_clear_locals(self): 39 class C: 40 pass 41 c = C() 42 wr = weakref.ref(c) 43 exc = self.outer(c=c) 44 del c 45 support.gc_collect() 46 # A reference to c is held through the frames 47 self.assertIsNot(None, wr()) 48 self.clear_traceback_frames(exc.__traceback__) 49 support.gc_collect() 50 # The reference was released by .clear() 51 self.assertIs(None, wr()) 52 53 def test_clear_does_not_clear_specials(self): 54 class C: 55 pass 56 c = C() 57 exc = self.outer(c=c) 58 del c 59 f = exc.__traceback__.tb_frame 60 f.clear() 61 self.assertIsNot(f.f_code, None) 62 self.assertIsNot(f.f_locals, None) 63 self.assertIsNot(f.f_builtins, None) 64 self.assertIsNot(f.f_globals, None) 65 66 def test_clear_generator(self): 67 endly = False 68 def g(): 69 nonlocal endly 70 try: 71 yield 72 self.inner() 73 finally: 74 endly = True 75 gen = g() 76 next(gen) 77 self.assertFalse(endly) 78 # Clearing the frame closes the generator 79 gen.gi_frame.clear() 80 self.assertTrue(endly) 81 82 def test_clear_executing(self): 83 # Attempting to clear an executing frame is forbidden. 84 try: 85 1/0 86 except ZeroDivisionError as e: 87 f = e.__traceback__.tb_frame 88 with self.assertRaises(RuntimeError): 89 f.clear() 90 with self.assertRaises(RuntimeError): 91 f.f_back.clear() 92 93 def test_clear_executing_generator(self): 94 # Attempting to clear an executing generator frame is forbidden. 95 endly = False 96 def g(): 97 nonlocal endly 98 try: 99 1/0 100 except ZeroDivisionError as e: 101 f = e.__traceback__.tb_frame 102 with self.assertRaises(RuntimeError): 103 f.clear() 104 with self.assertRaises(RuntimeError): 105 f.f_back.clear() 106 yield f 107 finally: 108 endly = True 109 gen = g() 110 f = next(gen) 111 self.assertFalse(endly) 112 # Clearing the frame closes the generator 113 f.clear() 114 self.assertTrue(endly) 115 116 def test_lineno_with_tracing(self): 117 def record_line(): 118 f = sys._getframe(1) 119 lines.append(f.f_lineno-f.f_code.co_firstlineno) 120 121 def test(trace): 122 record_line() 123 if trace: 124 sys._getframe(0).f_trace = True 125 record_line() 126 record_line() 127 128 expected_lines = [1, 4, 5] 129 lines = [] 130 test(False) 131 self.assertEqual(lines, expected_lines) 132 lines = [] 133 test(True) 134 self.assertEqual(lines, expected_lines) 135 136 @support.cpython_only 137 def test_clear_refcycles(self): 138 # .clear() doesn't leave any refcycle behind 139 with support.disable_gc(): 140 class C: 141 pass 142 c = C() 143 wr = weakref.ref(c) 144 exc = self.outer(c=c) 145 del c 146 self.assertIsNot(None, wr()) 147 self.clear_traceback_frames(exc.__traceback__) 148 self.assertIs(None, wr()) 149 150 151class FrameAttrsTest(unittest.TestCase): 152 153 def make_frames(self): 154 def outer(): 155 x = 5 156 y = 6 157 def inner(): 158 z = x + 2 159 1/0 160 t = 9 161 return inner() 162 try: 163 outer() 164 except ZeroDivisionError as e: 165 tb = e.__traceback__ 166 frames = [] 167 while tb: 168 frames.append(tb.tb_frame) 169 tb = tb.tb_next 170 return frames 171 172 def test_locals(self): 173 f, outer, inner = self.make_frames() 174 outer_locals = outer.f_locals 175 self.assertIsInstance(outer_locals.pop('inner'), types.FunctionType) 176 self.assertEqual(outer_locals, {'x': 5, 'y': 6}) 177 inner_locals = inner.f_locals 178 self.assertEqual(inner_locals, {'x': 5, 'z': 7}) 179 180 def test_clear_locals(self): 181 # Test f_locals after clear() (issue #21897) 182 f, outer, inner = self.make_frames() 183 outer.clear() 184 inner.clear() 185 self.assertEqual(outer.f_locals, {}) 186 self.assertEqual(inner.f_locals, {}) 187 188 def test_locals_clear_locals(self): 189 # Test f_locals before and after clear() (to exercise caching) 190 f, outer, inner = self.make_frames() 191 outer.f_locals 192 inner.f_locals 193 outer.clear() 194 inner.clear() 195 self.assertEqual(outer.f_locals, {}) 196 self.assertEqual(inner.f_locals, {}) 197 198 def test_f_lineno_del_segfault(self): 199 f, _, _ = self.make_frames() 200 with self.assertRaises(AttributeError): 201 del f.f_lineno 202 203 204class ReprTest(unittest.TestCase): 205 """ 206 Tests for repr(frame). 207 """ 208 209 def test_repr(self): 210 def outer(): 211 x = 5 212 y = 6 213 def inner(): 214 z = x + 2 215 1/0 216 t = 9 217 return inner() 218 219 offset = outer.__code__.co_firstlineno 220 try: 221 outer() 222 except ZeroDivisionError as e: 223 tb = e.__traceback__ 224 frames = [] 225 while tb: 226 frames.append(tb.tb_frame) 227 tb = tb.tb_next 228 else: 229 self.fail("should have raised") 230 231 f_this, f_outer, f_inner = frames 232 file_repr = re.escape(repr(__file__)) 233 self.assertRegex(repr(f_this), 234 r"^<frame at 0x[0-9a-fA-F]+, file %s, line %d, code test_repr>$" 235 % (file_repr, offset + 23)) 236 self.assertRegex(repr(f_outer), 237 r"^<frame at 0x[0-9a-fA-F]+, file %s, line %d, code outer>$" 238 % (file_repr, offset + 7)) 239 self.assertRegex(repr(f_inner), 240 r"^<frame at 0x[0-9a-fA-F]+, file %s, line %d, code inner>$" 241 % (file_repr, offset + 5)) 242 243class TestIncompleteFrameAreInvisible(unittest.TestCase): 244 245 def test_issue95818(self): 246 # See GH-95818 for details 247 code = textwrap.dedent(f""" 248 import gc 249 250 gc.set_threshold(1,1,1) 251 class GCHello: 252 def __del__(self): 253 print("Destroyed from gc") 254 255 def gen(): 256 yield 257 258 fd = open({__file__!r}) 259 l = [fd, GCHello()] 260 l.append(l) 261 del fd 262 del l 263 gen() 264 """) 265 assert_python_ok("-c", code) 266 267 @support.cpython_only 268 def test_sneaky_frame_object(self): 269 270 def trace(frame, event, arg): 271 """ 272 Don't actually do anything, just force a frame object to be created. 273 """ 274 275 def callback(phase, info): 276 """ 277 Yo dawg, I heard you like frames, so I'm allocating a frame while 278 you're allocating a frame, so you can have a frame while you have a 279 frame! 280 """ 281 nonlocal sneaky_frame_object 282 sneaky_frame_object = sys._getframe().f_back 283 # We're done here: 284 gc.callbacks.remove(callback) 285 286 def f(): 287 while True: 288 yield 289 290 old_threshold = gc.get_threshold() 291 old_callbacks = gc.callbacks[:] 292 old_enabled = gc.isenabled() 293 old_trace = sys.gettrace() 294 try: 295 # Stop the GC for a second while we set things up: 296 gc.disable() 297 # Create a paused generator: 298 g = f() 299 next(g) 300 # Move all objects to the oldest generation, and tell the GC to run 301 # on the *very next* allocation: 302 gc.collect() 303 gc.set_threshold(1, 0, 0) 304 # Okay, so here's the nightmare scenario: 305 # - We're tracing the resumption of a generator, which creates a new 306 # frame object. 307 # - The allocation of this frame object triggers a collection 308 # *before* the frame object is actually created. 309 # - During the collection, we request the exact same frame object. 310 # This test does it with a GC callback, but in real code it would 311 # likely be a trace function, weakref callback, or finalizer. 312 # - The collection finishes, and the original frame object is 313 # created. We now have two frame objects fighting over ownership 314 # of the same interpreter frame! 315 sys.settrace(trace) 316 gc.callbacks.append(callback) 317 sneaky_frame_object = None 318 gc.enable() 319 next(g) 320 # g.gi_frame should be the the frame object from the callback (the 321 # one that was *requested* second, but *created* first): 322 self.assertIs(g.gi_frame, sneaky_frame_object) 323 finally: 324 gc.set_threshold(*old_threshold) 325 gc.callbacks[:] = old_callbacks 326 sys.settrace(old_trace) 327 if old_enabled: 328 gc.enable() 329 330 @support.cpython_only 331 @threading_helper.requires_working_threading() 332 def test_sneaky_frame_object_teardown(self): 333 334 class SneakyDel: 335 def __del__(self): 336 """ 337 Stash a reference to the entire stack for walking later. 338 339 It may look crazy, but you'd be surprised how common this is 340 when using a test runner (like pytest). The typical recipe is: 341 ResourceWarning + -Werror + a custom sys.unraisablehook. 342 """ 343 nonlocal sneaky_frame_object 344 sneaky_frame_object = sys._getframe() 345 346 class SneakyThread(threading.Thread): 347 """ 348 A separate thread isn't needed to make this code crash, but it does 349 make crashes more consistent, since it means sneaky_frame_object is 350 backed by freed memory after the thread completes! 351 """ 352 353 def run(self): 354 """Run SneakyDel.__del__ as this frame is popped.""" 355 ref = SneakyDel() 356 357 sneaky_frame_object = None 358 t = SneakyThread() 359 t.start() 360 t.join() 361 # sneaky_frame_object can be anything, really, but it's crucial that 362 # SneakyThread.run's frame isn't anywhere on the stack while it's being 363 # torn down: 364 self.assertIsNotNone(sneaky_frame_object) 365 while sneaky_frame_object is not None: 366 self.assertIsNot( 367 sneaky_frame_object.f_code, SneakyThread.run.__code__ 368 ) 369 sneaky_frame_object = sneaky_frame_object.f_back 370 371if __name__ == "__main__": 372 unittest.main() 373