|
9 | 9 | import unittest |
10 | 10 | from collections import defaultdict |
11 | 11 |
|
| 12 | +from test.support import captured_stderr |
| 13 | + |
12 | 14 | try: |
13 | 15 | import _remote_debugging |
14 | 16 | from _remote_debugging import ( |
@@ -1051,6 +1053,142 @@ def test_writer_total_samples_after_close_returns_zero(self): |
1051 | 1053 | w.close() |
1052 | 1054 | self.assertEqual(w.total_samples, 0) |
1053 | 1055 |
|
| 1056 | + def test_binary_collector_stops_gracefully_on_overflow(self): |
| 1057 | + """OverflowError from the writer stops collection via the running |
| 1058 | + protocol instead of propagating and corrupting the file. |
| 1059 | + See gh-151292.""" |
| 1060 | + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: |
| 1061 | + filename = f.name |
| 1062 | + self.temp_files.append(filename) |
| 1063 | + |
| 1064 | + collector = BinaryCollector(filename, 1000, compression="none") |
| 1065 | + self.assertTrue(collector.running) |
| 1066 | + |
| 1067 | + sample = [ |
| 1068 | + make_interpreter(0, [make_thread(1, [make_frame("a.py", 1, "f")])]) |
| 1069 | + ] |
| 1070 | + |
| 1071 | + # Collect real samples first, then hit the limit. |
| 1072 | + for i in range(3): |
| 1073 | + collector.collect(sample, timestamp_us=(i + 1) * 1000) |
| 1074 | + self.assertTrue(collector.running) |
| 1075 | + |
| 1076 | + bad = [make_interpreter(2**32, sample[0].threads)] |
| 1077 | + with captured_stderr() as stderr: |
| 1078 | + collector.collect(bad, timestamp_us=4000) |
| 1079 | + collector.collect(sample, timestamp_us=5000) |
| 1080 | + |
| 1081 | + self.assertFalse(collector.running) |
| 1082 | + self.assertTrue(collector._writer.limit_reached) |
| 1083 | + self.assertEqual(stderr.getvalue().count("Warning:"), 1) |
| 1084 | + self.assertIn("interpreter_id", stderr.getvalue()) |
| 1085 | + |
| 1086 | + collector.export(None) |
| 1087 | + |
| 1088 | + self.assertEqual(collector.total_samples, 3) |
| 1089 | + |
| 1090 | + reader_collector = RawCollector() |
| 1091 | + with BinaryReader(filename) as reader: |
| 1092 | + self.assertEqual(reader.replay_samples(reader_collector), 3) |
| 1093 | + |
| 1094 | + def test_interpreter_id_overflow_rejected(self): |
| 1095 | + """An interpreter_id wider than u32 raises OverflowError before any |
| 1096 | + writer state is mutated: subsequent valid samples are still accepted |
| 1097 | + and finalize produces a readable file.""" |
| 1098 | + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: |
| 1099 | + filename = f.name |
| 1100 | + self.temp_files.append(filename) |
| 1101 | + |
| 1102 | + good = [ |
| 1103 | + make_interpreter(0, [make_thread(1, [make_frame("a.py", 1, "f")])]) |
| 1104 | + ] |
| 1105 | + bad = [ |
| 1106 | + make_interpreter(2**32, [make_thread(1, [make_frame("a.py", 1, "f")])]) |
| 1107 | + ] |
| 1108 | + |
| 1109 | + writer = _remote_debugging.BinaryWriter(filename, 1000, 0, compression=0) |
| 1110 | + writer.write_sample(good, 1000) |
| 1111 | + with self.assertRaises(OverflowError): |
| 1112 | + writer.write_sample(bad, 2000) |
| 1113 | + writer.write_sample(good, 3000) |
| 1114 | + writer.finalize() |
| 1115 | + self.assertEqual(writer.total_samples, 2) |
| 1116 | + |
| 1117 | + reader_collector = RawCollector() |
| 1118 | + with BinaryReader(filename) as reader: |
| 1119 | + self.assertEqual(reader.replay_samples(reader_collector), 2) |
| 1120 | + |
| 1121 | + def test_writer_finalizes_after_format_limit(self): |
| 1122 | + for compression in (0, 1) if ZSTD_AVAILABLE else (0,): |
| 1123 | + with self.subTest(compression=compression): |
| 1124 | + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: |
| 1125 | + filename = f.name |
| 1126 | + self.temp_files.append(filename) |
| 1127 | + good = [make_interpreter(0, [ |
| 1128 | + make_thread(1, [make_frame("a.py", 1, "f")]) |
| 1129 | + ])] |
| 1130 | + bad = [make_interpreter(2**32, good[0].threads)] |
| 1131 | + writer = _remote_debugging.BinaryWriter( |
| 1132 | + filename, 1000, 0, compression=compression |
| 1133 | + ) |
| 1134 | + with self.assertRaises(OverflowError): |
| 1135 | + with writer: |
| 1136 | + writer.write_sample(good, 1000) |
| 1137 | + writer.write_sample(good, 2000) |
| 1138 | + # The first interpreter is committed before the limit. |
| 1139 | + writer.write_sample(good + bad, 3000) |
| 1140 | + self.assertEqual(writer.total_samples, 3) |
| 1141 | + with BinaryReader(filename) as reader: |
| 1142 | + self.assertEqual(reader.replay_samples(RawCollector()), 3) |
| 1143 | + |
| 1144 | + def test_collector_does_not_swallow_unrelated_overflow(self): |
| 1145 | + class BadStatus: |
| 1146 | + def __index__(self): |
| 1147 | + raise OverflowError("status conversion failed") |
| 1148 | + |
| 1149 | + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: |
| 1150 | + filename = f.name |
| 1151 | + self.temp_files.append(filename) |
| 1152 | + collector = BinaryCollector(filename, 1000, compression="none") |
| 1153 | + self.addCleanup(collector._writer.close) |
| 1154 | + sample = [make_interpreter(0, [make_thread(1, [], BadStatus())])] |
| 1155 | + with captured_stderr() as stderr: |
| 1156 | + with self.assertRaisesRegex(OverflowError, "status conversion failed"): |
| 1157 | + collector.collect(sample, timestamp_us=1000) |
| 1158 | + self.assertEqual(stderr.getvalue(), "") |
| 1159 | + self.assertFalse(collector._writer.limit_reached) |
| 1160 | + with self.assertRaisesRegex(ValueError, "broken"): |
| 1161 | + collector.export() |
| 1162 | + with self.assertRaisesRegex(ValueError, "broken"): |
| 1163 | + collector._writer.write_sample([], 2000) |
| 1164 | + # Closing a broken writer must not attempt to finalize it. |
| 1165 | + collector.__exit__(None, None, None) |
| 1166 | + |
| 1167 | + def test_collector_finalizes_after_external_exception(self): |
| 1168 | + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: |
| 1169 | + filename = f.name |
| 1170 | + self.temp_files.append(filename) |
| 1171 | + with self.assertRaisesRegex(RuntimeError, "sampling failed"): |
| 1172 | + with BinaryCollector(filename, 1000, compression="none") as collector: |
| 1173 | + collector.collect([make_interpreter(0, [make_thread(1, [])])]) |
| 1174 | + raise RuntimeError("sampling failed") |
| 1175 | + self.assertEqual(collector.total_samples, 1) |
| 1176 | + with BinaryReader(filename) as reader: |
| 1177 | + self.assertEqual(reader.replay_samples(RawCollector()), 1) |
| 1178 | + |
| 1179 | + @unittest.skipUnless(os.path.exists("/dev/full"), "requires /dev/full") |
| 1180 | + def test_finalize_failure_breaks_writer(self): |
| 1181 | + writer = _remote_debugging.BinaryWriter("/dev/full", 1000, 0) |
| 1182 | + self.addCleanup(writer.close) |
| 1183 | + writer.write_sample([make_interpreter(0, [make_thread(1, [])])], 1000) |
| 1184 | + with self.assertRaises(OSError): |
| 1185 | + writer.finalize() |
| 1186 | + self.assertFalse(writer.limit_reached) |
| 1187 | + with self.assertRaisesRegex(ValueError, "broken"): |
| 1188 | + writer.finalize() |
| 1189 | + with self.assertRaisesRegex(ValueError, "broken"): |
| 1190 | + writer.write_sample([], 2000) |
| 1191 | + |
1054 | 1192 |
|
1055 | 1193 | class TestBinaryFormatValidation(BinaryFormatTestBase): |
1056 | 1194 | """Tests for malformed binary files.""" |
|
0 commit comments