pytest-regtest 2.3.0__py2.py3-none-any.whl → 2.3.2__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pytest_regtest-2.3.0.dist-info → pytest_regtest-2.3.2.dist-info}/METADATA +8 -28
- pytest_regtest-2.3.2.dist-info/RECORD +5 -0
- pytest_regtest/__init__.py +0 -108
- pytest_regtest/numpy_handler.py +0 -216
- pytest_regtest/pandas_handler.py +0 -143
- pytest_regtest/polars_handler.py +0 -114
- pytest_regtest/pytest_regtest.py +0 -636
- pytest_regtest/register_third_party_handlers.py +0 -43
- pytest_regtest/snapshot_handler.py +0 -188
- pytest_regtest/utils.py +0 -28
- pytest_regtest-2.3.0.dist-info/RECORD +0 -13
- {pytest_regtest-2.3.0.dist-info → pytest_regtest-2.3.2.dist-info}/WHEEL +0 -0
- {pytest_regtest-2.3.0.dist-info → pytest_regtest-2.3.2.dist-info}/entry_points.txt +0 -0
- {pytest_regtest-2.3.0.dist-info → pytest_regtest-2.3.2.dist-info}/licenses/LICENSE.txt +0 -0
pytest_regtest/pytest_regtest.py
DELETED
|
@@ -1,636 +0,0 @@
|
|
|
1
|
-
import difflib
|
|
2
|
-
import functools
|
|
3
|
-
import inspect
|
|
4
|
-
import os
|
|
5
|
-
import re
|
|
6
|
-
import shutil
|
|
7
|
-
import sys
|
|
8
|
-
import tempfile
|
|
9
|
-
from collections.abc import Callable
|
|
10
|
-
from hashlib import sha512
|
|
11
|
-
from io import StringIO
|
|
12
|
-
from typing import Optional
|
|
13
|
-
|
|
14
|
-
import _pytest
|
|
15
|
-
import pytest
|
|
16
|
-
from _pytest._code.code import TerminalRepr
|
|
17
|
-
from _pytest._io import TerminalWriter
|
|
18
|
-
|
|
19
|
-
from .snapshot_handler import PythonObjectHandler # noqa: F401
|
|
20
|
-
from .snapshot_handler import SnapshotHandlerRegistry
|
|
21
|
-
|
|
22
|
-
IS_WIN = sys.platform == "win32"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def patch_terminal_size(w, h):
|
|
26
|
-
def get_terminal_size(fallback=None):
|
|
27
|
-
return w, h
|
|
28
|
-
|
|
29
|
-
shutil.get_terminal_size = get_terminal_size
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# we determine actual terminal size before pytest changes this:
|
|
33
|
-
tw, _ = shutil.get_terminal_size()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class RegtestException(Exception):
|
|
37
|
-
pass
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class RecordedOutputException(RegtestException):
|
|
41
|
-
pass
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class SnapshotException(RegtestException):
|
|
45
|
-
pass
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class PytestRegtestCommonHooks:
|
|
49
|
-
def __init__(self):
|
|
50
|
-
self._reset_snapshots = []
|
|
51
|
-
self._reset_regtest_outputs = []
|
|
52
|
-
self._failed_snapshots = []
|
|
53
|
-
self._failed_regtests = []
|
|
54
|
-
|
|
55
|
-
@pytest.hookimpl(hookwrapper=False)
|
|
56
|
-
def pytest_terminal_summary(self, terminalreporter, exitstatus, config):
|
|
57
|
-
tr = terminalreporter
|
|
58
|
-
tr.ensure_newline()
|
|
59
|
-
tr.section("pytest-regtest report", sep="-", blue=True, bold=True)
|
|
60
|
-
tr.write("total number of failed regression tests: ", bold=True)
|
|
61
|
-
tr.line(str(len(self._failed_regtests)))
|
|
62
|
-
tr.write("total number of failed snapshot tests : ", bold=True)
|
|
63
|
-
tr.line(str(len(self._failed_snapshots)))
|
|
64
|
-
|
|
65
|
-
if config.getvalue("--regtest-reset"):
|
|
66
|
-
if config.option.verbose:
|
|
67
|
-
tr.line("the following output files have been reset:", bold=True)
|
|
68
|
-
for path in self._reset_regtest_outputs:
|
|
69
|
-
rel_path = os.path.relpath(path)
|
|
70
|
-
tr.line(" " + rel_path)
|
|
71
|
-
for path in self._reset_snapshots:
|
|
72
|
-
rel_path = os.path.relpath(path)
|
|
73
|
-
tr.line(" " + rel_path)
|
|
74
|
-
else:
|
|
75
|
-
tr.write("total number of reset output files: ", bold=True)
|
|
76
|
-
tr.line(
|
|
77
|
-
str(len(self._reset_regtest_outputs) + len(self._reset_snapshots))
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
@pytest.hookimpl(hookwrapper=True)
|
|
81
|
-
def pytest_pyfunc_call(self, pyfuncitem):
|
|
82
|
-
stdout = sys.stdout
|
|
83
|
-
if "regtest_all" in pyfuncitem.fixturenames and hasattr(
|
|
84
|
-
pyfuncitem, "regtest_stream"
|
|
85
|
-
):
|
|
86
|
-
sys.stdout = pyfuncitem.regtest_stream
|
|
87
|
-
yield
|
|
88
|
-
sys.stdout = stdout
|
|
89
|
-
|
|
90
|
-
@pytest.hookimpl(hookwrapper=True)
|
|
91
|
-
def pytest_report_teststatus(self, report, config):
|
|
92
|
-
outcome = yield
|
|
93
|
-
if report.when == "call" and "uses-regtest" in report.keywords:
|
|
94
|
-
if config.getvalue("--regtest-reset"):
|
|
95
|
-
result = outcome.get_result()
|
|
96
|
-
if result[0] != "failed":
|
|
97
|
-
outcome.force_result((result[0], "R", "RESET"))
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class PytestRegtestPlugin:
|
|
101
|
-
def __init__(self, recorder):
|
|
102
|
-
self.recorder = recorder
|
|
103
|
-
|
|
104
|
-
@pytest.hookimpl(trylast=True)
|
|
105
|
-
def pytest_runtest_call(self, item):
|
|
106
|
-
if hasattr(item, "regtest_stream"):
|
|
107
|
-
output_exception = self.check_recorded_output(item)
|
|
108
|
-
if output_exception is not None:
|
|
109
|
-
raise output_exception
|
|
110
|
-
|
|
111
|
-
if item.get_closest_marker("xfail") and item.config.getvalue("--regtest-reset"):
|
|
112
|
-
# enforce consistency with xfail:
|
|
113
|
-
assert False
|
|
114
|
-
|
|
115
|
-
def check_recorded_output(self, item):
|
|
116
|
-
test_folder = item.fspath.dirname
|
|
117
|
-
regtest_stream = item.regtest_stream
|
|
118
|
-
version = regtest_stream.version or regtest_stream.identifier
|
|
119
|
-
if not isinstance(regtest_stream, RegtestStream):
|
|
120
|
-
return
|
|
121
|
-
|
|
122
|
-
orig_identifer, recorded_output_path = result_file_paths(
|
|
123
|
-
test_folder, item.nodeid, version
|
|
124
|
-
)
|
|
125
|
-
config = item.config
|
|
126
|
-
|
|
127
|
-
consider_line_endings = config.getvalue("--regtest-consider-line-endings")
|
|
128
|
-
reset = config.getvalue("--regtest-reset")
|
|
129
|
-
|
|
130
|
-
if reset:
|
|
131
|
-
os.makedirs(os.path.dirname(recorded_output_path), exist_ok=True)
|
|
132
|
-
with open(recorded_output_path + ".out", "w", encoding="utf-8") as fh:
|
|
133
|
-
fh.write("".join(regtest_stream.get_lines()))
|
|
134
|
-
if orig_identifer is not None:
|
|
135
|
-
self.recorder._reset_regtest_outputs.append(
|
|
136
|
-
recorded_output_path + ".item"
|
|
137
|
-
)
|
|
138
|
-
with open(recorded_output_path + ".item", "w") as fh:
|
|
139
|
-
print(orig_identifer, file=fh)
|
|
140
|
-
self.recorder._reset_regtest_outputs.append(recorded_output_path + ".out")
|
|
141
|
-
return
|
|
142
|
-
|
|
143
|
-
if os.path.exists(recorded_output_path + ".out"):
|
|
144
|
-
with open(recorded_output_path + ".out", "r", encoding="utf-8") as fh:
|
|
145
|
-
tobe = fh.readlines()
|
|
146
|
-
recorded_output_file_exists = True
|
|
147
|
-
else:
|
|
148
|
-
tobe = []
|
|
149
|
-
recorded_output_file_exists = False
|
|
150
|
-
|
|
151
|
-
current = regtest_stream.get_lines()
|
|
152
|
-
if consider_line_endings:
|
|
153
|
-
current = [repr(line.rstrip("\n")) for line in current]
|
|
154
|
-
tobe = [repr(line.rstrip("\n")) for line in tobe]
|
|
155
|
-
else:
|
|
156
|
-
current = [line.rstrip() for line in current]
|
|
157
|
-
tobe = [line.rstrip() for line in tobe]
|
|
158
|
-
|
|
159
|
-
if current != tobe:
|
|
160
|
-
self.recorder._failed_regtests.append(item)
|
|
161
|
-
return RecordedOutputException(
|
|
162
|
-
current,
|
|
163
|
-
tobe,
|
|
164
|
-
recorded_output_path,
|
|
165
|
-
regtest_stream,
|
|
166
|
-
recorded_output_file_exists,
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
@pytest.hookimpl(hookwrapper=True)
|
|
170
|
-
def pytest_runtest_makereport(self, item, call):
|
|
171
|
-
outcome = yield
|
|
172
|
-
result = outcome.get_result()
|
|
173
|
-
if call.when == "teardown" and hasattr(item, "regtest_stream"):
|
|
174
|
-
if item.config.getvalue("--regtest-tee"):
|
|
175
|
-
tw = TerminalWriter()
|
|
176
|
-
output = item.regtest_stream.get_output()
|
|
177
|
-
|
|
178
|
-
if output:
|
|
179
|
-
tw.line()
|
|
180
|
-
line = "recorded raw output to regtest fixture: "
|
|
181
|
-
line = line.ljust(tw.fullwidth, "-")
|
|
182
|
-
tw.line(line, green=True)
|
|
183
|
-
tw.write(item.regtest_stream.get_output() + "\n", cyan=True)
|
|
184
|
-
line = "-" * tw.fullwidth
|
|
185
|
-
tw.line(line, green=True)
|
|
186
|
-
|
|
187
|
-
if call.when != "call" or not getattr(item, "regtest", False):
|
|
188
|
-
return
|
|
189
|
-
|
|
190
|
-
result.keywords["uses-regtest"] = True
|
|
191
|
-
|
|
192
|
-
if call.excinfo is not None:
|
|
193
|
-
all_lines, all_colors = [], []
|
|
194
|
-
if call.excinfo.type is RecordedOutputException:
|
|
195
|
-
output_exception = call.excinfo
|
|
196
|
-
if output_exception is not None:
|
|
197
|
-
lines, colors = self._handle_regtest_exception(
|
|
198
|
-
item, output_exception.value.args, result
|
|
199
|
-
)
|
|
200
|
-
all_lines.extend(lines)
|
|
201
|
-
all_colors.extend(colors)
|
|
202
|
-
|
|
203
|
-
else:
|
|
204
|
-
return
|
|
205
|
-
|
|
206
|
-
result.longrepr = CollectErrorRepr(all_lines, all_colors)
|
|
207
|
-
|
|
208
|
-
def _handle_regtest_exception(self, item, exc_args, result):
|
|
209
|
-
(
|
|
210
|
-
current,
|
|
211
|
-
recorded,
|
|
212
|
-
recorded_output_path,
|
|
213
|
-
regtest_stream,
|
|
214
|
-
recorded_output_file_exists,
|
|
215
|
-
) = exc_args
|
|
216
|
-
|
|
217
|
-
nodeid = item.nodeid + (
|
|
218
|
-
"" if regtest_stream.version is None else "__" + regtest_stream.version
|
|
219
|
-
)
|
|
220
|
-
if not recorded_output_file_exists:
|
|
221
|
-
msg = "\nregression test output not recorded yet for {}:\n".format(nodeid)
|
|
222
|
-
return (
|
|
223
|
-
[msg] + current,
|
|
224
|
-
[dict()] + len(current) * [dict(red=True, bold=True)],
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
nodiff = item.config.getvalue("--regtest-nodiff")
|
|
228
|
-
diffs = list(
|
|
229
|
-
difflib.unified_diff(current, recorded, "current", "expected", lineterm="")
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
msg = "\nregression test output differences for {}:\n".format(nodeid)
|
|
233
|
-
|
|
234
|
-
if nodiff:
|
|
235
|
-
msg_diff = f" {len(diffs)} lines in diff"
|
|
236
|
-
else:
|
|
237
|
-
recorded_output_path = os.path.relpath(recorded_output_path)
|
|
238
|
-
msg += f" (recorded output from {recorded_output_path})\n"
|
|
239
|
-
msg_diff = " > " + "\n > ".join(diffs)
|
|
240
|
-
|
|
241
|
-
return [msg, msg_diff + "\n"], [dict(), dict(red=True, bold=True)]
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
class SnapshotPlugin:
|
|
245
|
-
def __init__(self, recorder):
|
|
246
|
-
self.recorder = recorder
|
|
247
|
-
|
|
248
|
-
@pytest.hookimpl(trylast=True)
|
|
249
|
-
def pytest_runtest_call(self, item):
|
|
250
|
-
if hasattr(item, "snapshot"):
|
|
251
|
-
snapshot_exception = self.check_snapshots(item)
|
|
252
|
-
if snapshot_exception is not None:
|
|
253
|
-
raise snapshot_exception
|
|
254
|
-
|
|
255
|
-
if item.get_closest_marker("xfail") and item.config.getvalue("--regtest-reset"):
|
|
256
|
-
# enforce fail
|
|
257
|
-
assert False
|
|
258
|
-
|
|
259
|
-
def check_snapshots(self, item):
|
|
260
|
-
results = []
|
|
261
|
-
|
|
262
|
-
any_failed = False
|
|
263
|
-
for idx, snapshot in enumerate(item.snapshot.snapshots):
|
|
264
|
-
is_recorded, ok, msg = self.check_snapshot(idx, item, snapshot)
|
|
265
|
-
if not ok:
|
|
266
|
-
any_failed = True
|
|
267
|
-
results.append((ok, snapshot, is_recorded, msg))
|
|
268
|
-
|
|
269
|
-
if any_failed:
|
|
270
|
-
self.recorder._failed_snapshots.append(item)
|
|
271
|
-
return SnapshotException(results)
|
|
272
|
-
|
|
273
|
-
def check_snapshot(self, idx, item, snapshot):
|
|
274
|
-
handler, obj, version, _ = snapshot
|
|
275
|
-
|
|
276
|
-
test_folder = item.fspath.dirname
|
|
277
|
-
if version is not None:
|
|
278
|
-
identifier = str(version) + "__" + str(idx)
|
|
279
|
-
else:
|
|
280
|
-
identifier = str(idx)
|
|
281
|
-
|
|
282
|
-
config = item.config
|
|
283
|
-
|
|
284
|
-
orig_identifer, recorded_output_path = result_file_paths(
|
|
285
|
-
test_folder, item.nodeid, identifier
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
reset = config.getvalue("--regtest-reset")
|
|
289
|
-
|
|
290
|
-
if reset:
|
|
291
|
-
os.makedirs(recorded_output_path, exist_ok=True)
|
|
292
|
-
handler.save(recorded_output_path, obj)
|
|
293
|
-
if orig_identifer is not None:
|
|
294
|
-
self.recorder._reset_snapshots.append(recorded_output_path + ".item")
|
|
295
|
-
with open(recorded_output_path + ".item", "w") as fh:
|
|
296
|
-
print(orig_identifer, file=fh)
|
|
297
|
-
self.recorder._reset_snapshots.append(recorded_output_path)
|
|
298
|
-
return True, True, None
|
|
299
|
-
|
|
300
|
-
has_markup = item.config.get_terminal_writer().hasmarkup
|
|
301
|
-
if os.path.exists(recorded_output_path):
|
|
302
|
-
recorded_obj = handler.load(recorded_output_path)
|
|
303
|
-
ok = handler.compare(obj, recorded_obj)
|
|
304
|
-
if ok:
|
|
305
|
-
return True, True, None
|
|
306
|
-
msg = handler.show_differences(obj, recorded_obj, has_markup)
|
|
307
|
-
return True, False, msg
|
|
308
|
-
|
|
309
|
-
msg = handler.show(obj)
|
|
310
|
-
return False, False, msg
|
|
311
|
-
|
|
312
|
-
@pytest.hookimpl(hookwrapper=True)
|
|
313
|
-
def pytest_runtest_makereport(self, item, call):
|
|
314
|
-
outcome = yield
|
|
315
|
-
result = outcome.get_result()
|
|
316
|
-
if call.when == "teardown" and hasattr(item, "snapshot"):
|
|
317
|
-
if item.config.getvalue("--regtest-tee"):
|
|
318
|
-
tw = TerminalWriter()
|
|
319
|
-
snapshots = item.snapshot.snapshots
|
|
320
|
-
if not snapshots:
|
|
321
|
-
return
|
|
322
|
-
|
|
323
|
-
tw.line()
|
|
324
|
-
line = "recorded snapshots: "
|
|
325
|
-
line = line.ljust(tw.fullwidth, "-")
|
|
326
|
-
tw.line(line, green=True)
|
|
327
|
-
path = item.fspath.relto(item.session.fspath)
|
|
328
|
-
code_lines = item.fspath.readlines()
|
|
329
|
-
|
|
330
|
-
for handler, obj, version, line_no in snapshots:
|
|
331
|
-
info = code_lines[line_no - 1].strip()
|
|
332
|
-
tw.line(f"> {path} +{line_no}")
|
|
333
|
-
tw.line(f"> {info}")
|
|
334
|
-
lines = handler.show(obj)
|
|
335
|
-
for line in lines:
|
|
336
|
-
tw.line(line, cyan=True)
|
|
337
|
-
tw.line("-" * tw.fullwidth, green=True)
|
|
338
|
-
tw.line()
|
|
339
|
-
tw.flush()
|
|
340
|
-
|
|
341
|
-
if call.when != "call" or not hasattr(item, "snapshot"):
|
|
342
|
-
return
|
|
343
|
-
|
|
344
|
-
result.keywords["uses-regtest"] = True
|
|
345
|
-
|
|
346
|
-
if call.excinfo is not None:
|
|
347
|
-
all_lines, all_colors = [], []
|
|
348
|
-
if call.excinfo.type is SnapshotException:
|
|
349
|
-
snapshot_exception = call.excinfo
|
|
350
|
-
if snapshot_exception is not None:
|
|
351
|
-
lines, colors = self._handle_snapshot_exception(
|
|
352
|
-
item, snapshot_exception.value.args, result
|
|
353
|
-
)
|
|
354
|
-
all_lines.extend(lines)
|
|
355
|
-
all_colors.extend(colors)
|
|
356
|
-
else:
|
|
357
|
-
return
|
|
358
|
-
|
|
359
|
-
result.longrepr = CollectErrorRepr(all_lines, all_colors)
|
|
360
|
-
|
|
361
|
-
def _handle_snapshot_exception(self, item, exc_args, result):
|
|
362
|
-
snapshot = item.snapshot
|
|
363
|
-
lines = []
|
|
364
|
-
colors = []
|
|
365
|
-
|
|
366
|
-
code_lines = item.fspath.readlines()
|
|
367
|
-
|
|
368
|
-
NO_COLOR = dict()
|
|
369
|
-
RED = dict(red=True, bold=True)
|
|
370
|
-
GREEN = dict(green=True, bold=False)
|
|
371
|
-
|
|
372
|
-
headline = "\nsnapshot error(s) for {}:".format(item.nodeid)
|
|
373
|
-
lines.append(headline)
|
|
374
|
-
colors.append(NO_COLOR)
|
|
375
|
-
|
|
376
|
-
for ok, snapshot, is_recorded, msg in exc_args[0]:
|
|
377
|
-
obj, version, kw, line_no = snapshot
|
|
378
|
-
info = code_lines[line_no - 1].strip()
|
|
379
|
-
|
|
380
|
-
path = item.fspath.relto(item.session.fspath)
|
|
381
|
-
if ok:
|
|
382
|
-
lines.append("\nsnapshot ok:")
|
|
383
|
-
lines.append(f" > {path} +{line_no}")
|
|
384
|
-
lines.append(f" > {info}")
|
|
385
|
-
colors.append(GREEN)
|
|
386
|
-
colors.append(NO_COLOR)
|
|
387
|
-
colors.append(NO_COLOR)
|
|
388
|
-
elif is_recorded:
|
|
389
|
-
lines.append("\nsnapshot mismatch:")
|
|
390
|
-
lines.append(f" > {path} +{line_no}:")
|
|
391
|
-
lines.append(f" > {info}")
|
|
392
|
-
colors.append(RED)
|
|
393
|
-
colors.append(NO_COLOR)
|
|
394
|
-
colors.append(NO_COLOR)
|
|
395
|
-
nodiff = item.config.getvalue("--regtest-nodiff")
|
|
396
|
-
if nodiff:
|
|
397
|
-
lines.append(f" {len(msg)} lines in report")
|
|
398
|
-
colors.append(RED)
|
|
399
|
-
else:
|
|
400
|
-
lines.extend(" " + ll for ll in msg)
|
|
401
|
-
colors.extend(len(msg) * [RED])
|
|
402
|
-
else:
|
|
403
|
-
headline = "\nsnapshot not recorded yet:"
|
|
404
|
-
lines.append(headline)
|
|
405
|
-
colors.append(NO_COLOR)
|
|
406
|
-
lines.append(" > " + info.strip())
|
|
407
|
-
colors.append(RED)
|
|
408
|
-
lines.extend(" " + ll for ll in msg)
|
|
409
|
-
colors.extend(len(msg) * [RED])
|
|
410
|
-
|
|
411
|
-
return lines, colors
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
def result_file_paths(test_folder, nodeid, version):
|
|
415
|
-
file_path, __, test_function_name = nodeid.partition("::")
|
|
416
|
-
file_name = os.path.basename(file_path)
|
|
417
|
-
|
|
418
|
-
orig_test_function_identifier = f"{file_name}::{test_function_name}"
|
|
419
|
-
|
|
420
|
-
for c in "/\\:*\"'?<>|":
|
|
421
|
-
test_function_name = test_function_name.replace(c, "-")
|
|
422
|
-
|
|
423
|
-
# If file name is too long, hash parameters.
|
|
424
|
-
if len(test_function_name) > 100:
|
|
425
|
-
test_function_name = (
|
|
426
|
-
test_function_name[:88]
|
|
427
|
-
+ "__"
|
|
428
|
-
+ sha512(test_function_name.encode("utf-8")).hexdigest()[:10]
|
|
429
|
-
)
|
|
430
|
-
else:
|
|
431
|
-
orig_test_function_identifier = None
|
|
432
|
-
|
|
433
|
-
test_function_name = test_function_name.replace(" ", "_")
|
|
434
|
-
stem, __ = os.path.splitext(file_name)
|
|
435
|
-
if version is not None:
|
|
436
|
-
output_file_name = stem + "." + test_function_name + "__" + str(version)
|
|
437
|
-
else:
|
|
438
|
-
output_file_name = stem + "." + test_function_name
|
|
439
|
-
|
|
440
|
-
return orig_test_function_identifier, os.path.join(
|
|
441
|
-
test_folder, "_regtest_outputs", output_file_name
|
|
442
|
-
)
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
class RegtestStream:
|
|
446
|
-
def __init__(self, request):
|
|
447
|
-
request.node.regtest_stream = self
|
|
448
|
-
request.node.regtest = True
|
|
449
|
-
self.request = request
|
|
450
|
-
self.buffer = StringIO()
|
|
451
|
-
self.version = None
|
|
452
|
-
self.identifier = None
|
|
453
|
-
|
|
454
|
-
self.snapshots = []
|
|
455
|
-
|
|
456
|
-
def write(self, what):
|
|
457
|
-
self.buffer.write(what)
|
|
458
|
-
|
|
459
|
-
def flush(self):
|
|
460
|
-
pass
|
|
461
|
-
|
|
462
|
-
def get_lines(self):
|
|
463
|
-
output = self.buffer.getvalue()
|
|
464
|
-
if not output:
|
|
465
|
-
return []
|
|
466
|
-
output = cleanup(output, self.request)
|
|
467
|
-
lines = output.splitlines(keepends=True)
|
|
468
|
-
return lines
|
|
469
|
-
|
|
470
|
-
def get_output(self):
|
|
471
|
-
return self.buffer.getvalue()
|
|
472
|
-
|
|
473
|
-
def __enter__(self):
|
|
474
|
-
sys.stdout = self
|
|
475
|
-
return self
|
|
476
|
-
|
|
477
|
-
def __exit__(self, *a):
|
|
478
|
-
sys.stdout = sys.__stdout__
|
|
479
|
-
return False # dont suppress exception
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
class Snapshot:
|
|
483
|
-
def __init__(self, request):
|
|
484
|
-
request.node.snapshot = self
|
|
485
|
-
request.node.regtest = True
|
|
486
|
-
self.request = request
|
|
487
|
-
self.buffer = StringIO()
|
|
488
|
-
|
|
489
|
-
self.snapshots = []
|
|
490
|
-
|
|
491
|
-
def check(self, obj, *, version=None, **options):
|
|
492
|
-
handler_class = SnapshotHandlerRegistry.get_handler(obj)
|
|
493
|
-
if handler_class is None:
|
|
494
|
-
raise ValueError(f"no handler registered for {obj}")
|
|
495
|
-
|
|
496
|
-
handler = handler_class(options, self.request.config, tw)
|
|
497
|
-
line_no = inspect.currentframe().f_back.f_lineno
|
|
498
|
-
self.snapshots.append((handler, obj, version, line_no))
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
def cleanup(output, request):
|
|
502
|
-
for converter in _converters_pre:
|
|
503
|
-
output = converter(output, request)
|
|
504
|
-
|
|
505
|
-
if not request.config.getvalue("--regtest-disable-stdconv"):
|
|
506
|
-
output = _std_conversion(output, request)
|
|
507
|
-
|
|
508
|
-
for converter in _converters_post:
|
|
509
|
-
output = converter(output, request)
|
|
510
|
-
|
|
511
|
-
# in python 3 a string should not contain binary symbols...:
|
|
512
|
-
if contains_binary(output):
|
|
513
|
-
request.raiseerror(
|
|
514
|
-
"recorded output for regression test contains unprintable characters."
|
|
515
|
-
)
|
|
516
|
-
|
|
517
|
-
return output
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
# the function below is modified version of http://stackoverflow.com/questions/898669/
|
|
521
|
-
textchars = bytearray({7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100)) - {0x7F})
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
def contains_binary(txt):
|
|
525
|
-
return bool(txt.translate(dict(zip(textchars, " " * 9999))).replace(" ", ""))
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
_converters_pre = []
|
|
529
|
-
_converters_post = []
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
def clear_converters() -> None:
|
|
533
|
-
"""Unregisters all converters, including the builtin converters."""
|
|
534
|
-
_converters_pre.clear()
|
|
535
|
-
_converters_post.clear()
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
def _fix_pre_v2_converter_function(function):
|
|
539
|
-
@functools.wraps(function)
|
|
540
|
-
def fixed_converter_function(output, request):
|
|
541
|
-
return function(output)
|
|
542
|
-
|
|
543
|
-
return fixed_converter_function
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
def register_converter_pre(
|
|
547
|
-
function: Callable[[str, Optional[_pytest.fixtures.FixtureRequest]], None],
|
|
548
|
-
) -> None:
|
|
549
|
-
"""Registers a new conversion function at the head of the list
|
|
550
|
-
of existing converters.
|
|
551
|
-
|
|
552
|
-
Parameters:
|
|
553
|
-
function: Function to cleanup given string and remove data which can change
|
|
554
|
-
between test runs without affecting the correctness of the test.
|
|
555
|
-
The second argument is optional and is a `pytest` object which holds
|
|
556
|
-
information about the current `config` or the current test function.
|
|
557
|
-
This argument can be ignored in many situations.
|
|
558
|
-
|
|
559
|
-
"""
|
|
560
|
-
if function not in _converters_pre:
|
|
561
|
-
signature = inspect.signature(function)
|
|
562
|
-
# keep downward compatibility:
|
|
563
|
-
if len(signature.parameters) == 1:
|
|
564
|
-
function = _fix_pre_v2_converter_function(function)
|
|
565
|
-
_converters_pre.append(function)
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
def register_converter_post(
|
|
569
|
-
function: Callable[[str, Optional[_pytest.fixtures.FixtureRequest]], None],
|
|
570
|
-
) -> None:
|
|
571
|
-
"""Registers a new conversion function at the head of the list
|
|
572
|
-
of existing converters
|
|
573
|
-
|
|
574
|
-
Parameters:
|
|
575
|
-
function: Function to cleanup given string and remove data which can change
|
|
576
|
-
between test runs without affecting the correctness of the test.
|
|
577
|
-
The second argument is optional and is a `pytest` object which holds
|
|
578
|
-
information about the current `config` or the current test function.
|
|
579
|
-
This argument can be ignored in many situations.
|
|
580
|
-
"""
|
|
581
|
-
if function not in _converters_post:
|
|
582
|
-
signature = inspect.signature(function)
|
|
583
|
-
# keep downward compatibility:
|
|
584
|
-
if len(signature.parameters) == 1:
|
|
585
|
-
function = _fix_pre_v2_converter_function(function)
|
|
586
|
-
_converters_post.append(function)
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
def _std_replacements(request):
|
|
590
|
-
if "tmpdir" in request.fixturenames:
|
|
591
|
-
tmpdir = request.getfixturevalue("tmpdir").strpath + os.path.sep
|
|
592
|
-
yield tmpdir, "<tmpdir_from_fixture>/"
|
|
593
|
-
tmpdir = request.getfixturevalue("tmpdir").strpath
|
|
594
|
-
yield tmpdir, "<tmpdir_from_fixture>"
|
|
595
|
-
|
|
596
|
-
regexp = os.path.join(
|
|
597
|
-
os.path.realpath(tempfile.gettempdir()), "pytest-of-.*", r"pytest-\d+/"
|
|
598
|
-
)
|
|
599
|
-
yield regexp, "<pytest_tempdir>/"
|
|
600
|
-
|
|
601
|
-
regexp = os.path.join(tempfile.gettempdir(), "tmp[_a-zA-Z0-9]+")
|
|
602
|
-
|
|
603
|
-
yield regexp, "<tmpdir_from_tempfile_module>"
|
|
604
|
-
yield (
|
|
605
|
-
os.path.realpath(tempfile.gettempdir()) + os.path.sep,
|
|
606
|
-
"<tmpdir_from_tempfile_module>/",
|
|
607
|
-
)
|
|
608
|
-
yield os.path.realpath(tempfile.gettempdir()), "<tmpdir_from_tempfile_module>"
|
|
609
|
-
yield tempfile.tempdir + os.path.sep, "<tmpdir_from_tempfile_module>/"
|
|
610
|
-
yield tempfile.tempdir, "<tmpdir_from_tempfile_module>"
|
|
611
|
-
yield r"var/folders/.*/pytest-of.*/", "<pytest_tempdir>/"
|
|
612
|
-
|
|
613
|
-
# replace hex object ids in output by 0x?????????
|
|
614
|
-
yield r" 0x[0-9a-fA-F]+", " 0x?????????"
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
def _std_conversion(output, request):
|
|
618
|
-
fixed = []
|
|
619
|
-
for line in output.splitlines(keepends=True):
|
|
620
|
-
for regex, replacement in _std_replacements(request):
|
|
621
|
-
if IS_WIN:
|
|
622
|
-
# fix windows backwards slashes in regex
|
|
623
|
-
regex = regex.replace("\\", "\\\\")
|
|
624
|
-
line, __ = re.subn(regex, replacement, line)
|
|
625
|
-
fixed.append(line)
|
|
626
|
-
return "".join(fixed)
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
class CollectErrorRepr(TerminalRepr):
|
|
630
|
-
def __init__(self, messages, colors):
|
|
631
|
-
self.messages = messages
|
|
632
|
-
self.colors = colors
|
|
633
|
-
|
|
634
|
-
def toterminal(self, out):
|
|
635
|
-
for message, color in zip(self.messages, self.colors):
|
|
636
|
-
out.line(message, **color)
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
def register_pandas_handler():
|
|
2
|
-
def is_dataframe(obj):
|
|
3
|
-
try:
|
|
4
|
-
import pandas as pd
|
|
5
|
-
|
|
6
|
-
return isinstance(obj, pd.DataFrame)
|
|
7
|
-
except ImportError:
|
|
8
|
-
return False
|
|
9
|
-
|
|
10
|
-
from .pandas_handler import DataFrameHandler
|
|
11
|
-
from .snapshot_handler import SnapshotHandlerRegistry
|
|
12
|
-
|
|
13
|
-
SnapshotHandlerRegistry.add_handler(is_dataframe, DataFrameHandler)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def register_numpy_handler():
|
|
17
|
-
def is_numpy(obj):
|
|
18
|
-
try:
|
|
19
|
-
import numpy as np
|
|
20
|
-
|
|
21
|
-
return isinstance(obj, np.ndarray)
|
|
22
|
-
except ImportError:
|
|
23
|
-
return False
|
|
24
|
-
|
|
25
|
-
from .numpy_handler import NumpyHandler
|
|
26
|
-
from .snapshot_handler import SnapshotHandlerRegistry
|
|
27
|
-
|
|
28
|
-
SnapshotHandlerRegistry.add_handler(is_numpy, NumpyHandler)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def register_polars_handler():
|
|
32
|
-
def is_polars(obj):
|
|
33
|
-
try:
|
|
34
|
-
import polars as pl
|
|
35
|
-
|
|
36
|
-
return isinstance(obj, pl.DataFrame)
|
|
37
|
-
except ImportError:
|
|
38
|
-
return False
|
|
39
|
-
|
|
40
|
-
from .polars_handler import PolarsHandler
|
|
41
|
-
from .snapshot_handler import SnapshotHandlerRegistry
|
|
42
|
-
|
|
43
|
-
SnapshotHandlerRegistry.add_handler(is_polars, PolarsHandler)
|