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