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


49
50
51
52
53
54
55
56
57
58
59
60
# 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()


61
62
def test_python_call_in_catch():
    d = {}
63
    assert m.python_call_in_destructor(d) is True
64
65
66
    assert d["good"] is True


67
68
69
70
71
72
73
74
75
76
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
77
78
79
80
def test_python_alreadyset_in_destructor(monkeypatch, capsys):
    hooked = False
    triggered = [False]  # mutable, so Python 2.7 closure can modify it

81
    if hasattr(sys, "unraisablehook"):  # Python 3.8+
82
        hooked = True
83
84
        # Don't take `sys.unraisablehook`, as that's overwritten by pytest
        default_hook = sys.__unraisablehook__
85
86
87

        def hook(unraisable_hook_args):
            exc_type, exc_value, exc_tb, err_msg, obj = unraisable_hook_args
88
            if obj == "already_set demo":
89
90
91
92
93
                triggered[0] = True
            default_hook(unraisable_hook_args)
            return

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

96
    assert m.python_alreadyset_in_destructor("already_set demo") is True
97
98
99
100
101
    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
102
    assert "ignored" in captured_stderr and "already_set demo" in captured_stderr
103
104


105
def test_exception_matches():
106
107
108
    assert m.exception_matches()
    assert m.exception_matches_base()
    assert m.modulenotfound_exception_matches_base()
109
110


Dean Moldovan's avatar
Dean Moldovan committed
111
def test_custom(msg):
112
113
114
    # Can we catch a MyException?
    with pytest.raises(m.MyException) as excinfo:
        m.throws1()
Dean Moldovan's avatar
Dean Moldovan committed
115
116
117
118
    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:
119
        m.throws2()
Dean Moldovan's avatar
Dean Moldovan committed
120
121
122
123
    assert msg(excinfo.value) == "this error should go to a standard Python exception"

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

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

132
    # Can we fall-through to the default handler?
Dean Moldovan's avatar
Dean Moldovan committed
133
    with pytest.raises(RuntimeError) as excinfo:
134
        m.throws_logic_error()
135
136
137
    assert (
        msg(excinfo.value) == "this error should fall through to the standard handler"
    )
138

139
140
141
142
    # OverFlow error translation.
    with pytest.raises(OverflowError) as excinfo:
        m.throws_overflow_error()

143
    # Can we handle a helper-declared exception?
144
145
    with pytest.raises(m.MyException5) as excinfo:
        m.throws5()
146
147
148
    assert msg(excinfo.value) == "this is a helper-defined translated exception"

    # Exception subclassing:
149
150
    with pytest.raises(m.MyException5) as excinfo:
        m.throws5_1()
151
    assert msg(excinfo.value) == "MyException5 subclass"
152
    assert isinstance(excinfo.value, m.MyException5_1)
153

154
155
    with pytest.raises(m.MyException5_1) as excinfo:
        m.throws5_1()
156
157
    assert msg(excinfo.value) == "MyException5 subclass"

158
    with pytest.raises(m.MyException5) as excinfo:
159
        try:
160
161
            m.throws5()
        except m.MyException5_1:
162
163
            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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195


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(
196
197
198
199
200
201
202
            m.MyException5,
            pycatch,
            m.MyException,
            m.try_catch,
            m.MyException,
            throw_myex5,
        )
Jason Rhinelander's avatar
Jason Rhinelander committed
203
204
205
206
207
208
209
210
211
212
213
    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"
214
215
216
217
218
219
220
221
222
223


# 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())