blob: 57f681c66fd396877acc877a643a3bdd2097af24 [file] [log] [blame]
Dean Moldovana0c1ccf2016-08-12 13:50:00 +02001"""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
Dean Moldovana0c1ccf2016-08-12 13:50:00 +020011import sys
12import contextlib
Wenzel Jakob1d1f81b2016-12-16 15:00:46 +010013import platform
14import gc
Dean Moldovana0c1ccf2016-08-12 13:50:00 +020015
16_unicode_marker = re.compile(r'u(\'[^\']*\')')
Dean Moldovanbad17402016-11-20 21:21:54 +010017_long_marker = re.compile(r'([0-9])L')
18_hexadecimal = re.compile(r'0x[0-9a-fA-F]+')
Dean Moldovana0c1ccf2016-08-12 13:50:00 +020019
Jeremy Maitin-Sheparda3f4a0e2019-07-18 00:02:35 -070020# 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
Dean Moldovana0c1ccf2016-08-12 13:50:00 +020025
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 = ""
Dean Moldovan67990d92016-08-29 18:03:34 +020077 self.err = ""
Dean Moldovana0c1ccf2016-08-12 13:50:00 +020078
Dean Moldovana0c1ccf2016-08-12 13:50:00 +020079 def __enter__(self):
Dean Moldovan81511be2016-09-07 00:50:10 +020080 self.capfd.readouterr()
Dean Moldovana0c1ccf2016-08-12 13:50:00 +020081 return self
82
Wenzel Jakob25abf7e2019-02-04 17:09:47 +010083 def __exit__(self, *args):
Dean Moldovan81511be2016-09-07 00:50:10 +020084 self.out, self.err = self.capfd.readouterr()
Dean Moldovana0c1ccf2016-08-12 13:50:00 +020085
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
Dean Moldovan67990d92016-08-29 18:03:34 +0200105 @property
106 def stderr(self):
107 return Output(self.err)
108
Dean Moldovana0c1ccf2016-08-12 13:50:00 +0200109
110@pytest.fixture
Dean Moldovand47febc2017-03-10 15:42:42 +0100111def capture(capsys):
112 """Extended `capsys` with context manager and custom equality operators"""
113 return Capture(capsys)
Dean Moldovana0c1ccf2016-08-12 13:50:00 +0200114
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
Wenzel Jakob1d1f81b2016-12-16 15:00:46 +0100186def 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
Guilhem Saurele7ef34f2019-01-23 14:22:39 +0100193def pytest_configure():
Dean Moldovana0c1ccf2016-08-12 13:50:00 +0200194 """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:
Jason Rhinelander391c7542017-07-25 16:47:36 -0400204 from pybind11_tests.eigen import have_eigen
Dean Moldovana0c1ccf2016-08-12 13:50:00 +0200205 except ImportError:
206 have_eigen = False
Wenzel Jakob1d1f81b2016-12-16 15:00:46 +0100207 pypy = platform.python_implementation() == "PyPy"
Dean Moldovana0c1ccf2016-08-12 13:50:00 +0200208
209 skipif = pytest.mark.skipif
Guilhem Saurele7ef34f2019-01-23 14:22:39 +0100210 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
Dean Moldovan23919172016-08-25 17:08:09 +0200221
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:
Dean Moldovanbad17402016-11-20 21:21:54 +0100237 import pybind11_tests # noqa: F401 imported but unused
Dean Moldovan23919172016-08-25 17:08:09 +0200238 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()