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

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

6
7
import env  # noqa: F401

8
from pybind11_tests import exceptions as m
9
import pybind11_cross_module_tests as cm
Dean Moldovan's avatar
Dean Moldovan committed
10

11

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


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

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


28
def test_cross_module_exceptions(msg):
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
    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()

48
49
50
51
52
53
54
55
56
    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'"

57

58
59
60
61
62
63
64
65
66
67
68
69
# 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()


70
71
def test_python_call_in_catch():
    d = {}
72
    assert m.python_call_in_destructor(d) is True
73
74
75
    assert d["good"] is True


76
77
78
79
80
81
82
83
84
85
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
86
87
88
89
def test_python_alreadyset_in_destructor(monkeypatch, capsys):
    hooked = False
    triggered = [False]  # mutable, so Python 2.7 closure can modify it

90
    if hasattr(sys, "unraisablehook"):  # Python 3.8+
91
        hooked = True
92
93
        # Don't take `sys.unraisablehook`, as that's overwritten by pytest
        default_hook = sys.__unraisablehook__
94
95
96

        def hook(unraisable_hook_args):
            exc_type, exc_value, exc_tb, err_msg, obj = unraisable_hook_args
97
            if obj == "already_set demo":
98
99
100
101
102
                triggered[0] = True
            default_hook(unraisable_hook_args)
            return

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

105
    assert m.python_alreadyset_in_destructor("already_set demo") is True
106
107
108
109
110
    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
111
    assert "ignored" in captured_stderr and "already_set demo" in captured_stderr
112
113


114
def test_exception_matches():
115
116
117
    assert m.exception_matches()
    assert m.exception_matches_base()
    assert m.modulenotfound_exception_matches_base()
118
119


Dean Moldovan's avatar
Dean Moldovan committed
120
def test_custom(msg):
121
122
123
    # Can we catch a MyException?
    with pytest.raises(m.MyException) as excinfo:
        m.throws1()
Dean Moldovan's avatar
Dean Moldovan committed
124
125
126
127
    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:
128
        m.throws2()
Dean Moldovan's avatar
Dean Moldovan committed
129
130
131
132
    assert msg(excinfo.value) == "this error should go to a standard Python exception"

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

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

141
    # Can we fall-through to the default handler?
Dean Moldovan's avatar
Dean Moldovan committed
142
    with pytest.raises(RuntimeError) as excinfo:
143
        m.throws_logic_error()
144
145
146
    assert (
        msg(excinfo.value) == "this error should fall through to the standard handler"
    )
147

148
149
150
151
    # OverFlow error translation.
    with pytest.raises(OverflowError) as excinfo:
        m.throws_overflow_error()

152
    # Can we handle a helper-declared exception?
153
154
    with pytest.raises(m.MyException5) as excinfo:
        m.throws5()
155
156
157
    assert msg(excinfo.value) == "this is a helper-defined translated exception"

    # Exception subclassing:
158
159
    with pytest.raises(m.MyException5) as excinfo:
        m.throws5_1()
160
    assert msg(excinfo.value) == "MyException5 subclass"
161
    assert isinstance(excinfo.value, m.MyException5_1)
162

163
164
    with pytest.raises(m.MyException5_1) as excinfo:
        m.throws5_1()
165
166
    assert msg(excinfo.value) == "MyException5 subclass"

167
    with pytest.raises(m.MyException5) as excinfo:
168
        try:
169
170
            m.throws5()
        except m.MyException5_1:
171
172
            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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204


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(
205
206
207
208
209
210
211
            m.MyException5,
            pycatch,
            m.MyException,
            m.try_catch,
            m.MyException,
            throw_myex5,
        )
Jason Rhinelander's avatar
Jason Rhinelander committed
212
213
214
215
216
217
218
219
220
221
222
    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"
223
224
225
226
227
228
229
230
231
232


# 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())
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250


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"