@@ -146,12 +146,6 @@ def samples_to_by_thread(samples):
146146class BinaryFormatTestBase (unittest .TestCase ):
147147 """Base class with common setup/teardown for binary format tests."""
148148
149- HDR_OFF_SAMPLES = 28
150- HDR_OFF_THREADS = 36
151- HDR_OFF_STR_TABLE = 40
152- HDR_OFF_FRAME_TABLE = 48
153- FILE_HEADER_PLACEHOLDER_SIZE = 64
154-
155149 def setUp (self ):
156150 self .temp_files = []
157151
@@ -1059,33 +1053,19 @@ def test_binary_collector_stops_gracefully_on_overflow(self):
10591053 collector .collect (sample , timestamp_us = (i + 1 ) * 1000 )
10601054 self .assertTrue (collector .running )
10611055
1062- real_writer = collector ._writer
1063-
1064- class _OverflowingWriter :
1065- def write_sample (self , stack_frames , timestamp_us ):
1066- raise OverflowError ("too many samples for binary format" )
1067-
1068- collector ._writer = _OverflowingWriter ()
1056+ bad = [make_interpreter (2 ** 32 , sample [0 ].threads )]
10691057 with captured_stderr () as stderr :
1070- collector .collect (sample , timestamp_us = 4000 )
1058+ collector .collect (bad , timestamp_us = 4000 )
1059+ collector .collect (sample , timestamp_us = 5000 )
10711060
10721061 self .assertFalse (collector .running )
1073- self .assertIn ("too many samples" , stderr .getvalue ())
1062+ self .assertTrue (collector ._writer .limit_reached )
1063+ self .assertEqual (stderr .getvalue ().count ("Warning:" ), 1 )
1064+ self .assertIn ("interpreter_id" , stderr .getvalue ())
10741065
1075- # The real writer can still be finalized into a valid file that
1076- # keeps the samples collected before the limit was hit.
1077- collector ._writer = real_writer
10781066 collector .export (None )
10791067
1080- with open (filename , "rb" ) as f :
1081- header = f .read (self .FILE_HEADER_PLACEHOLDER_SIZE )
1082- magic , version = struct .unpack_from ("=II" , header , 0 )
1083- self .assertEqual (magic , 0x54414348 ) # "TACH"
1084- self .assertEqual (version , 1 )
1085- (sample_count ,) = struct .unpack_from (
1086- "=Q" , header , self .HDR_OFF_SAMPLES
1087- )
1088- self .assertEqual (sample_count , 3 )
1068+ self .assertEqual (collector .total_samples , 3 )
10891069
10901070 reader_collector = RawCollector ()
10911071 with BinaryReader (filename ) as reader :
@@ -1118,10 +1098,86 @@ def test_interpreter_id_overflow_rejected(self):
11181098 with BinaryReader (filename ) as reader :
11191099 self .assertEqual (reader .replay_samples (reader_collector ), 2 )
11201100
1101+ def test_writer_finalizes_after_format_limit (self ):
1102+ for compression in (0 , 1 ) if ZSTD_AVAILABLE else (0 ,):
1103+ with self .subTest (compression = compression ):
1104+ with tempfile .NamedTemporaryFile (suffix = ".bin" , delete = False ) as f :
1105+ filename = f .name
1106+ self .temp_files .append (filename )
1107+ good = [make_interpreter (0 , [
1108+ make_thread (1 , [make_frame ("a.py" , 1 , "f" )])
1109+ ])]
1110+ bad = [make_interpreter (2 ** 32 , good [0 ].threads )]
1111+ writer = _remote_debugging .BinaryWriter (
1112+ filename , 1000 , 0 , compression = compression
1113+ )
1114+ with self .assertRaises (OverflowError ):
1115+ with writer :
1116+ writer .write_sample (good , 1000 )
1117+ writer .write_sample (good , 2000 )
1118+ # The first interpreter is committed before the limit.
1119+ writer .write_sample (good + bad , 3000 )
1120+ self .assertEqual (writer .total_samples , 3 )
1121+ with BinaryReader (filename ) as reader :
1122+ self .assertEqual (reader .replay_samples (RawCollector ()), 3 )
1123+
1124+ def test_collector_does_not_swallow_unrelated_overflow (self ):
1125+ class BadStatus :
1126+ def __index__ (self ):
1127+ raise OverflowError ("status conversion failed" )
1128+
1129+ with tempfile .NamedTemporaryFile (suffix = ".bin" , delete = False ) as f :
1130+ filename = f .name
1131+ self .temp_files .append (filename )
1132+ collector = BinaryCollector (filename , 1000 , compression = "none" )
1133+ self .addCleanup (collector ._writer .close )
1134+ sample = [make_interpreter (0 , [make_thread (1 , [], BadStatus ())])]
1135+ with captured_stderr () as stderr :
1136+ with self .assertRaisesRegex (OverflowError , "status conversion failed" ):
1137+ collector .collect (sample , timestamp_us = 1000 )
1138+ self .assertEqual (stderr .getvalue (), "" )
1139+ self .assertFalse (collector ._writer .limit_reached )
1140+ with self .assertRaisesRegex (ValueError , "broken" ):
1141+ collector .export ()
1142+ with self .assertRaisesRegex (ValueError , "broken" ):
1143+ collector ._writer .write_sample ([], 2000 )
1144+ # Closing a broken writer must not attempt to finalize it.
1145+ collector .__exit__ (None , None , None )
1146+
1147+ def test_collector_finalizes_after_external_exception (self ):
1148+ with tempfile .NamedTemporaryFile (suffix = ".bin" , delete = False ) as f :
1149+ filename = f .name
1150+ self .temp_files .append (filename )
1151+ with self .assertRaisesRegex (RuntimeError , "sampling failed" ):
1152+ with BinaryCollector (filename , 1000 , compression = "none" ) as collector :
1153+ collector .collect ([make_interpreter (0 , [make_thread (1 , [])])])
1154+ raise RuntimeError ("sampling failed" )
1155+ self .assertEqual (collector .total_samples , 1 )
1156+ with BinaryReader (filename ) as reader :
1157+ self .assertEqual (reader .replay_samples (RawCollector ()), 1 )
1158+
1159+ @unittest .skipUnless (os .path .exists ("/dev/full" ), "requires /dev/full" )
1160+ def test_finalize_failure_breaks_writer (self ):
1161+ writer = _remote_debugging .BinaryWriter ("/dev/full" , 1000 , 0 )
1162+ self .addCleanup (writer .close )
1163+ writer .write_sample ([make_interpreter (0 , [make_thread (1 , [])])], 1000 )
1164+ with self .assertRaises (OSError ):
1165+ writer .finalize ()
1166+ self .assertFalse (writer .limit_reached )
1167+ with self .assertRaisesRegex (ValueError , "broken" ):
1168+ writer .finalize ()
1169+ with self .assertRaisesRegex (ValueError , "broken" ):
1170+ writer .write_sample ([], 2000 )
1171+
11211172
11221173class TestBinaryFormatValidation (BinaryFormatTestBase ):
11231174 """Tests for malformed binary files."""
11241175
1176+ HDR_OFF_SAMPLES = 28
1177+ HDR_OFF_THREADS = 36
1178+ HDR_OFF_STR_TABLE = 40
1179+ HDR_OFF_FRAME_TABLE = 48
1180+ FILE_HEADER_PLACEHOLDER_SIZE = 64
11251181 FILE_FOOTER_SIZE = 32
11261182 FTR_OFF_STRINGS = 0
11271183 FTR_OFF_FRAMES = 4
0 commit comments