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