conftest.py revision 14299:2fbea9df56d2
1"""pytest configuration
2
3Extends output capture as needed by pybind11: ignore constructors, optional unordered lines.
4Adds docstring and exceptions message sanitizers: ignore Python 2 vs 3 differences.
5"""
6
7import pytest
8import textwrap
9import difflib
10import re
11import sys
12import contextlib
13import platform
14import gc
15
16_unicode_marker = re.compile(r'u(\'[^\']*\')')
17_long_marker = re.compile(r'([0-9])L')
18_hexadecimal = re.compile(r'0x[0-9a-fA-F]+')
19
20# test_async.py requires support for async and await
21collect_ignore = []
22if sys.version_info[:2] < (3, 5):
23    collect_ignore.append("test_async.py")
24
25
26def _strip_and_dedent(s):
27    """For triple-quote strings"""
28    return textwrap.dedent(s.lstrip('\n').rstrip())
29
30
31def _split_and_sort(s):
32    """For output which does not require specific line order"""
33    return sorted(_strip_and_dedent(s).splitlines())
34
35
36def _make_explanation(a, b):
37    """Explanation for a failed assert -- the a and b arguments are List[str]"""
38    return ["--- actual / +++ expected"] + [line.strip('\n') for line in difflib.ndiff(a, b)]
39
40
41class Output(object):
42    """Basic output post-processing and comparison"""
43    def __init__(self, string):
44        self.string = string
45        self.explanation = []
46
47    def __str__(self):
48        return self.string
49
50    def __eq__(self, other):
51        # Ignore constructor/destructor output which is prefixed with "###"
52        a = [line for line in self.string.strip().splitlines() if not line.startswith("###")]
53        b = _strip_and_dedent(other).splitlines()
54        if a == b:
55            return True
56        else:
57            self.explanation = _make_explanation(a, b)
58            return False
59
60
61class Unordered(Output):
62    """Custom comparison for output without strict line ordering"""
63    def __eq__(self, other):
64        a = _split_and_sort(self.string)
65        b = _split_and_sort(other)
66        if a == b:
67            return True
68        else:
69            self.explanation = _make_explanation(a, b)
70            return False
71
72
73class Capture(object):
74    def __init__(self, capfd):
75        self.capfd = capfd
76        self.out = ""
77        self.err = ""
78
79    def __enter__(self):
80        self.capfd.readouterr()
81        return self
82
83    def __exit__(self, *args):
84        self.out, self.err = self.capfd.readouterr()
85
86    def __eq__(self, other):
87        a = Output(self.out)
88        b = other
89        if a == b:
90            return True
91        else:
92            self.explanation = a.explanation
93            return False
94
95    def __str__(self):
96        return self.out
97
98    def __contains__(self, item):
99        return item in self.out
100
101    @property
102    def unordered(self):
103        return Unordered(self.out)
104
105    @property
106    def stderr(self):
107        return Output(self.err)
108
109
110@pytest.fixture
111def capture(capsys):
112    """Extended `capsys` with context manager and custom equality operators"""
113    return Capture(capsys)
114
115
116class SanitizedString(object):
117    def __init__(self, sanitizer):
118        self.sanitizer = sanitizer
119        self.string = ""
120        self.explanation = []
121
122    def __call__(self, thing):
123        self.string = self.sanitizer(thing)
124        return self
125
126    def __eq__(self, other):
127        a = self.string
128        b = _strip_and_dedent(other)
129        if a == b:
130            return True
131        else:
132            self.explanation = _make_explanation(a.splitlines(), b.splitlines())
133            return False
134
135
136def _sanitize_general(s):
137    s = s.strip()
138    s = s.replace("pybind11_tests.", "m.")
139    s = s.replace("unicode", "str")
140    s = _long_marker.sub(r"\1", s)
141    s = _unicode_marker.sub(r"\1", s)
142    return s
143
144
145def _sanitize_docstring(thing):
146    s = thing.__doc__
147    s = _sanitize_general(s)
148    return s
149
150
151@pytest.fixture
152def doc():
153    """Sanitize docstrings and add custom failure explanation"""
154    return SanitizedString(_sanitize_docstring)
155
156
157def _sanitize_message(thing):
158    s = str(thing)
159    s = _sanitize_general(s)
160    s = _hexadecimal.sub("0", s)
161    return s
162
163
164@pytest.fixture
165def msg():
166    """Sanitize messages and add custom failure explanation"""
167    return SanitizedString(_sanitize_message)
168
169
170# noinspection PyUnusedLocal
171def pytest_assertrepr_compare(op, left, right):
172    """Hook to insert custom failure explanation"""
173    if hasattr(left, 'explanation'):
174        return left.explanation
175
176
177@contextlib.contextmanager
178def suppress(exception):
179    """Suppress the desired exception"""
180    try:
181        yield
182    except exception:
183        pass
184
185
186def gc_collect():
187    ''' Run the garbage collector twice (needed when running
188    reference counting tests with PyPy) '''
189    gc.collect()
190    gc.collect()
191
192
193def pytest_configure():
194    """Add import suppression and test requirements to `pytest` namespace"""
195    try:
196        import numpy as np
197    except ImportError:
198        np = None
199    try:
200        import scipy
201    except ImportError:
202        scipy = None
203    try:
204        from pybind11_tests.eigen import have_eigen
205    except ImportError:
206        have_eigen = False
207    pypy = platform.python_implementation() == "PyPy"
208
209    skipif = pytest.mark.skipif
210    pytest.suppress = suppress
211    pytest.requires_numpy = skipif(not np, reason="numpy is not installed")
212    pytest.requires_scipy = skipif(not np, reason="scipy is not installed")
213    pytest.requires_eigen_and_numpy = skipif(not have_eigen or not np,
214                                             reason="eigen and/or numpy are not installed")
215    pytest.requires_eigen_and_scipy = skipif(
216        not have_eigen or not scipy, reason="eigen and/or scipy are not installed")
217    pytest.unsupported_on_pypy = skipif(pypy, reason="unsupported on PyPy")
218    pytest.unsupported_on_py2 = skipif(sys.version_info.major < 3,
219                                       reason="unsupported on Python 2.x")
220    pytest.gc_collect = gc_collect
221
222
223def _test_import_pybind11():
224    """Early diagnostic for test module initialization errors
225
226    When there is an error during initialization, the first import will report the
227    real error while all subsequent imports will report nonsense. This import test
228    is done early (in the pytest configuration file, before any tests) in order to
229    avoid the noise of having all tests fail with identical error messages.
230
231    Any possible exception is caught here and reported manually *without* the stack
232    trace. This further reduces noise since the trace would only show pytest internals
233    which are not useful for debugging pybind11 module issues.
234    """
235    # noinspection PyBroadException
236    try:
237        import pybind11_tests  # noqa: F401 imported but unused
238    except Exception as e:
239        print("Failed to import pybind11_tests from pytest:")
240        print("  {}: {}".format(type(e).__name__, e))
241        sys.exit(1)
242
243
244_test_import_pybind11()
245