blob: 1d7a24aa17813283a2e28f7ba05138e59ff27db0 [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
11import os
12import sys
13import contextlib
14
15_unicode_marker = re.compile(r'u(\'[^\']*\')')
16_long_marker = re.compile(r'([0-9])L')
17_hexadecimal = re.compile(r'0x[0-9a-fA-F]+')
18
19
20def _strip_and_dedent(s):
21 """For triple-quote strings"""
22 return textwrap.dedent(s.lstrip('\n').rstrip())
23
24
25def _split_and_sort(s):
26 """For output which does not require specific line order"""
27 return sorted(_strip_and_dedent(s).splitlines())
28
29
30def _make_explanation(a, b):
31 """Explanation for a failed assert -- the a and b arguments are List[str]"""
32 return ["--- actual / +++ expected"] + [line.strip('\n') for line in difflib.ndiff(a, b)]
33
34
35class Output(object):
36 """Basic output post-processing and comparison"""
37 def __init__(self, string):
38 self.string = string
39 self.explanation = []
40
41 def __str__(self):
42 return self.string
43
44 def __eq__(self, other):
45 # Ignore constructor/destructor output which is prefixed with "###"
46 a = [line for line in self.string.strip().splitlines() if not line.startswith("###")]
47 b = _strip_and_dedent(other).splitlines()
48 if a == b:
49 return True
50 else:
51 self.explanation = _make_explanation(a, b)
52 return False
53
54
55class Unordered(Output):
56 """Custom comparison for output without strict line ordering"""
57 def __eq__(self, other):
58 a = _split_and_sort(self.string)
59 b = _split_and_sort(other)
60 if a == b:
61 return True
62 else:
63 self.explanation = _make_explanation(a, b)
64 return False
65
66
67class Capture(object):
68 def __init__(self, capfd):
69 self.capfd = capfd
70 self.out = ""
Dean Moldovan67990d92016-08-29 18:03:34 +020071 self.err = ""
Dean Moldovana0c1ccf2016-08-12 13:50:00 +020072
Dean Moldovana0c1ccf2016-08-12 13:50:00 +020073 def __enter__(self):
Dean Moldovan81511be2016-09-07 00:50:10 +020074 self.capfd.readouterr()
Dean Moldovana0c1ccf2016-08-12 13:50:00 +020075 return self
76
77 def __exit__(self, *_):
Dean Moldovan81511be2016-09-07 00:50:10 +020078 self.out, self.err = self.capfd.readouterr()
Dean Moldovana0c1ccf2016-08-12 13:50:00 +020079
80 def __eq__(self, other):
81 a = Output(self.out)
82 b = other
83 if a == b:
84 return True
85 else:
86 self.explanation = a.explanation
87 return False
88
89 def __str__(self):
90 return self.out
91
92 def __contains__(self, item):
93 return item in self.out
94
95 @property
96 def unordered(self):
97 return Unordered(self.out)
98
Dean Moldovan67990d92016-08-29 18:03:34 +020099 @property
100 def stderr(self):
101 return Output(self.err)
102
Dean Moldovana0c1ccf2016-08-12 13:50:00 +0200103
104@pytest.fixture
105def capture(capfd):
106 """Extended `capfd` with context manager and custom equality operators"""
107 return Capture(capfd)
108
109
110class SanitizedString(object):
111 def __init__(self, sanitizer):
112 self.sanitizer = sanitizer
113 self.string = ""
114 self.explanation = []
115
116 def __call__(self, thing):
117 self.string = self.sanitizer(thing)
118 return self
119
120 def __eq__(self, other):
121 a = self.string
122 b = _strip_and_dedent(other)
123 if a == b:
124 return True
125 else:
126 self.explanation = _make_explanation(a.splitlines(), b.splitlines())
127 return False
128
129
130def _sanitize_general(s):
131 s = s.strip()
132 s = s.replace("pybind11_tests.", "m.")
133 s = s.replace("unicode", "str")
134 s = _long_marker.sub(r"\1", s)
135 s = _unicode_marker.sub(r"\1", s)
136 return s
137
138
139def _sanitize_docstring(thing):
140 s = thing.__doc__
141 s = _sanitize_general(s)
142 return s
143
144
145@pytest.fixture
146def doc():
147 """Sanitize docstrings and add custom failure explanation"""
148 return SanitizedString(_sanitize_docstring)
149
150
151def _sanitize_message(thing):
152 s = str(thing)
153 s = _sanitize_general(s)
154 s = _hexadecimal.sub("0", s)
155 return s
156
157
158@pytest.fixture
159def msg():
160 """Sanitize messages and add custom failure explanation"""
161 return SanitizedString(_sanitize_message)
162
163
164# noinspection PyUnusedLocal
165def pytest_assertrepr_compare(op, left, right):
166 """Hook to insert custom failure explanation"""
167 if hasattr(left, 'explanation'):
168 return left.explanation
169
170
171@contextlib.contextmanager
172def suppress(exception):
173 """Suppress the desired exception"""
174 try:
175 yield
176 except exception:
177 pass
178
179
180def pytest_namespace():
181 """Add import suppression and test requirements to `pytest` namespace"""
182 try:
183 import numpy as np
184 except ImportError:
185 np = None
186 try:
187 import scipy
188 except ImportError:
189 scipy = None
190 try:
191 from pybind11_tests import have_eigen
192 except ImportError:
193 have_eigen = False
194
195 skipif = pytest.mark.skipif
196 return {
197 'suppress': suppress,
198 'requires_numpy': skipif(not np, reason="numpy is not installed"),
199 'requires_scipy': skipif(not np, reason="scipy is not installed"),
200 'requires_eigen_and_numpy': skipif(not have_eigen or not np,
201 reason="eigen and/or numpy are not installed"),
202 'requires_eigen_and_scipy': skipif(not have_eigen or not scipy,
203 reason="eigen and/or scipy are not installed"),
204 }
Dean Moldovan23919172016-08-25 17:08:09 +0200205
206
207def _test_import_pybind11():
208 """Early diagnostic for test module initialization errors
209
210 When there is an error during initialization, the first import will report the
211 real error while all subsequent imports will report nonsense. This import test
212 is done early (in the pytest configuration file, before any tests) in order to
213 avoid the noise of having all tests fail with identical error messages.
214
215 Any possible exception is caught here and reported manually *without* the stack
216 trace. This further reduces noise since the trace would only show pytest internals
217 which are not useful for debugging pybind11 module issues.
218 """
219 # noinspection PyBroadException
220 try:
221 import pybind11_tests
222 except Exception as e:
223 print("Failed to import pybind11_tests from pytest:")
224 print(" {}: {}".format(type(e).__name__, e))
225 sys.exit(1)
226
227
228_test_import_pybind11()