python (3.11.7)

(root)/
lib/
python3.11/
test/
test_frame.py
       1  import gc
       2  import re
       3  import sys
       4  import textwrap
       5  import threading
       6  import types
       7  import unittest
       8  import weakref
       9  
      10  from test import support
      11  from test.support import threading_helper
      12  from test.support.script_helper import assert_python_ok
      13  
      14  
      15  class ESC[4;38;5;81mClearTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
      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 ESC[4;38;5;81mC:
      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 ESC[4;38;5;81mC:
      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 ESC[4;38;5;81mC:
     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  
     151  class ESC[4;38;5;81mFrameAttrsTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
     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  
     204  class ESC[4;38;5;81mReprTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
     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  
     243  class ESC[4;38;5;81mTestIncompleteFrameAreInvisible(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
     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 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 ESC[4;38;5;81mSneakyDel:
     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 ESC[4;38;5;81mSneakyThread(ESC[4;38;5;149mthreadingESC[4;38;5;149m.ESC[4;38;5;149mThread):
     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  
     371  if __name__ == "__main__":
     372      unittest.main()