blob: 8b6e47dc2ebd700e11da9966cb9c8b42a44127a1 [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
Dean Moldovana0c1ccf2016-08-12 13:50:00 +02008import contextlib
Henry Schreiner4d9024e2020-08-16 16:02:12 -04009import difflib
Wenzel Jakob1d1f81b2016-12-16 15:00:46 +010010import gc
Henry Schreiner4d9024e2020-08-16 16:02:12 -040011import re
12import textwrap
13
14import pytest
15
16# Early diagnostic for failed imports
17import pybind11_tests # noqa: F401
Dean Moldovana0c1ccf2016-08-12 13:50:00 +020018
19_unicode_marker = re.compile(r'u(\'[^\']*\')')
Dean Moldovanbad17402016-11-20 21:21:54 +010020_long_marker = re.compile(r'([0-9])L')
21_hexadecimal = re.compile(r'0x[0-9a-fA-F]+')
Dean Moldovana0c1ccf2016-08-12 13:50:00 +020022
23
24def _strip_and_dedent(s):
25 """For triple-quote strings"""
26 return textwrap.dedent(s.lstrip('\n').rstrip())
27
28
29def _split_and_sort(s):
30 """For output which does not require specific line order"""
31 return sorted(_strip_and_dedent(s).splitlines())
32
33
34def _make_explanation(a, b):
35 """Explanation for a failed assert -- the a and b arguments are List[str]"""
36 return ["--- actual / +++ expected"] + [line.strip('\n') for line in difflib.ndiff(a, b)]
37
38
39class Output(object):
40 """Basic output post-processing and comparison"""
41 def __init__(self, string):
42 self.string = string
43 self.explanation = []
44
45 def __str__(self):
46 return self.string
47
48 def __eq__(self, other):
49 # Ignore constructor/destructor output which is prefixed with "###"
50 a = [line for line in self.string.strip().splitlines() if not line.startswith("###")]
51 b = _strip_and_dedent(other).splitlines()
52 if a == b:
53 return True
54 else:
55 self.explanation = _make_explanation(a, b)
56 return False
57
58
59class Unordered(Output):
60 """Custom comparison for output without strict line ordering"""
61 def __eq__(self, other):
62 a = _split_and_sort(self.string)
63 b = _split_and_sort(other)
64 if a == b:
65 return True
66 else:
67 self.explanation = _make_explanation(a, b)
68 return False
69
70
71class Capture(object):
72 def __init__(self, capfd):
73 self.capfd = capfd
74 self.out = ""
Dean Moldovan67990d92016-08-29 18:03:34 +020075 self.err = ""
Dean Moldovana0c1ccf2016-08-12 13:50:00 +020076
Dean Moldovana0c1ccf2016-08-12 13:50:00 +020077 def __enter__(self):
Dean Moldovan81511be2016-09-07 00:50:10 +020078 self.capfd.readouterr()
Dean Moldovana0c1ccf2016-08-12 13:50:00 +020079 return self
80
Wenzel Jakob25abf7e2019-02-04 17:09:47 +010081 def __exit__(self, *args):
Dean Moldovan81511be2016-09-07 00:50:10 +020082 self.out, self.err = self.capfd.readouterr()
Dean Moldovana0c1ccf2016-08-12 13:50:00 +020083
84 def __eq__(self, other):
85 a = Output(self.out)
86 b = other
87 if a == b:
88 return True
89 else:
90 self.explanation = a.explanation
91 return False
92
93 def __str__(self):
94 return self.out
95
96 def __contains__(self, item):
97 return item in self.out
98
99 @property
100 def unordered(self):
101 return Unordered(self.out)
102
Dean Moldovan67990d92016-08-29 18:03:34 +0200103 @property
104 def stderr(self):
105 return Output(self.err)
106
Dean Moldovana0c1ccf2016-08-12 13:50:00 +0200107
108@pytest.fixture
Dean Moldovand47febc2017-03-10 15:42:42 +0100109def capture(capsys):
110 """Extended `capsys` with context manager and custom equality operators"""
111 return Capture(capsys)
Dean Moldovana0c1ccf2016-08-12 13:50:00 +0200112
113
114class SanitizedString(object):
115 def __init__(self, sanitizer):
116 self.sanitizer = sanitizer
117 self.string = ""
118 self.explanation = []
119
120 def __call__(self, thing):
121 self.string = self.sanitizer(thing)
122 return self
123
124 def __eq__(self, other):
125 a = self.string
126 b = _strip_and_dedent(other)
127 if a == b:
128 return True
129 else:
130 self.explanation = _make_explanation(a.splitlines(), b.splitlines())
131 return False
132
133
134def _sanitize_general(s):
135 s = s.strip()
136 s = s.replace("pybind11_tests.", "m.")
137 s = s.replace("unicode", "str")
138 s = _long_marker.sub(r"\1", s)
139 s = _unicode_marker.sub(r"\1", s)
140 return s
141
142
143def _sanitize_docstring(thing):
144 s = thing.__doc__
145 s = _sanitize_general(s)
146 return s
147
148
149@pytest.fixture
150def doc():
151 """Sanitize docstrings and add custom failure explanation"""
152 return SanitizedString(_sanitize_docstring)
153
154
155def _sanitize_message(thing):
156 s = str(thing)
157 s = _sanitize_general(s)
158 s = _hexadecimal.sub("0", s)
159 return s
160
161
162@pytest.fixture
163def msg():
164 """Sanitize messages and add custom failure explanation"""
165 return SanitizedString(_sanitize_message)
166
167
168# noinspection PyUnusedLocal
169def pytest_assertrepr_compare(op, left, right):
170 """Hook to insert custom failure explanation"""
171 if hasattr(left, 'explanation'):
172 return left.explanation
173
174
175@contextlib.contextmanager
176def suppress(exception):
177 """Suppress the desired exception"""
178 try:
179 yield
180 except exception:
181 pass
182
183
Wenzel Jakob1d1f81b2016-12-16 15:00:46 +0100184def gc_collect():
185 ''' Run the garbage collector twice (needed when running
186 reference counting tests with PyPy) '''
187 gc.collect()
188 gc.collect()
189
190
Guilhem Saurele7ef34f2019-01-23 14:22:39 +0100191def pytest_configure():
Guilhem Saurele7ef34f2019-01-23 14:22:39 +0100192 pytest.suppress = suppress
Guilhem Saurele7ef34f2019-01-23 14:22:39 +0100193 pytest.gc_collect = gc_collect