"vscode:/vscode.git/clone" did not exist on "cf0d0f9d5ad79349e313694ddf3efeef7de21890"
test_exceptions.py 11 KB
Newer Older
1
2
import sys

Dean Moldovan's avatar
Dean Moldovan committed
3
4
import pytest

5
import env
6
import pybind11_cross_module_tests as cm
7
from pybind11_tests import exceptions as m
Dean Moldovan's avatar
Dean Moldovan committed
8

9

10
def test_std_exception(msg):
11
    with pytest.raises(RuntimeError) as excinfo:
12
        m.throw_std_exception()
13
14
15
    assert msg(excinfo.value) == "This exception was intentionally thrown."


16
17
def test_error_already_set(msg):
    with pytest.raises(RuntimeError) as excinfo:
18
        m.throw_already_set(False)
19
20
21
22
    assert (
        msg(excinfo.value)
        == "Internal error: pybind11::error_already_set called while Python error indicator not set."
    )
23
24

    with pytest.raises(ValueError) as excinfo:
25
        m.throw_already_set(True)
26
27
28
    assert msg(excinfo.value) == "foo"


29
30
31
32
33
34
35
36
37
38
39
40
41
42
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"


43
def test_cross_module_exceptions(msg):
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
    with pytest.raises(RuntimeError) as excinfo:
        cm.raise_runtime_error()
    assert str(excinfo.value) == "My runtime error"

    with pytest.raises(ValueError) as excinfo:
        cm.raise_value_error()
    assert str(excinfo.value) == "My value error"

    with pytest.raises(ValueError) as excinfo:
        cm.throw_pybind_value_error()
    assert str(excinfo.value) == "pybind11 value error"

    with pytest.raises(TypeError) as excinfo:
        cm.throw_pybind_type_error()
    assert str(excinfo.value) == "pybind11 type error"

    with pytest.raises(StopIteration) as excinfo:
        cm.throw_stop_iteration()

63
64
65
66
67
68
69
70
71
    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'"

72

73
74
75
76
77
78
79
80
81
82
83
84
# TODO: FIXME
@pytest.mark.xfail(
    "env.PYPY and env.MACOS",
    raises=RuntimeError,
    reason="Expected failure with PyPy and libc++ (Issue #2847 & PR #2999)",
)
def test_cross_module_exception_translator():
    with pytest.raises(KeyError):
        # translator registered in cross_module_tests
        m.throw_should_be_translated_to_key_error()


85
86
def test_python_call_in_catch():
    d = {}
87
    assert m.python_call_in_destructor(d) is True
88
89
90
    assert d["good"] is True


91
92
93
def ignore_pytest_unraisable_warning(f):
    unraisable = "PytestUnraisableExceptionWarning"
    if hasattr(pytest, unraisable):  # Python >= 3.8 and pytest >= 6
94
        dec = pytest.mark.filterwarnings(f"ignore::pytest.{unraisable}")
95
96
97
98
99
        return dec(f)
    else:
        return f


100
101
# 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)
102
@ignore_pytest_unraisable_warning
103
104
def test_python_alreadyset_in_destructor(monkeypatch, capsys):
    hooked = False
105
    triggered = False
106

107
    if hasattr(sys, "unraisablehook"):  # Python 3.8+
108
        hooked = True
109
110
        # Don't take `sys.unraisablehook`, as that's overwritten by pytest
        default_hook = sys.__unraisablehook__
111
112
113

        def hook(unraisable_hook_args):
            exc_type, exc_value, exc_tb, err_msg, obj = unraisable_hook_args
114
            if obj == "already_set demo":
115
116
                nonlocal triggered
                triggered = True
117
118
119
120
            default_hook(unraisable_hook_args)
            return

        # Use monkeypatch so pytest can apply and remove the patch as appropriate
121
        monkeypatch.setattr(sys, "unraisablehook", hook)
122

123
    assert m.python_alreadyset_in_destructor("already_set demo") is True
124
    if hooked:
125
        assert triggered is True
126
127

    _, captured_stderr = capsys.readouterr()
128
129
    assert captured_stderr.startswith("Exception ignored in: 'already_set demo'")
    assert captured_stderr.rstrip().endswith("KeyError: 'bar'")
130
131


132
def test_exception_matches():
133
134
135
    assert m.exception_matches()
    assert m.exception_matches_base()
    assert m.modulenotfound_exception_matches_base()
136
137


Dean Moldovan's avatar
Dean Moldovan committed
138
def test_custom(msg):
139
140
141
    # Can we catch a MyException?
    with pytest.raises(m.MyException) as excinfo:
        m.throws1()
Dean Moldovan's avatar
Dean Moldovan committed
142
143
144
145
    assert msg(excinfo.value) == "this error should go to a custom type"

    # Can we translate to standard Python exceptions?
    with pytest.raises(RuntimeError) as excinfo:
146
        m.throws2()
Dean Moldovan's avatar
Dean Moldovan committed
147
148
149
150
    assert msg(excinfo.value) == "this error should go to a standard Python exception"

    # Can we handle unknown exceptions?
    with pytest.raises(RuntimeError) as excinfo:
151
        m.throws3()
Dean Moldovan's avatar
Dean Moldovan committed
152
153
154
    assert msg(excinfo.value) == "Caught an unknown exception!"

    # Can we delegate to another handler by rethrowing?
155
156
    with pytest.raises(m.MyException) as excinfo:
        m.throws4()
Dean Moldovan's avatar
Dean Moldovan committed
157
158
    assert msg(excinfo.value) == "this error is rethrown"

159
    # Can we fall-through to the default handler?
Dean Moldovan's avatar
Dean Moldovan committed
160
    with pytest.raises(RuntimeError) as excinfo:
161
        m.throws_logic_error()
162
163
164
    assert (
        msg(excinfo.value) == "this error should fall through to the standard handler"
    )
165

166
167
168
169
    # OverFlow error translation.
    with pytest.raises(OverflowError) as excinfo:
        m.throws_overflow_error()

170
    # Can we handle a helper-declared exception?
171
172
    with pytest.raises(m.MyException5) as excinfo:
        m.throws5()
173
174
175
    assert msg(excinfo.value) == "this is a helper-defined translated exception"

    # Exception subclassing:
176
177
    with pytest.raises(m.MyException5) as excinfo:
        m.throws5_1()
178
    assert msg(excinfo.value) == "MyException5 subclass"
179
    assert isinstance(excinfo.value, m.MyException5_1)
180

181
182
    with pytest.raises(m.MyException5_1) as excinfo:
        m.throws5_1()
183
184
    assert msg(excinfo.value) == "MyException5 subclass"

185
    with pytest.raises(m.MyException5) as excinfo:
186
        try:
187
188
            m.throws5()
        except m.MyException5_1:
189
190
            raise RuntimeError("Exception error: caught child from parent")
    assert msg(excinfo.value) == "this is a helper-defined translated exception"
Jason Rhinelander's avatar
Jason Rhinelander committed
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222


def test_nested_throws(capture):
    """Tests nested (e.g. C++ -> Python -> C++) exception handling"""

    def throw_myex():
        raise m.MyException("nested error")

    def throw_myex5():
        raise m.MyException5("nested error 5")

    # In the comments below, the exception is caught in the first step, thrown in the last step

    # C++ -> Python
    with capture:
        m.try_catch(m.MyException5, throw_myex5)
    assert str(capture).startswith("MyException5: nested error 5")

    # Python -> C++ -> Python
    with pytest.raises(m.MyException) as excinfo:
        m.try_catch(m.MyException5, throw_myex)
    assert str(excinfo.value) == "nested error"

    def pycatch(exctype, f, *args):
        try:
            f(*args)
        except m.MyException as e:
            print(e)

    # C++ -> Python -> C++ -> Python
    with capture:
        m.try_catch(
223
224
225
226
227
228
229
            m.MyException5,
            pycatch,
            m.MyException,
            m.try_catch,
            m.MyException,
            throw_myex5,
        )
Jason Rhinelander's avatar
Jason Rhinelander committed
230
231
232
233
234
235
236
237
238
239
240
    assert str(capture).startswith("MyException5: nested error 5")

    # C++ -> Python -> C++
    with capture:
        m.try_catch(m.MyException, pycatch, m.MyException5, m.throws4)
    assert capture == "this error is rethrown"

    # Python -> C++ -> Python -> C++
    with pytest.raises(m.MyException5) as excinfo:
        m.try_catch(m.MyException, pycatch, m.MyException, m.throws5)
    assert str(excinfo.value) == "this is a helper-defined translated exception"
241
242


243
244
245
246
247
248
249
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"


250
251
# This can often happen if you wrap a pybind11 class in a Python wrapper
def test_invalid_repr():
252
    class MyRepr:
253
254
255
256
257
        def __repr__(self):
            raise AttributeError("Example error")

    with pytest.raises(TypeError):
        m.simple_bool_passthrough(MyRepr())
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275


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"
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325


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


@pytest.mark.skipif("env.PYPY", reason="PyErr_NormalizeException Segmentation fault")
def test_flaky_exception_failure_point_init():
    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()
    # PyErr_NormalizeException replaces the original FlakyException with ValueError:
    assert lines[:3] == ["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")


def test_flaky_exception_failure_point_str():
    # The error_already_set ctor fails due to a ValueError in error_string():
    with pytest.raises(ValueError) as excinfo:
        m.error_already_set_what(FlakyException, ("failure_point_str",))
    assert str(excinfo.value) == "triggered_failure_point_str"
326
327
328
329
330
331
332
333
334


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