| Dean Moldovan | a0c1ccf | 2016-08-12 13:50:00 +0200 | [diff] [blame] | 1 | """pytest configuration | 
 | 2 |  | 
 | 3 | Extends output capture as needed by pybind11: ignore constructors, optional unordered lines. | 
 | 4 | Adds docstring and exceptions message sanitizers: ignore Python 2 vs 3 differences. | 
 | 5 | """ | 
 | 6 |  | 
 | 7 | import pytest | 
 | 8 | import textwrap | 
 | 9 | import difflib | 
 | 10 | import re | 
 | 11 | import os | 
 | 12 | import sys | 
 | 13 | import 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 |  | 
 | 20 | def _strip_and_dedent(s): | 
 | 21 |     """For triple-quote strings""" | 
 | 22 |     return textwrap.dedent(s.lstrip('\n').rstrip()) | 
 | 23 |  | 
 | 24 |  | 
 | 25 | def _split_and_sort(s): | 
 | 26 |     """For output which does not require specific line order""" | 
 | 27 |     return sorted(_strip_and_dedent(s).splitlines()) | 
 | 28 |  | 
 | 29 |  | 
 | 30 | def _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 |  | 
 | 35 | class 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 |  | 
 | 55 | class 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 |  | 
 | 67 | class Capture(object): | 
 | 68 |     def __init__(self, capfd): | 
 | 69 |         self.capfd = capfd | 
 | 70 |         self.out = "" | 
| Dean Moldovan | 67990d9 | 2016-08-29 18:03:34 +0200 | [diff] [blame] | 71 |         self.err = "" | 
| Dean Moldovan | a0c1ccf | 2016-08-12 13:50:00 +0200 | [diff] [blame] | 72 |  | 
| Dean Moldovan | a0c1ccf | 2016-08-12 13:50:00 +0200 | [diff] [blame] | 73 |     def __enter__(self): | 
| Dean Moldovan | 81511be | 2016-09-07 00:50:10 +0200 | [diff] [blame] | 74 |         self.capfd.readouterr() | 
| Dean Moldovan | a0c1ccf | 2016-08-12 13:50:00 +0200 | [diff] [blame] | 75 |         return self | 
 | 76 |  | 
 | 77 |     def __exit__(self, *_): | 
| Dean Moldovan | 81511be | 2016-09-07 00:50:10 +0200 | [diff] [blame] | 78 |         self.out, self.err = self.capfd.readouterr() | 
| Dean Moldovan | a0c1ccf | 2016-08-12 13:50:00 +0200 | [diff] [blame] | 79 |  | 
 | 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 Moldovan | 67990d9 | 2016-08-29 18:03:34 +0200 | [diff] [blame] | 99 |     @property | 
 | 100 |     def stderr(self): | 
 | 101 |         return Output(self.err) | 
 | 102 |  | 
| Dean Moldovan | a0c1ccf | 2016-08-12 13:50:00 +0200 | [diff] [blame] | 103 |  | 
 | 104 | @pytest.fixture | 
 | 105 | def capture(capfd): | 
 | 106 |     """Extended `capfd` with context manager and custom equality operators""" | 
 | 107 |     return Capture(capfd) | 
 | 108 |  | 
 | 109 |  | 
 | 110 | class 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 |  | 
 | 130 | def _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 |  | 
 | 139 | def _sanitize_docstring(thing): | 
 | 140 |     s = thing.__doc__ | 
 | 141 |     s = _sanitize_general(s) | 
 | 142 |     return s | 
 | 143 |  | 
 | 144 |  | 
 | 145 | @pytest.fixture | 
 | 146 | def doc(): | 
 | 147 |     """Sanitize docstrings and add custom failure explanation""" | 
 | 148 |     return SanitizedString(_sanitize_docstring) | 
 | 149 |  | 
 | 150 |  | 
 | 151 | def _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 | 
 | 159 | def msg(): | 
 | 160 |     """Sanitize messages and add custom failure explanation""" | 
 | 161 |     return SanitizedString(_sanitize_message) | 
 | 162 |  | 
 | 163 |  | 
 | 164 | # noinspection PyUnusedLocal | 
 | 165 | def 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 | 
 | 172 | def suppress(exception): | 
 | 173 |     """Suppress the desired exception""" | 
 | 174 |     try: | 
 | 175 |         yield | 
 | 176 |     except exception: | 
 | 177 |         pass | 
 | 178 |  | 
 | 179 |  | 
 | 180 | def 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 Moldovan | 2391917 | 2016-08-25 17:08:09 +0200 | [diff] [blame] | 205 |  | 
 | 206 |  | 
 | 207 | def _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() |