@@ -1598,6 +1598,99 @@ def _get_awaited_by_relationships(self, stack_trace):
15981598
15991599
16001600
1601+ @skip_if_not_supported
1602+ @unittest .skipIf (
1603+ sys .platform == "linux" and not PROCESS_VM_READV_SUPPORTED ,
1604+ "Test only runs on Linux with process_vm_readv support" ,
1605+ )
1606+ class TestMetadataDegradation (RemoteInspectionTestBase ):
1607+ """Tests for graceful degradation of oversized code-object metadata."""
1608+
1609+ def _find_frame_in_trace (self , stack_trace , predicate ):
1610+ for thread_info in stack_trace :
1611+ for frame in thread_info .frame_info :
1612+ if predicate (frame ):
1613+ return frame
1614+ return None
1615+
1616+ def test_long_qualname_truncated_not_dropped (self ):
1617+ """A qualname longer than 1024 chars is truncated instead of
1618+ failing the whole sample."""
1619+ name = "f" * 1100
1620+ src = f"def { name } (sample):\n return sample()\n "
1621+ ns = {}
1622+ exec (src , ns )
1623+
1624+ trace = ns [name ](RemoteUnwinder (os .getpid ()).get_stack_trace )
1625+ frame = self ._find_frame_in_trace (
1626+ trace , lambda f : f .funcname .startswith ("fff" )
1627+ )
1628+ self .assertIsNotNone (frame )
1629+ self .assertEqual (frame .funcname , "f" * 1024 )
1630+
1631+ def test_long_filename_truncated (self ):
1632+ """A filename longer than 1024 chars is truncated instead of
1633+ failing the whole sample."""
1634+ src = "def g(sample):\n return sample()\n "
1635+ ns = {}
1636+ exec (compile (src , "x" * 1500 + ".py" , "exec" ), ns )
1637+
1638+ trace = ns ["g" ](RemoteUnwinder (os .getpid ()).get_stack_trace )
1639+ frame = self ._find_frame_in_trace (trace , lambda f : f .funcname == "g" )
1640+ self .assertIsNotNone (frame )
1641+ self .assertEqual (frame .filename , "x" * 1024 )
1642+
1643+ def test_oversized_linetable_degrades_to_no_location (self ):
1644+ """A linetable over the 4096-byte limit degrades to a frame without
1645+ location instead of failing the whole sample."""
1646+ src = (
1647+ "def big(sample):\n "
1648+ + " x = 1\n " * 1200
1649+ + " return sample()\n "
1650+ )
1651+ ns = {}
1652+ exec (compile (src , "big_linetable.py" , "exec" ), ns )
1653+ big = ns ["big" ]
1654+ self .assertGreater (len (big .__code__ .co_linetable ), 4096 )
1655+
1656+ trace = big (RemoteUnwinder (os .getpid ()).get_stack_trace )
1657+ frame = self ._find_frame_in_trace (
1658+ trace , lambda f : f .funcname == "big"
1659+ )
1660+ self .assertIsNone (frame .lineno )
1661+ self .assertEqual (frame .filename , "big_linetable.py" )
1662+
1663+ @unittest .skipUnless (
1664+ sys .platform == "linux" ,
1665+ "Process death maps to ProcessLookupError only on Linux in 3.14" ,
1666+ )
1667+ def test_dead_process_raises_not_degrades (self ):
1668+ """Death of the target raises ProcessLookupError instead of
1669+ degrading to synthetic frames."""
1670+ script_body = """\
1671+ import time
1672+ sock.sendall(b"ready")
1673+ time.sleep(10_000)
1674+ """
1675+ with self ._target_process (script_body ) as (p , client_socket , make_unwinder ):
1676+ _wait_for_signal (client_socket , b"ready" )
1677+ unwinder = make_unwinder ()
1678+ unwinder .get_stack_trace ()
1679+
1680+ p .kill ()
1681+ p .wait ()
1682+
1683+ for _ in busy_retry (SHORT_TIMEOUT , error = False ):
1684+ try :
1685+ unwinder .get_stack_trace ()
1686+ except ProcessLookupError :
1687+ break
1688+ except RuntimeError :
1689+ continue
1690+ else :
1691+ self .fail ("ProcessLookupError never raised for dead process" )
1692+
1693+
16011694class TestFrameChainLimits (RemoteInspectionTestBase ):
16021695 """Frame chain walks abort instead of looping/overflowing on deep chains."""
16031696
0 commit comments