aboutsummaryrefslogtreecommitdiff
path: root/tests/test_exceptions.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_exceptions.py')
-rw-r--r--tests/test_exceptions.py238
1 files changed, 221 insertions, 17 deletions
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index c6cb6529..ccac4536 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -1,10 +1,11 @@
-# -*- coding: utf-8 -*-
import sys
import pytest
-from pybind11_tests import exceptions as m
+import env
import pybind11_cross_module_tests as cm
+import pybind11_tests # noqa: F401
+from pybind11_tests import exceptions as m
def test_std_exception(msg):
@@ -16,14 +17,31 @@ def test_std_exception(msg):
def test_error_already_set(msg):
with pytest.raises(RuntimeError) as excinfo:
m.throw_already_set(False)
- assert msg(excinfo.value) == "Unknown internal error occurred"
+ assert (
+ msg(excinfo.value)
+ == "Internal error: pybind11::error_already_set called while Python error indicator not set."
+ )
with pytest.raises(ValueError) as excinfo:
m.throw_already_set(True)
assert msg(excinfo.value) == "foo"
-def test_cross_module_exceptions():
+def test_raise_from(msg):
+ with pytest.raises(ValueError) as excinfo:
+ m.raise_from()
+ assert msg(excinfo.value) == "outer"
+ assert msg(excinfo.value.__cause__) == "inner"
+
+
+def test_raise_from_already_set(msg):
+ with pytest.raises(ValueError) as excinfo:
+ m.raise_from_already_set()
+ assert msg(excinfo.value) == "outer"
+ assert msg(excinfo.value.__cause__) == "inner"
+
+
+def test_cross_module_exceptions(msg):
with pytest.raises(RuntimeError) as excinfo:
cm.raise_runtime_error()
assert str(excinfo.value) == "My runtime error"
@@ -43,6 +61,27 @@ def test_cross_module_exceptions():
with pytest.raises(StopIteration) as excinfo:
cm.throw_stop_iteration()
+ with pytest.raises(cm.LocalSimpleException) as excinfo:
+ cm.throw_local_simple_error()
+ assert msg(excinfo.value) == "external mod"
+
+ with pytest.raises(KeyError) as excinfo:
+ cm.throw_local_error()
+ # KeyError is a repr of the key, so it has an extra set of quotes
+ assert str(excinfo.value) == "'just local'"
+
+
+# TODO: FIXME
+@pytest.mark.xfail(
+ "env.MACOS and (env.PYPY or pybind11_tests.compiler_info.startswith('Homebrew Clang'))",
+ raises=RuntimeError,
+ reason="See Issue #2847, PR #2999, PR #4324",
+)
+def test_cross_module_exception_translator():
+ with pytest.raises(KeyError):
+ # translator registered in cross_module_tests
+ m.throw_should_be_translated_to_key_error()
+
def test_python_call_in_catch():
d = {}
@@ -53,16 +92,17 @@ def test_python_call_in_catch():
def ignore_pytest_unraisable_warning(f):
unraisable = "PytestUnraisableExceptionWarning"
if hasattr(pytest, unraisable): # Python >= 3.8 and pytest >= 6
- dec = pytest.mark.filterwarnings("ignore::pytest.{}".format(unraisable))
+ dec = pytest.mark.filterwarnings(f"ignore::pytest.{unraisable}")
return dec(f)
- else:
- return f
+ return f
+# TODO: find out why this fails on PyPy, https://foss.heptapod.net/pypy/pypy/-/issues/3583
+@pytest.mark.xfail(env.PYPY, reason="Failure on PyPy 3.8 (7.3.7)", strict=False)
@ignore_pytest_unraisable_warning
def test_python_alreadyset_in_destructor(monkeypatch, capsys):
hooked = False
- triggered = [False] # mutable, so Python 2.7 closure can modify it
+ triggered = False
if hasattr(sys, "unraisablehook"): # Python 3.8+
hooked = True
@@ -72,7 +112,8 @@ def test_python_alreadyset_in_destructor(monkeypatch, capsys):
def hook(unraisable_hook_args):
exc_type, exc_value, exc_tb, err_msg, obj = unraisable_hook_args
if obj == "already_set demo":
- triggered[0] = True
+ nonlocal triggered
+ triggered = True
default_hook(unraisable_hook_args)
return
@@ -81,11 +122,11 @@ def test_python_alreadyset_in_destructor(monkeypatch, capsys):
assert m.python_alreadyset_in_destructor("already_set demo") is True
if hooked:
- assert triggered[0] is True
+ assert triggered is True
_, captured_stderr = capsys.readouterr()
- # Error message is different in Python 2 and 3, check for words that appear in both
- assert "ignored" in captured_stderr and "already_set demo" in captured_stderr
+ assert captured_stderr.startswith("Exception ignored in: 'already_set demo'")
+ assert captured_stderr.rstrip().endswith("KeyError: 'bar'")
def test_exception_matches():
@@ -141,11 +182,11 @@ def test_custom(msg):
m.throws5_1()
assert msg(excinfo.value) == "MyException5 subclass"
- with pytest.raises(m.MyException5) as excinfo:
+ with pytest.raises(m.MyException5) as excinfo: # noqa: PT012
try:
m.throws5()
- except m.MyException5_1:
- raise RuntimeError("Exception error: caught child from parent")
+ except m.MyException5_1 as err:
+ raise RuntimeError("Exception error: caught child from parent") from err
assert msg(excinfo.value) == "this is a helper-defined translated exception"
@@ -170,7 +211,7 @@ def test_nested_throws(capture):
m.try_catch(m.MyException5, throw_myex)
assert str(excinfo.value) == "nested error"
- def pycatch(exctype, f, *args):
+ def pycatch(exctype, f, *args): # noqa: ARG001
try:
f(*args)
except m.MyException as e:
@@ -199,11 +240,174 @@ def test_nested_throws(capture):
assert str(excinfo.value) == "this is a helper-defined translated exception"
+def test_throw_nested_exception():
+ with pytest.raises(RuntimeError) as excinfo:
+ m.throw_nested_exception()
+ assert str(excinfo.value) == "Outer Exception"
+ assert str(excinfo.value.__cause__) == "Inner Exception"
+
+
# This can often happen if you wrap a pybind11 class in a Python wrapper
def test_invalid_repr():
- class MyRepr(object):
+ class MyRepr:
def __repr__(self):
raise AttributeError("Example error")
with pytest.raises(TypeError):
m.simple_bool_passthrough(MyRepr())
+
+
+def test_local_translator(msg):
+ """Tests that a local translator works and that the local translator from
+ the cross module is not applied"""
+ with pytest.raises(RuntimeError) as excinfo:
+ m.throws6()
+ assert msg(excinfo.value) == "MyException6 only handled in this module"
+
+ with pytest.raises(RuntimeError) as excinfo:
+ m.throws_local_error()
+ assert not isinstance(excinfo.value, KeyError)
+ assert msg(excinfo.value) == "never caught"
+
+ with pytest.raises(Exception) as excinfo:
+ m.throws_local_simple_error()
+ assert not isinstance(excinfo.value, cm.LocalSimpleException)
+ assert msg(excinfo.value) == "this mod"
+
+
+def test_error_already_set_message_with_unicode_surrogate(): # Issue #4288
+ assert m.error_already_set_what(RuntimeError, "\ud927") == (
+ "RuntimeError: \\ud927",
+ False,
+ )
+
+
+def test_error_already_set_message_with_malformed_utf8():
+ assert m.error_already_set_what(RuntimeError, b"\x80") == (
+ "RuntimeError: b'\\x80'",
+ False,
+ )
+
+
+class FlakyException(Exception):
+ def __init__(self, failure_point):
+ if failure_point == "failure_point_init":
+ raise ValueError("triggered_failure_point_init")
+ self.failure_point = failure_point
+
+ def __str__(self):
+ if self.failure_point == "failure_point_str":
+ raise ValueError("triggered_failure_point_str")
+ return "FlakyException.__str__"
+
+
+@pytest.mark.parametrize(
+ ("exc_type", "exc_value", "expected_what"),
+ [
+ (ValueError, "plain_str", "ValueError: plain_str"),
+ (ValueError, ("tuple_elem",), "ValueError: tuple_elem"),
+ (FlakyException, ("happy",), "FlakyException: FlakyException.__str__"),
+ ],
+)
+def test_error_already_set_what_with_happy_exceptions(
+ exc_type, exc_value, expected_what
+):
+ what, py_err_set_after_what = m.error_already_set_what(exc_type, exc_value)
+ assert not py_err_set_after_what
+ assert what == expected_what
+
+
+def _test_flaky_exception_failure_point_init_before_py_3_12():
+ with pytest.raises(RuntimeError) as excinfo:
+ m.error_already_set_what(FlakyException, ("failure_point_init",))
+ lines = str(excinfo.value).splitlines()
+ # PyErr_NormalizeException replaces the original FlakyException with ValueError:
+ assert lines[:3] == [
+ "pybind11::error_already_set: MISMATCH of original and normalized active exception types:"
+ " ORIGINAL FlakyException REPLACED BY ValueError: triggered_failure_point_init",
+ "",
+ "At:",
+ ]
+ # Checking the first two lines of the traceback as formatted in error_string():
+ assert "test_exceptions.py(" in lines[3]
+ assert lines[3].endswith("): __init__")
+ assert lines[4].endswith(
+ "): _test_flaky_exception_failure_point_init_before_py_3_12"
+ )
+
+
+def _test_flaky_exception_failure_point_init_py_3_12():
+ # Behavior change in Python 3.12: https://github.com/python/cpython/issues/102594
+ what, py_err_set_after_what = m.error_already_set_what(
+ FlakyException, ("failure_point_init",)
+ )
+ assert not py_err_set_after_what
+ lines = what.splitlines()
+ assert lines[0].endswith("ValueError[WITH __notes__]: triggered_failure_point_init")
+ assert lines[1] == "__notes__ (len=1):"
+ assert "Normalization failed:" in lines[2]
+ assert "FlakyException" in lines[2]
+
+
+@pytest.mark.skipif(
+ "env.PYPY and sys.version_info[:2] < (3, 12)",
+ reason="PyErr_NormalizeException Segmentation fault",
+)
+def test_flaky_exception_failure_point_init():
+ if sys.version_info[:2] < (3, 12):
+ _test_flaky_exception_failure_point_init_before_py_3_12()
+ else:
+ _test_flaky_exception_failure_point_init_py_3_12()
+
+
+def test_flaky_exception_failure_point_str():
+ what, py_err_set_after_what = m.error_already_set_what(
+ FlakyException, ("failure_point_str",)
+ )
+ assert not py_err_set_after_what
+ lines = what.splitlines()
+ n = 3 if env.PYPY and len(lines) == 3 else 5
+ assert (
+ lines[:n]
+ == [
+ "FlakyException: <MESSAGE UNAVAILABLE DUE TO ANOTHER EXCEPTION>",
+ "",
+ "MESSAGE UNAVAILABLE DUE TO EXCEPTION: ValueError: triggered_failure_point_str",
+ "",
+ "At:",
+ ][:n]
+ )
+
+
+def test_cross_module_interleaved_error_already_set():
+ with pytest.raises(RuntimeError) as excinfo:
+ m.test_cross_module_interleaved_error_already_set()
+ assert str(excinfo.value) in (
+ "2nd error.", # Almost all platforms.
+ "RuntimeError: 2nd error.", # Some PyPy builds (seen under macOS).
+ )
+
+
+def test_error_already_set_double_restore():
+ m.test_error_already_set_double_restore(True) # dry_run
+ with pytest.raises(RuntimeError) as excinfo:
+ m.test_error_already_set_double_restore(False)
+ assert str(excinfo.value) == (
+ "Internal error: pybind11::detail::error_fetch_and_normalize::restore()"
+ " called a second time. ORIGINAL ERROR: ValueError: Random error."
+ )
+
+
+def test_pypy_oserror_normalization():
+ # https://github.com/pybind/pybind11/issues/4075
+ what = m.test_pypy_oserror_normalization()
+ assert "this_filename_must_not_exist" in what
+
+
+def test_fn_cast_int_exception():
+ with pytest.raises(RuntimeError) as excinfo:
+ m.test_fn_cast_int(lambda: None)
+
+ assert str(excinfo.value).startswith(
+ "Unable to cast Python instance of type <class 'NoneType'> to C++ type"
+ )