Skip to content

Commit b6716b8

Browse files
committed
update unraisable tests to use sys.unraisablehook
* assert shape of calls to unraisablehook and sanity check traceback contents instead of (varying) stderr output from default unraisablehook impl
1 parent dc03908 commit b6716b8

File tree

1 file changed

+69
-148
lines changed

1 file changed

+69
-148
lines changed

src/c/test_c.py

Lines changed: 69 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
from __future__ import annotations
2+
3+
import contextlib
4+
import traceback
5+
import unittest.mock
6+
17
import pytest
28
import sys
9+
import typing as t
310

411
is_musl = False
512
if sys.platform == 'linux':
@@ -1337,27 +1344,37 @@ def cb(n):
13371344
e = pytest.raises(TypeError, f)
13381345
assert str(e.value) == "'int(*)(int)' expects 1 arguments, got 0"
13391346

1347+
@contextlib.contextmanager
1348+
def _assert_unraisable(error_type: type[Exception] | None, message: str = '', traceback_tokens: list[str] | None = None):
1349+
"""Assert sys.unraisablehook interaction (or an approximation for older Pythons) occurred while this context is active"""
1350+
raised_errors: list[Exception] = []
1351+
raised_traceback: str = ''
1352+
1353+
# sys.unraisablehook is called more than once for chained exceptions; accumulate the errors and tracebacks for inspection
1354+
def _capture_unraisable_hook(ur_args):
1355+
nonlocal raised_traceback
1356+
raised_errors.append(ur_args.exc_value)
1357+
1358+
# NB: need to use the old etype/value/tb form until 3.10 is the minimum
1359+
raised_traceback += (ur_args.err_msg or '' + '\n') + ''.join(traceback.format_exception(None, ur_args.exc_value, ur_args.exc_traceback))
1360+
1361+
1362+
with pytest.MonkeyPatch.context() as mp:
1363+
mp.setattr(sys, 'unraisablehook', _capture_unraisable_hook)
1364+
yield
1365+
1366+
if error_type is None:
1367+
assert not raised_errors
1368+
assert not raised_traceback
1369+
return
1370+
1371+
assert any(type(raised_error) is error_type for raised_error in raised_errors)
1372+
assert any(message in str(raised_error) for raised_error in raised_errors)
1373+
for t in traceback_tokens or []:
1374+
assert t in raised_traceback
1375+
1376+
13401377
def test_callback_exception():
1341-
try:
1342-
import cStringIO
1343-
except ImportError:
1344-
import io as cStringIO # Python 3
1345-
import linecache
1346-
def matches(istr, ipattern, ipattern38, ipattern311=None):
1347-
if sys.version_info >= (3, 8):
1348-
ipattern = ipattern38
1349-
if sys.version_info >= (3, 11):
1350-
ipattern = ipattern311 or ipattern38
1351-
str, pattern = istr, ipattern
1352-
while '$' in pattern:
1353-
i = pattern.index('$')
1354-
assert str[:i] == pattern[:i]
1355-
j = str.find(pattern[i+1], i)
1356-
assert i + 1 <= j <= str.find('\n', i)
1357-
str = str[j:]
1358-
pattern = pattern[i+1:]
1359-
assert str == pattern
1360-
return True
13611378
def check_value(x):
13621379
if x == 10000:
13631380
raise ValueError(42)
@@ -1366,148 +1383,52 @@ def Zcb1(x):
13661383
return x * 3
13671384
BShort = new_primitive_type("short")
13681385
BFunc = new_function_type((BShort,), BShort, False)
1386+
13691387
f = callback(BFunc, Zcb1, -42)
1370-
#
13711388
seen = []
13721389
oops_result = None
13731390
def oops(*args):
13741391
seen.append(args)
13751392
return oops_result
13761393
ff = callback(BFunc, Zcb1, -42, oops)
1377-
#
1378-
orig_stderr = sys.stderr
1379-
orig_getline = linecache.getline
1380-
try:
1381-
linecache.getline = lambda *args: 'LINE' # hack: speed up PyPy tests
1382-
sys.stderr = cStringIO.StringIO()
1383-
if hasattr(sys, '__unraisablehook__'): # work around pytest
1384-
sys.unraisablehook = sys.__unraisablehook__ # on recent CPythons
1394+
with _assert_unraisable(None):
13851395
assert f(100) == 300
1386-
assert sys.stderr.getvalue() == ''
1396+
with _assert_unraisable(ValueError, '42', ['in Zcb1', 'in check_value']):
13871397
assert f(10000) == -42
1388-
assert matches(sys.stderr.getvalue(), """\
1389-
From cffi callback <function$Zcb1 at 0x$>:
1390-
Traceback (most recent call last):
1391-
File "$", line $, in Zcb1
1392-
$
1393-
File "$", line $, in check_value
1394-
$
1395-
ValueError: 42
1396-
""", """\
1397-
Exception ignored from cffi callback <function$Zcb1 at 0x$>:
1398-
Traceback (most recent call last):
1399-
File "$", line $, in Zcb1
1400-
$
1401-
File "$", line $, in check_value
1402-
$
1403-
ValueError: 42
1404-
""")
1405-
sys.stderr = cStringIO.StringIO()
1406-
bigvalue = 20000
1398+
1399+
bigvalue = 20000
1400+
with _assert_unraisable(OverflowError, "integer 60000 does not fit 'short'", ['callback', 'Zcb1']):
14071401
assert f(bigvalue) == -42
1408-
assert matches(sys.stderr.getvalue(), """\
1409-
From cffi callback <function$Zcb1 at 0x$>:
1410-
Trying to convert the result back to C:
1411-
OverflowError: integer 60000 does not fit 'short'
1412-
""", """\
1413-
Exception ignored from cffi callback <function$Zcb1 at 0x$>, trying to convert the result back to C:
1414-
Traceback (most recent call last):
1415-
File "$", line $, in test_callback_exception
1416-
$
1417-
OverflowError: integer 60000 does not fit 'short'
1418-
""")
1419-
sys.stderr = cStringIO.StringIO()
1420-
bigvalue = 20000
1421-
assert len(seen) == 0
1402+
assert len(seen) == 0
1403+
1404+
with _assert_unraisable(None):
14221405
assert ff(bigvalue) == -42
1423-
assert sys.stderr.getvalue() == ""
1424-
assert len(seen) == 1
1425-
exc, val, tb = seen[0]
1426-
assert exc is OverflowError
1427-
assert str(val) == "integer 60000 does not fit 'short'"
1428-
#
1429-
sys.stderr = cStringIO.StringIO()
1430-
bigvalue = 20000
1431-
del seen[:]
1432-
oops_result = 81
1406+
assert len(seen) == 1
1407+
exc, val, tb = seen[0]
1408+
assert exc is OverflowError
1409+
assert str(val) == "integer 60000 does not fit 'short'"
1410+
1411+
del seen[:]
1412+
oops_result = 81
1413+
with _assert_unraisable(None):
14331414
assert ff(bigvalue) == 81
1434-
oops_result = None
1435-
assert sys.stderr.getvalue() == ""
1436-
assert len(seen) == 1
1437-
exc, val, tb = seen[0]
1438-
assert exc is OverflowError
1439-
assert str(val) == "integer 60000 does not fit 'short'"
1440-
#
1441-
sys.stderr = cStringIO.StringIO()
1442-
bigvalue = 20000
1443-
del seen[:]
1444-
oops_result = "xy" # not None and not an int!
1415+
1416+
assert len(seen) == 1
1417+
exc, val, tb = seen[0]
1418+
assert exc is OverflowError
1419+
assert str(val) == "integer 60000 does not fit 'short'"
1420+
1421+
del seen[:]
1422+
oops_result = "xy" # not None and not an int!
1423+
1424+
with _assert_unraisable(TypeError, "an integer is required", ["integer 60000 does not fit 'short'"]):
14451425
assert ff(bigvalue) == -42
1446-
oops_result = None
1447-
assert matches(sys.stderr.getvalue(), """\
1448-
From cffi callback <function$Zcb1 at 0x$>:
1449-
Trying to convert the result back to C:
1450-
OverflowError: integer 60000 does not fit 'short'
1451-
1452-
During the call to 'onerror', another exception occurred:
1453-
1454-
TypeError: $integer$
1455-
""", """\
1456-
Exception ignored from cffi callback <function$Zcb1 at 0x$>, trying to convert the result back to C:
1457-
Traceback (most recent call last):
1458-
File "$", line $, in test_callback_exception
1459-
$
1460-
OverflowError: integer 60000 does not fit 'short'
1461-
Exception ignored during handling of the above exception by 'onerror':
1462-
Traceback (most recent call last):
1463-
File "$", line $, in test_callback_exception
1464-
$
1465-
TypeError: $integer$
1466-
""")
1467-
#
1468-
sys.stderr = cStringIO.StringIO()
1469-
seen = "not a list" # this makes the oops() function crash
1426+
1427+
seen = "not a list" # this makes the oops() function crash
1428+
oops_result = None
1429+
with _assert_unraisable(AttributeError, "'str' object has no attribute 'append", ['Zcb1', 'ff', 'oops']):
14701430
assert ff(bigvalue) == -42
1471-
# the $ after the AttributeError message are for the suggestions that
1472-
# will be added in Python 3.10
1473-
assert matches(sys.stderr.getvalue(), """\
1474-
From cffi callback <function$Zcb1 at 0x$>:
1475-
Trying to convert the result back to C:
1476-
OverflowError: integer 60000 does not fit 'short'
1477-
1478-
During the call to 'onerror', another exception occurred:
1479-
1480-
Traceback (most recent call last):
1481-
File "$", line $, in oops
1482-
$
1483-
AttributeError: 'str' object has no attribute 'append$
1484-
""", """\
1485-
Exception ignored from cffi callback <function$Zcb1 at 0x$>, trying to convert the result back to C:
1486-
Traceback (most recent call last):
1487-
File "$", line $, in test_callback_exception
1488-
$
1489-
OverflowError: integer 60000 does not fit 'short'
1490-
Exception ignored during handling of the above exception by 'onerror':
1491-
Traceback (most recent call last):
1492-
File "$", line $, in oops
1493-
$
1494-
AttributeError: 'str' object has no attribute 'append$
1495-
""", """\
1496-
Exception ignored from cffi callback <function$Zcb1 at 0x$>, trying to convert the result back to C:
1497-
Traceback (most recent call last):
1498-
File "$", line $, in test_callback_exception
1499-
$
1500-
OverflowError: integer 60000 does not fit 'short'
1501-
Exception ignored during handling of the above exception by 'onerror':
1502-
Traceback (most recent call last):
1503-
File "$", line $, in oops
1504-
$
1505-
$
1506-
AttributeError: 'str' object has no attribute 'append$
1507-
""")
1508-
finally:
1509-
sys.stderr = orig_stderr
1510-
linecache.getline = orig_getline
1431+
15111432

15121433
def test_callback_return_type():
15131434
for rettype in ["signed char", "short", "int", "long", "long long",

0 commit comments

Comments
 (0)