test_exceptions.py 8.41 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
3
import sys

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

6
import env  # noqa: F401
7
import pybind11_cross_module_tests as cm
8
from pybind11_tests import exceptions as m
Dean Moldovan's avatar
Dean Moldovan committed
9

10

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


17
18
def test_error_already_set(msg):
    with pytest.raises(RuntimeError) as excinfo:
19
        m.throw_already_set(False)
20
21
22
    assert msg(excinfo.value) == "Unknown internal error occurred"

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


27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@pytest.mark.skipif("env.PY2")
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"


@pytest.mark.skipif("env.PY2")
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
94
95
96
97
98
99
100
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))
        return dec(f)
    else:
        return f


@ignore_pytest_unraisable_warning
101
102
103
104
def test_python_alreadyset_in_destructor(monkeypatch, capsys):
    hooked = False
    triggered = [False]  # mutable, so Python 2.7 closure can modify it

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

        def hook(unraisable_hook_args):
            exc_type, exc_value, exc_tb, err_msg, obj = unraisable_hook_args
112
            if obj == "already_set demo":
113
114
115
116
117
                triggered[0] = True
            default_hook(unraisable_hook_args)
            return

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

120
    assert m.python_alreadyset_in_destructor("already_set demo") is True
121
122
123
124
125
    if hooked:
        assert triggered[0] is True

    _, captured_stderr = capsys.readouterr()
    # Error message is different in Python 2 and 3, check for words that appear in both
126
    assert "ignored" in captured_stderr and "already_set demo" in captured_stderr
127
128


129
def test_exception_matches():
130
131
132
    assert m.exception_matches()
    assert m.exception_matches_base()
    assert m.modulenotfound_exception_matches_base()
133
134


Dean Moldovan's avatar
Dean Moldovan committed
135
def test_custom(msg):
136
137
138
    # Can we catch a MyException?
    with pytest.raises(m.MyException) as excinfo:
        m.throws1()
Dean Moldovan's avatar
Dean Moldovan committed
139
140
141
142
    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:
143
        m.throws2()
Dean Moldovan's avatar
Dean Moldovan committed
144
145
146
147
    assert msg(excinfo.value) == "this error should go to a standard Python exception"

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

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

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

163
164
165
166
    # OverFlow error translation.
    with pytest.raises(OverflowError) as excinfo:
        m.throws_overflow_error()

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

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

178
179
    with pytest.raises(m.MyException5_1) as excinfo:
        m.throws5_1()
180
181
    assert msg(excinfo.value) == "MyException5 subclass"

182
    with pytest.raises(m.MyException5) as excinfo:
183
        try:
184
185
            m.throws5()
        except m.MyException5_1:
186
187
            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
188
189
190
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


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(
220
221
222
223
224
225
226
            m.MyException5,
            pycatch,
            m.MyException,
            m.try_catch,
            m.MyException,
            throw_myex5,
        )
Jason Rhinelander's avatar
Jason Rhinelander committed
227
228
229
230
231
232
233
234
235
236
237
    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"
238
239
240
241
242
243
244
245
246
247


# This can often happen if you wrap a pybind11 class in a Python wrapper
def test_invalid_repr():
    class MyRepr(object):
        def __repr__(self):
            raise AttributeError("Example error")

    with pytest.raises(TypeError):
        m.simple_bool_passthrough(MyRepr())
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265


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"