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