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