@@ -2131,7 +2131,12 @@ def _sample_frames(self, client_socket, unwinder, wait_signal, send_ack, require
21312131 "Test only runs on Linux with process_vm_readv support" ,
21322132 )
21332133 def test_cache_hit_same_stack (self ):
2134- """Test that 3 consecutive samples reuse cached frame objects (identity check)."""
2134+ """Test that consecutive samples reuse cached parent frame objects.
2135+
2136+ The current frame (index 0) is always re-read from memory to get
2137+ updated line numbers, so it may be a different object. Parent frames
2138+ (index 1+) should be identical objects from cache.
2139+ """
21352140 script_body = """\
21362141 def level3():
21372142 sock.sendall(b"sync1")
@@ -2164,11 +2169,71 @@ def level1():
21642169 self .assertEqual (len (frames1 ), len (frames2 ))
21652170 self .assertEqual (len (frames2 ), len (frames3 ))
21662171
2167- # All frames must be identical objects (cache reuse)
2168- for i , (f1 , f2 , f3 ) in enumerate (zip (frames1 , frames2 , frames3 )):
2172+ # Current frame (index 0) is always re-read, so check value equality
2173+ self .assertEqual (frames1 [0 ].funcname , frames2 [0 ].funcname )
2174+ self .assertEqual (frames2 [0 ].funcname , frames3 [0 ].funcname )
2175+
2176+ # Parent frames (index 1+) must be identical objects (cache reuse)
2177+ for i in range (1 , len (frames1 )):
2178+ f1 , f2 , f3 = frames1 [i ], frames2 [i ], frames3 [i ]
21692179 self .assertIs (f1 , f2 , f"Frame { i } : samples 1-2 must be same object" )
21702180 self .assertIs (f2 , f3 , f"Frame { i } : samples 2-3 must be same object" )
2171- self .assertIs (f1 , f3 , f"Frame { i } : samples 1-3 must be same object" )
2181+
2182+ @skip_if_not_supported
2183+ @unittest .skipIf (
2184+ sys .platform == "linux" and not PROCESS_VM_READV_SUPPORTED ,
2185+ "Test only runs on Linux with process_vm_readv support" ,
2186+ )
2187+ def test_line_number_updates_in_same_frame (self ):
2188+ """Test that line numbers are correctly updated when execution moves within a function.
2189+
2190+ When the profiler samples at different points within the same function,
2191+ it must report the correct line number for each sample, not stale cached values.
2192+ """
2193+ script_body = """\
2194+ def outer():
2195+ inner()
2196+
2197+ def inner():
2198+ sock.sendall(b"line_a"); sock.recv(16)
2199+ sock.sendall(b"line_b"); sock.recv(16)
2200+ sock.sendall(b"line_c"); sock.recv(16)
2201+ sock.sendall(b"line_d"); sock.recv(16)
2202+
2203+ outer()
2204+ """
2205+
2206+ with self ._target_process (script_body ) as (p , client_socket , make_unwinder ):
2207+ unwinder = make_unwinder (cache_frames = True )
2208+
2209+ frames_a = self ._sample_frames (client_socket , unwinder , b"line_a" , b"ack" , {"inner" })
2210+ frames_b = self ._sample_frames (client_socket , unwinder , b"line_b" , b"ack" , {"inner" })
2211+ frames_c = self ._sample_frames (client_socket , unwinder , b"line_c" , b"ack" , {"inner" })
2212+ frames_d = self ._sample_frames (client_socket , unwinder , b"line_d" , b"done" , {"inner" })
2213+
2214+ self .assertIsNotNone (frames_a )
2215+ self .assertIsNotNone (frames_b )
2216+ self .assertIsNotNone (frames_c )
2217+ self .assertIsNotNone (frames_d )
2218+
2219+ # Get the 'inner' frame from each sample (should be index 0)
2220+ inner_a = frames_a [0 ]
2221+ inner_b = frames_b [0 ]
2222+ inner_c = frames_c [0 ]
2223+ inner_d = frames_d [0 ]
2224+
2225+ self .assertEqual (inner_a .funcname , "inner" )
2226+ self .assertEqual (inner_b .funcname , "inner" )
2227+ self .assertEqual (inner_c .funcname , "inner" )
2228+ self .assertEqual (inner_d .funcname , "inner" )
2229+
2230+ # Line numbers must be different and increasing (execution moves forward)
2231+ self .assertLess (inner_a .lineno , inner_b .lineno ,
2232+ "Line B should be after line A" )
2233+ self .assertLess (inner_b .lineno , inner_c .lineno ,
2234+ "Line C should be after line B" )
2235+ self .assertLess (inner_c .lineno , inner_d .lineno ,
2236+ "Line D should be after line C" )
21722237
21732238 @skip_if_not_supported
21742239 @unittest .skipIf (
@@ -2351,9 +2416,13 @@ def recurse(n):
23512416
23522417 self .assertEqual (len (frames1 ), len (frames2 ))
23532418
2354- # All frames should be identical objects (cache reuse)
2355- for i , (f1 , f2 ) in enumerate (zip (frames1 , frames2 )):
2356- self .assertIs (f1 , f2 , f"Frame { i } : recursive frames must be same object" )
2419+ # Current frame (index 0) is re-read, check value equality
2420+ self .assertEqual (frames1 [0 ].funcname , frames2 [0 ].funcname )
2421+
2422+ # Parent frames (index 1+) should be identical objects (cache reuse)
2423+ for i in range (1 , len (frames1 )):
2424+ self .assertIs (frames1 [i ], frames2 [i ],
2425+ f"Frame { i } : recursive frames must be same object" )
23572426
23582427 @skip_if_not_supported
23592428 @unittest .skipIf (
0 commit comments