|
| 1 | +import builtins |
1 | 2 | from collections import namedtuple
|
2 | 3 | import contextlib
|
3 | 4 | import itertools
|
@@ -866,10 +867,11 @@ def assert_run_failed(self, exctype, msg=None):
|
866 | 867 | yield
|
867 | 868 | if msg is None:
|
868 | 869 | self.assertEqual(str(caught.exception).split(':')[0],
|
869 |
| - str(exctype)) |
| 870 | + exctype.__name__) |
870 | 871 | else:
|
871 | 872 | self.assertEqual(str(caught.exception),
|
872 |
| - "{}: {}".format(exctype, msg)) |
| 873 | + "{}: {}".format(exctype.__name__, msg)) |
| 874 | + self.assertIsInstance(caught.exception.__cause__, exctype) |
873 | 875 |
|
874 | 876 | def test_invalid_syntax(self):
|
875 | 877 | with self.assert_run_failed(SyntaxError):
|
@@ -1060,6 +1062,301 @@ def f():
|
1060 | 1062 | self.assertEqual(retcode, 0)
|
1061 | 1063 |
|
1062 | 1064 |
|
| 1065 | +def build_exception(exctype, /, *args, **kwargs): |
| 1066 | + # XXX Use __qualname__? |
| 1067 | + name = exctype.__name__ |
| 1068 | + argreprs = [repr(a) for a in args] |
| 1069 | + if kwargs: |
| 1070 | + kwargreprs = [f'{k}={v!r}' for k, v in kwargs.items()] |
| 1071 | + script = f'{name}({", ".join(argreprs)}, {", ".join(kwargreprs)})' |
| 1072 | + else: |
| 1073 | + script = f'{name}({", ".join(argreprs)})' |
| 1074 | + expected = exctype(*args, **kwargs) |
| 1075 | + return script, expected |
| 1076 | + |
| 1077 | + |
| 1078 | +def build_exceptions(self, *exctypes, default=None, custom=None, bases=True): |
| 1079 | + if not exctypes: |
| 1080 | + raise NotImplementedError |
| 1081 | + if not default: |
| 1082 | + default = ((), {}) |
| 1083 | + elif isinstance(default, str): |
| 1084 | + default = ((default,), {}) |
| 1085 | + elif type(default) is not tuple: |
| 1086 | + raise NotImplementedError |
| 1087 | + elif len(default) != 2: |
| 1088 | + default = (default, {}) |
| 1089 | + elif type(default[0]) is not tuple: |
| 1090 | + default = (default, {}) |
| 1091 | + elif type(default[1]) is not dict: |
| 1092 | + default = (default, {}) |
| 1093 | + # else leave it alone |
| 1094 | + |
| 1095 | + for exctype in exctypes: |
| 1096 | + customtype = None |
| 1097 | + values = default |
| 1098 | + if custom: |
| 1099 | + if exctype in custom: |
| 1100 | + customtype = exctype |
| 1101 | + elif bases: |
| 1102 | + for customtype in custom: |
| 1103 | + if issubclass(exctype, customtype): |
| 1104 | + break |
| 1105 | + else: |
| 1106 | + customtype = None |
| 1107 | + if customtype is not None: |
| 1108 | + values = custom[customtype] |
| 1109 | + if values is None: |
| 1110 | + continue |
| 1111 | + args, kwargs = values |
| 1112 | + script, expected = build_exception(exctype, *args, **kwargs) |
| 1113 | + yield exctype, customtype, script, expected |
| 1114 | + |
| 1115 | + |
| 1116 | +try: |
| 1117 | + raise Exception |
| 1118 | +except Exception as exc: |
| 1119 | + assert exc.__traceback__ is not None |
| 1120 | + Traceback = type(exc.__traceback__) |
| 1121 | + |
| 1122 | + |
| 1123 | +class RunFailedTests(TestBase): |
| 1124 | + |
| 1125 | + BUILTINS = [v |
| 1126 | + for v in vars(builtins).values() |
| 1127 | + if (type(v) is type |
| 1128 | + and issubclass(v, Exception) |
| 1129 | + #and issubclass(v, BaseException) |
| 1130 | + ) |
| 1131 | + ] |
| 1132 | + BUILTINS_SPECIAL = [ |
| 1133 | + # These all have extra attributes (i.e. args/kwargs) |
| 1134 | + SyntaxError, |
| 1135 | + ImportError, |
| 1136 | + UnicodeError, |
| 1137 | + OSError, |
| 1138 | + SystemExit, |
| 1139 | + StopIteration, |
| 1140 | + ] |
| 1141 | + |
| 1142 | + @classmethod |
| 1143 | + def build_exceptions(cls, exctypes=None, default=(), custom=None): |
| 1144 | + if exctypes is None: |
| 1145 | + exctypes = cls.BUILTINS |
| 1146 | + if custom is None: |
| 1147 | + # Skip the "special" ones. |
| 1148 | + custom = {et: None for et in cls.BUILTINS_SPECIAL} |
| 1149 | + yield from build_exceptions(*exctypes, default=default, custom=custom) |
| 1150 | + |
| 1151 | + def assertExceptionsEqual(self, exc, expected, *, chained=True): |
| 1152 | + if type(expected) is type: |
| 1153 | + self.assertIs(type(exc), expected) |
| 1154 | + return |
| 1155 | + elif not isinstance(exc, Exception): |
| 1156 | + self.assertEqual(exc, expected) |
| 1157 | + elif not isinstance(expected, Exception): |
| 1158 | + self.assertEqual(exc, expected) |
| 1159 | + else: |
| 1160 | + # Plain equality doesn't work, so we have to compare manually. |
| 1161 | + self.assertIs(type(exc), type(expected)) |
| 1162 | + self.assertEqual(exc.args, expected.args) |
| 1163 | + self.assertEqual(exc.__reduce__(), expected.__reduce__()) |
| 1164 | + if chained: |
| 1165 | + self.assertExceptionsEqual(exc.__context__, |
| 1166 | + expected.__context__) |
| 1167 | + self.assertExceptionsEqual(exc.__cause__, |
| 1168 | + expected.__cause__) |
| 1169 | + self.assertEqual(exc.__suppress_context__, |
| 1170 | + expected.__suppress_context__) |
| 1171 | + |
| 1172 | + def assertTracebacksEqual(self, tb, expected): |
| 1173 | + if not isinstance(tb, Traceback): |
| 1174 | + self.assertEqual(tb, expected) |
| 1175 | + elif not isinstance(expected, Traceback): |
| 1176 | + self.assertEqual(tb, expected) |
| 1177 | + else: |
| 1178 | + self.assertEqual(tb.tb_frame.f_code.co_name, |
| 1179 | + expected.tb_frame.f_code.co_name) |
| 1180 | + self.assertEqual(tb.tb_frame.f_code.co_filename, |
| 1181 | + expected.tb_frame.f_code.co_filename) |
| 1182 | + self.assertEqual(tb.tb_lineno, expected.tb_lineno) |
| 1183 | + self.assertTracebacksEqual(tb.tb_next, expected.tb_next) |
| 1184 | + |
| 1185 | + # XXX Move this to TestBase? |
| 1186 | + @contextlib.contextmanager |
| 1187 | + def expected_run_failure(self, expected): |
| 1188 | + exctype = expected if type(expected) is type else type(expected) |
| 1189 | + |
| 1190 | + with self.assertRaises(interpreters.RunFailedError) as caught: |
| 1191 | + yield caught |
| 1192 | + exc = caught.exception |
| 1193 | + |
| 1194 | + modname = exctype.__module__ |
| 1195 | + if modname == 'builtins' or modname == '__main__': |
| 1196 | + exctypename = exctype.__name__ |
| 1197 | + else: |
| 1198 | + exctypename = f'{modname}.{exctype.__name__}' |
| 1199 | + if exctype is expected: |
| 1200 | + self.assertEqual(str(exc).split(':')[0], exctypename) |
| 1201 | + else: |
| 1202 | + self.assertEqual(str(exc), f'{exctypename}: {expected}') |
| 1203 | + self.assertExceptionsEqual(exc.__cause__, expected) |
| 1204 | + if exc.__cause__ is not None: |
| 1205 | + self.assertIsNotNone(exc.__cause__.__traceback__) |
| 1206 | + |
| 1207 | + def test_builtin_exceptions(self): |
| 1208 | + interpid = interpreters.create() |
| 1209 | + msg = '<a message>' |
| 1210 | + for i, info in enumerate(self.build_exceptions( |
| 1211 | + default=msg, |
| 1212 | + custom={ |
| 1213 | + SyntaxError: ((msg, '<stdin>', 1, 3, 'a +?'), {}), |
| 1214 | + ImportError: ((msg,), {'name': 'spam', 'path': '/x/spam.py'}), |
| 1215 | + UnicodeError: None, |
| 1216 | + #UnicodeError: ((), {}), |
| 1217 | + #OSError: ((), {}), |
| 1218 | + SystemExit: ((1,), {}), |
| 1219 | + StopIteration: (('<a value>',), {}), |
| 1220 | + }, |
| 1221 | + )): |
| 1222 | + exctype, _, script, expected = info |
| 1223 | + testname = f'{i+1} - {script}' |
| 1224 | + script = f'raise {script}' |
| 1225 | + |
| 1226 | + with self.subTest(testname): |
| 1227 | + with self.expected_run_failure(expected): |
| 1228 | + interpreters.run_string(interpid, script) |
| 1229 | + |
| 1230 | + def test_custom_exception_from___main__(self): |
| 1231 | + script = dedent(""" |
| 1232 | + class SpamError(Exception): |
| 1233 | + def __init__(self, q): |
| 1234 | + super().__init__(f'got {q}') |
| 1235 | + self.q = q |
| 1236 | + raise SpamError('eggs') |
| 1237 | + """) |
| 1238 | + expected = Exception(f'SpamError: got {"eggs"}') |
| 1239 | + |
| 1240 | + interpid = interpreters.create() |
| 1241 | + with self.assertRaises(interpreters.RunFailedError) as caught: |
| 1242 | + interpreters.run_string(interpid, script) |
| 1243 | + cause = caught.exception.__cause__ |
| 1244 | + |
| 1245 | + self.assertExceptionsEqual(cause, expected) |
| 1246 | + |
| 1247 | + class SpamError(Exception): |
| 1248 | + # The normal Exception.__reduce__() produces a funny result |
| 1249 | + # here. So we have to use a custom __new__(). |
| 1250 | + def __new__(cls, q): |
| 1251 | + if type(q) is SpamError: |
| 1252 | + return q |
| 1253 | + return super().__new__(cls, q) |
| 1254 | + def __init__(self, q): |
| 1255 | + super().__init__(f'got {q}') |
| 1256 | + self.q = q |
| 1257 | + |
| 1258 | + def test_custom_exception(self): |
| 1259 | + script = dedent(""" |
| 1260 | + import test.test__xxsubinterpreters |
| 1261 | + SpamError = test.test__xxsubinterpreters.RunFailedTests.SpamError |
| 1262 | + raise SpamError('eggs') |
| 1263 | + """) |
| 1264 | + try: |
| 1265 | + ns = {} |
| 1266 | + exec(script, ns, ns) |
| 1267 | + except Exception as exc: |
| 1268 | + expected = exc |
| 1269 | + |
| 1270 | + interpid = interpreters.create() |
| 1271 | + with self.expected_run_failure(expected): |
| 1272 | + interpreters.run_string(interpid, script) |
| 1273 | + |
| 1274 | + class SpamReducedError(Exception): |
| 1275 | + def __init__(self, q): |
| 1276 | + super().__init__(f'got {q}') |
| 1277 | + self.q = q |
| 1278 | + def __reduce__(self): |
| 1279 | + return (type(self), (self.q,), {}) |
| 1280 | + |
| 1281 | + def test_custom___reduce__(self): |
| 1282 | + script = dedent(""" |
| 1283 | + import test.test__xxsubinterpreters |
| 1284 | + SpamError = test.test__xxsubinterpreters.RunFailedTests.SpamReducedError |
| 1285 | + raise SpamError('eggs') |
| 1286 | + """) |
| 1287 | + try: |
| 1288 | + exec(script, (ns := {'__name__': '__main__'}), ns) |
| 1289 | + except Exception as exc: |
| 1290 | + expected = exc |
| 1291 | + |
| 1292 | + interpid = interpreters.create() |
| 1293 | + with self.expected_run_failure(expected): |
| 1294 | + interpreters.run_string(interpid, script) |
| 1295 | + |
| 1296 | + def test_traceback_propagated(self): |
| 1297 | + script = dedent(""" |
| 1298 | + def do_spam(): |
| 1299 | + raise Exception('uh-oh') |
| 1300 | + def do_eggs(): |
| 1301 | + return do_spam() |
| 1302 | + class Spam: |
| 1303 | + def do(self): |
| 1304 | + return do_eggs() |
| 1305 | + def get_handler(): |
| 1306 | + def handler(): |
| 1307 | + return Spam().do() |
| 1308 | + return handler |
| 1309 | + go = (lambda: get_handler()()) |
| 1310 | + def iter_all(): |
| 1311 | + yield from (go() for _ in [True]) |
| 1312 | + yield None |
| 1313 | + def main(): |
| 1314 | + for v in iter_all(): |
| 1315 | + pass |
| 1316 | + main() |
| 1317 | + """) |
| 1318 | + try: |
| 1319 | + ns = {} |
| 1320 | + exec(script, ns, ns) |
| 1321 | + except Exception as exc: |
| 1322 | + expected = exc |
| 1323 | + expectedtb = exc.__traceback__.tb_next |
| 1324 | + |
| 1325 | + interpid = interpreters.create() |
| 1326 | + with self.expected_run_failure(expected) as caught: |
| 1327 | + interpreters.run_string(interpid, script) |
| 1328 | + exc = caught.exception |
| 1329 | + |
| 1330 | + self.assertTracebacksEqual(exc.__cause__.__traceback__, |
| 1331 | + expectedtb) |
| 1332 | + |
| 1333 | + def test_chained_exceptions(self): |
| 1334 | + script = dedent(""" |
| 1335 | + try: |
| 1336 | + raise ValueError('msg 1') |
| 1337 | + except Exception as exc1: |
| 1338 | + try: |
| 1339 | + raise TypeError('msg 2') |
| 1340 | + except Exception as exc2: |
| 1341 | + try: |
| 1342 | + raise IndexError('msg 3') from exc2 |
| 1343 | + except Exception: |
| 1344 | + raise AttributeError('msg 4') |
| 1345 | + """) |
| 1346 | + try: |
| 1347 | + exec(script, {}, {}) |
| 1348 | + except Exception as exc: |
| 1349 | + expected = exc |
| 1350 | + |
| 1351 | + interpid = interpreters.create() |
| 1352 | + with self.expected_run_failure(expected) as caught: |
| 1353 | + interpreters.run_string(interpid, script) |
| 1354 | + exc = caught.exception |
| 1355 | + |
| 1356 | + # ...just to be sure. |
| 1357 | + self.assertIs(type(exc.__cause__), AttributeError) |
| 1358 | + |
| 1359 | + |
1063 | 1360 | ##################################
|
1064 | 1361 | # channel tests
|
1065 | 1362 |
|
|
0 commit comments