2929 )
3030
3131
32- def make_frame (filename , lineno , funcname ):
33- """Create a FrameInfo struct sequence."""
34- location = LocationInfo ((lineno , lineno , - 1 , - 1 ))
35- return FrameInfo ((filename , location , funcname , None ))
32+ def make_frame (filename , lineno , funcname , end_lineno = None , column = None ,
33+ end_column = None , opcode = None ):
34+ """Create a FrameInfo struct sequence with full location info and opcode."""
35+ if end_lineno is None :
36+ end_lineno = lineno
37+ if column is None :
38+ column = 0
39+ if end_column is None :
40+ end_column = 0
41+ location = LocationInfo ((lineno , end_lineno , column , end_column ))
42+ return FrameInfo ((filename , location , funcname , opcode ))
3643
3744
3845def make_thread (thread_id , frames , status = 0 ):
@@ -54,6 +61,22 @@ def extract_lineno(location):
5461 return location
5562
5663
64+ def extract_location (location ):
65+ """Extract full location info as dict from location tuple or None."""
66+ if location is None :
67+ return {"lineno" : 0 , "end_lineno" : 0 , "column" : 0 , "end_column" : 0 }
68+ if isinstance (location , tuple ) and len (location ) >= 4 :
69+ return {
70+ "lineno" : location [0 ] if location [0 ] is not None else 0 ,
71+ "end_lineno" : location [1 ] if location [1 ] is not None else 0 ,
72+ "column" : location [2 ] if location [2 ] is not None else 0 ,
73+ "end_column" : location [3 ] if location [3 ] is not None else 0 ,
74+ }
75+ # Fallback for old-style location
76+ lineno = location [0 ] if isinstance (location , tuple ) else location
77+ return {"lineno" : lineno or 0 , "end_lineno" : lineno or 0 , "column" : 0 , "end_column" : 0 }
78+
79+
5780class RawCollector :
5881 """Collector that captures all raw data grouped by thread."""
5982
@@ -70,11 +93,16 @@ def collect(self, stack_frames, timestamps_us):
7093 for thread in interp .threads :
7194 frames = []
7295 for frame in thread .frame_info :
96+ loc = extract_location (frame .location )
7397 frames .append (
7498 {
7599 "filename" : frame .filename ,
76100 "funcname" : frame .funcname ,
77- "lineno" : extract_lineno (frame .location ),
101+ "lineno" : loc ["lineno" ],
102+ "end_lineno" : loc ["end_lineno" ],
103+ "column" : loc ["column" ],
104+ "end_column" : loc ["end_column" ],
105+ "opcode" : frame .opcode ,
78106 }
79107 )
80108 key = (interp .interpreter_id , thread .thread_id )
@@ -95,11 +123,16 @@ def samples_to_by_thread(samples):
95123 for thread in interp .threads :
96124 frames = []
97125 for frame in thread .frame_info :
126+ loc = extract_location (frame .location )
98127 frames .append (
99128 {
100129 "filename" : frame .filename ,
101130 "funcname" : frame .funcname ,
102- "lineno" : extract_lineno (frame .location ),
131+ "lineno" : loc ["lineno" ],
132+ "end_lineno" : loc ["end_lineno" ],
133+ "column" : loc ["column" ],
134+ "end_column" : loc ["end_column" ],
135+ "opcode" : frame .opcode ,
103136 }
104137 )
105138 key = (interp .interpreter_id , thread .thread_id )
@@ -206,6 +239,34 @@ def assert_samples_equal(self, expected_samples, collector):
206239 f"frame { j } : lineno mismatch "
207240 f"(expected { exp_frame ['lineno' ]} , got { act_frame ['lineno' ]} )" ,
208241 )
242+ self .assertEqual (
243+ exp_frame ["end_lineno" ],
244+ act_frame ["end_lineno" ],
245+ f"Thread ({ interp_id } , { thread_id } ), sample { i } , "
246+ f"frame { j } : end_lineno mismatch "
247+ f"(expected { exp_frame ['end_lineno' ]} , got { act_frame ['end_lineno' ]} )" ,
248+ )
249+ self .assertEqual (
250+ exp_frame ["column" ],
251+ act_frame ["column" ],
252+ f"Thread ({ interp_id } , { thread_id } ), sample { i } , "
253+ f"frame { j } : column mismatch "
254+ f"(expected { exp_frame ['column' ]} , got { act_frame ['column' ]} )" ,
255+ )
256+ self .assertEqual (
257+ exp_frame ["end_column" ],
258+ act_frame ["end_column" ],
259+ f"Thread ({ interp_id } , { thread_id } ), sample { i } , "
260+ f"frame { j } : end_column mismatch "
261+ f"(expected { exp_frame ['end_column' ]} , got { act_frame ['end_column' ]} )" ,
262+ )
263+ self .assertEqual (
264+ exp_frame ["opcode" ],
265+ act_frame ["opcode" ],
266+ f"Thread ({ interp_id } , { thread_id } ), sample { i } , "
267+ f"frame { j } : opcode mismatch "
268+ f"(expected { exp_frame ['opcode' ]} , got { act_frame ['opcode' ]} )" ,
269+ )
209270
210271
211272class TestBinaryRoundTrip (BinaryFormatTestBase ):
@@ -484,6 +545,97 @@ def test_threads_interleaved_samples(self):
484545 self .assertEqual (count , 60 )
485546 self .assert_samples_equal (samples , collector )
486547
548+ def test_full_location_roundtrip (self ):
549+ """Full source location (end_lineno, column, end_column) roundtrips."""
550+ frames = [
551+ make_frame ("test.py" , 10 , "func1" , end_lineno = 12 , column = 4 , end_column = 20 ),
552+ make_frame ("test.py" , 20 , "func2" , end_lineno = 20 , column = 8 , end_column = 45 ),
553+ make_frame ("test.py" , 30 , "func3" , end_lineno = 35 , column = 0 , end_column = 100 ),
554+ ]
555+ samples = [[make_interpreter (0 , [make_thread (1 , frames )])]]
556+ collector , count = self .roundtrip (samples )
557+ self .assertEqual (count , 1 )
558+ self .assert_samples_equal (samples , collector )
559+
560+ def test_opcode_roundtrip (self ):
561+ """Opcode values roundtrip exactly."""
562+ opcodes = [0 , 1 , 50 , 100 , 150 , 200 , 254 ] # Valid Python opcodes
563+ samples = []
564+ for opcode in opcodes :
565+ frame = make_frame ("test.py" , 10 , "func" , opcode = opcode )
566+ samples .append ([make_interpreter (0 , [make_thread (1 , [frame ])])])
567+ collector , count = self .roundtrip (samples )
568+ self .assertEqual (count , len (opcodes ))
569+ self .assert_samples_equal (samples , collector )
570+
571+ def test_opcode_none_roundtrip (self ):
572+ """Opcode=None (sentinel 255) roundtrips as None."""
573+ frame = make_frame ("test.py" , 10 , "func" , opcode = None )
574+ samples = [[make_interpreter (0 , [make_thread (1 , [frame ])])]]
575+ collector , count = self .roundtrip (samples )
576+ self .assertEqual (count , 1 )
577+ self .assert_samples_equal (samples , collector )
578+
579+ def test_mixed_location_and_opcode (self ):
580+ """Mixed full location and opcode data roundtrips."""
581+ frames = [
582+ make_frame ("a.py" , 10 , "a" , end_lineno = 15 , column = 4 , end_column = 30 , opcode = 100 ),
583+ make_frame ("b.py" , 20 , "b" , end_lineno = 20 , column = 0 , end_column = 50 , opcode = None ),
584+ make_frame ("c.py" , 30 , "c" , end_lineno = 32 , column = 8 , end_column = 25 , opcode = 50 ),
585+ ]
586+ samples = [[make_interpreter (0 , [make_thread (1 , frames )])]]
587+ collector , count = self .roundtrip (samples )
588+ self .assertEqual (count , 1 )
589+ self .assert_samples_equal (samples , collector )
590+
591+ def test_delta_encoding_multiline (self ):
592+ """Multi-line spans (large end_lineno delta) roundtrip correctly."""
593+ # This tests the delta encoding: end_lineno = lineno + delta
594+ frames = [
595+ make_frame ("test.py" , 1 , "small" , end_lineno = 1 , column = 0 , end_column = 10 ),
596+ make_frame ("test.py" , 100 , "medium" , end_lineno = 110 , column = 0 , end_column = 50 ),
597+ make_frame ("test.py" , 1000 , "large" , end_lineno = 1500 , column = 0 , end_column = 200 ),
598+ ]
599+ samples = [[make_interpreter (0 , [make_thread (1 , frames )])]]
600+ collector , count = self .roundtrip (samples )
601+ self .assertEqual (count , 1 )
602+ self .assert_samples_equal (samples , collector )
603+
604+ def test_column_positions_preserved (self ):
605+ """Various column positions are preserved exactly."""
606+ columns = [(0 , 10 ), (4 , 50 ), (8 , 100 ), (100 , 200 )]
607+ samples = []
608+ for col , end_col in columns :
609+ frame = make_frame ("test.py" , 10 , "func" , column = col , end_column = end_col )
610+ samples .append ([make_interpreter (0 , [make_thread (1 , [frame ])])])
611+ collector , count = self .roundtrip (samples )
612+ self .assertEqual (count , len (columns ))
613+ self .assert_samples_equal (samples , collector )
614+
615+ def test_same_line_different_opcodes (self ):
616+ """Same line with different opcodes creates distinct frames."""
617+ # This tests that opcode is part of the frame key
618+ frames = [
619+ make_frame ("test.py" , 10 , "func" , opcode = 100 ),
620+ make_frame ("test.py" , 10 , "func" , opcode = 101 ),
621+ make_frame ("test.py" , 10 , "func" , opcode = 102 ),
622+ ]
623+ samples = [[make_interpreter (0 , [make_thread (1 , [f ])]) for f in frames ]]
624+ collector , count = self .roundtrip (samples )
625+ # Verify all three opcodes are preserved distinctly
626+ self .assertEqual (count , 3 )
627+
628+ def test_same_line_different_columns (self ):
629+ """Same line with different columns creates distinct frames."""
630+ frames = [
631+ make_frame ("test.py" , 10 , "func" , column = 0 , end_column = 10 ),
632+ make_frame ("test.py" , 10 , "func" , column = 15 , end_column = 25 ),
633+ make_frame ("test.py" , 10 , "func" , column = 30 , end_column = 40 ),
634+ ]
635+ samples = [[make_interpreter (0 , [make_thread (1 , [f ])]) for f in frames ]]
636+ collector , count = self .roundtrip (samples )
637+ self .assertEqual (count , 3 )
638+
487639
488640class TestBinaryEdgeCases (BinaryFormatTestBase ):
489641 """Tests for edge cases in binary format."""
0 commit comments