|  | 
|  | 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