pytest-regtest 2.2.0a2__py2.py3-none-any.whl → 2.3.0__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/__init__.py +47 -6
- pytest_regtest/numpy_handler.py +23 -3
- pytest_regtest/pandas_handler.py +8 -0
- pytest_regtest/polars_handler.py +114 -0
- pytest_regtest/pytest_regtest.py +284 -197
- pytest_regtest/register_third_party_handlers.py +21 -22
- pytest_regtest/snapshot_handler.py +130 -21
- pytest_regtest-2.3.0.dist-info/METADATA +111 -0
- pytest_regtest-2.3.0.dist-info/RECORD +13 -0
- pytest_regtest-2.2.0a2.dist-info/METADATA +0 -356
- pytest_regtest-2.2.0a2.dist-info/RECORD +0 -12
- {pytest_regtest-2.2.0a2.dist-info → pytest_regtest-2.3.0.dist-info}/WHEEL +0 -0
- {pytest_regtest-2.2.0a2.dist-info → pytest_regtest-2.3.0.dist-info}/entry_points.txt +0 -0
- {pytest_regtest-2.2.0a2.dist-info → pytest_regtest-2.3.0.dist-info}/licenses/LICENSE.txt +0 -0
pytest_regtest/pytest_regtest.py
CHANGED
|
@@ -6,17 +6,18 @@ import re
|
|
|
6
6
|
import shutil
|
|
7
7
|
import sys
|
|
8
8
|
import tempfile
|
|
9
|
+
from collections.abc import Callable
|
|
9
10
|
from hashlib import sha512
|
|
10
11
|
from io import StringIO
|
|
12
|
+
from typing import Optional
|
|
11
13
|
|
|
14
|
+
import _pytest
|
|
12
15
|
import pytest
|
|
13
16
|
from _pytest._code.code import TerminalRepr
|
|
14
17
|
from _pytest._io import TerminalWriter
|
|
15
18
|
|
|
16
|
-
# from .numpy_handler import NumpyHandler # noqa: F401
|
|
17
|
-
# from .pandas_handler import DataFrameHandler # noqa: F401
|
|
18
19
|
from .snapshot_handler import PythonObjectHandler # noqa: F401
|
|
19
|
-
from .snapshot_handler import
|
|
20
|
+
from .snapshot_handler import SnapshotHandlerRegistry
|
|
20
21
|
|
|
21
22
|
IS_WIN = sys.platform == "win32"
|
|
22
23
|
|
|
@@ -36,90 +37,90 @@ class RegtestException(Exception):
|
|
|
36
37
|
pass
|
|
37
38
|
|
|
38
39
|
|
|
39
|
-
class RecordedOutputException(
|
|
40
|
+
class RecordedOutputException(RegtestException):
|
|
40
41
|
pass
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
class SnapshotException(
|
|
44
|
+
class SnapshotException(RegtestException):
|
|
44
45
|
pass
|
|
45
46
|
|
|
46
47
|
|
|
47
|
-
class
|
|
48
|
+
class PytestRegtestCommonHooks:
|
|
48
49
|
def __init__(self):
|
|
50
|
+
self._reset_snapshots = []
|
|
49
51
|
self._reset_regtest_outputs = []
|
|
52
|
+
self._failed_snapshots = []
|
|
50
53
|
self._failed_regtests = []
|
|
51
54
|
|
|
52
|
-
@pytest.hookimpl(
|
|
53
|
-
def
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
raise RegtestException(output_exception, snapshot_exception)
|
|
62
|
-
|
|
63
|
-
if item.get_closest_marker("xfail") and item.config.getvalue("--regtest-reset"):
|
|
64
|
-
# enforce fail
|
|
65
|
-
assert False
|
|
66
|
-
|
|
67
|
-
def check_snapshots(self, item):
|
|
68
|
-
results = []
|
|
69
|
-
|
|
70
|
-
any_failed = False
|
|
71
|
-
for idx, snapshot in enumerate(item.regtest_stream.snapshots):
|
|
72
|
-
is_recorded, ok, msg = self.check_snapshot(idx, item, snapshot)
|
|
73
|
-
if not ok:
|
|
74
|
-
any_failed = True
|
|
75
|
-
results.append((ok, snapshot, is_recorded, msg))
|
|
76
|
-
|
|
77
|
-
if any_failed:
|
|
78
|
-
return SnapshotException(results)
|
|
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)))
|
|
79
64
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
)
|
|
83
79
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
87
89
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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"))
|
|
91
98
|
|
|
92
|
-
reset = config.getvalue("--regtest-reset")
|
|
93
99
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if orig_identifer is not None:
|
|
98
|
-
self._reset_regtest_outputs.append(recorded_output_path + ".item")
|
|
99
|
-
with open(recorded_output_path + ".item", "w") as fh:
|
|
100
|
-
print(orig_identifer, file=fh)
|
|
101
|
-
self._reset_regtest_outputs.append(recorded_output_path)
|
|
102
|
-
return True, True, None
|
|
100
|
+
class PytestRegtestPlugin:
|
|
101
|
+
def __init__(self, recorder):
|
|
102
|
+
self.recorder = recorder
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if
|
|
109
|
-
|
|
110
|
-
msg = handler.show_differences(obj, recorded_obj, has_markup)
|
|
111
|
-
return True, False, msg
|
|
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
|
|
112
110
|
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
if item.get_closest_marker("xfail") and item.config.getvalue("--regtest-reset"):
|
|
112
|
+
# enforce consistency with xfail:
|
|
113
|
+
assert False
|
|
115
114
|
|
|
116
115
|
def check_recorded_output(self, item):
|
|
117
116
|
test_folder = item.fspath.dirname
|
|
118
117
|
regtest_stream = item.regtest_stream
|
|
119
|
-
|
|
118
|
+
version = regtest_stream.version or regtest_stream.identifier
|
|
119
|
+
if not isinstance(regtest_stream, RegtestStream):
|
|
120
|
+
return
|
|
120
121
|
|
|
121
122
|
orig_identifer, recorded_output_path = result_file_paths(
|
|
122
|
-
test_folder, item.nodeid,
|
|
123
|
+
test_folder, item.nodeid, version
|
|
123
124
|
)
|
|
124
125
|
config = item.config
|
|
125
126
|
|
|
@@ -131,10 +132,12 @@ class PytestRegtestPlugin:
|
|
|
131
132
|
with open(recorded_output_path + ".out", "w", encoding="utf-8") as fh:
|
|
132
133
|
fh.write("".join(regtest_stream.get_lines()))
|
|
133
134
|
if orig_identifer is not None:
|
|
134
|
-
self._reset_regtest_outputs.append(
|
|
135
|
+
self.recorder._reset_regtest_outputs.append(
|
|
136
|
+
recorded_output_path + ".item"
|
|
137
|
+
)
|
|
135
138
|
with open(recorded_output_path + ".item", "w") as fh:
|
|
136
139
|
print(orig_identifer, file=fh)
|
|
137
|
-
self._reset_regtest_outputs.append(recorded_output_path + ".out")
|
|
140
|
+
self.recorder._reset_regtest_outputs.append(recorded_output_path + ".out")
|
|
138
141
|
return
|
|
139
142
|
|
|
140
143
|
if os.path.exists(recorded_output_path + ".out"):
|
|
@@ -154,7 +157,7 @@ class PytestRegtestPlugin:
|
|
|
154
157
|
tobe = [line.rstrip() for line in tobe]
|
|
155
158
|
|
|
156
159
|
if current != tobe:
|
|
157
|
-
self._failed_regtests.append(item)
|
|
160
|
+
self.recorder._failed_regtests.append(item)
|
|
158
161
|
return RecordedOutputException(
|
|
159
162
|
current,
|
|
160
163
|
tobe,
|
|
@@ -163,44 +166,6 @@ class PytestRegtestPlugin:
|
|
|
163
166
|
recorded_output_file_exists,
|
|
164
167
|
)
|
|
165
168
|
|
|
166
|
-
@pytest.hookimpl(hookwrapper=True)
|
|
167
|
-
def pytest_pyfunc_call(self, pyfuncitem):
|
|
168
|
-
stdout = sys.stdout
|
|
169
|
-
if "regtest_all" in pyfuncitem.fixturenames and hasattr(
|
|
170
|
-
pyfuncitem, "regtest_stream"
|
|
171
|
-
):
|
|
172
|
-
sys.stdout = pyfuncitem.regtest_stream
|
|
173
|
-
yield
|
|
174
|
-
sys.stdout = stdout
|
|
175
|
-
|
|
176
|
-
@pytest.hookimpl(hookwrapper=True)
|
|
177
|
-
def pytest_report_teststatus(self, report, config):
|
|
178
|
-
outcome = yield
|
|
179
|
-
if report.when == "call" and "uses-regtest" in report.keywords:
|
|
180
|
-
if config.getvalue("--regtest-reset"):
|
|
181
|
-
result = outcome.get_result()
|
|
182
|
-
if result[0] != "failed":
|
|
183
|
-
outcome.force_result((result[0], "R", "RESET"))
|
|
184
|
-
|
|
185
|
-
def pytest_terminal_summary(self, terminalreporter, exitstatus, config):
|
|
186
|
-
terminalreporter.ensure_newline()
|
|
187
|
-
terminalreporter.section("pytest-regtest report", sep="-", blue=True, bold=True)
|
|
188
|
-
terminalreporter.write("total number of failed regression tests: ", bold=True)
|
|
189
|
-
terminalreporter.line(str(len(self._failed_regtests)))
|
|
190
|
-
if config.getvalue("--regtest-reset"):
|
|
191
|
-
if config.option.verbose:
|
|
192
|
-
terminalreporter.line(
|
|
193
|
-
"the following output files have been reset:", bold=True
|
|
194
|
-
)
|
|
195
|
-
for path in self._reset_regtest_outputs:
|
|
196
|
-
rel_path = os.path.relpath(path)
|
|
197
|
-
terminalreporter.line(" " + rel_path)
|
|
198
|
-
else:
|
|
199
|
-
terminalreporter.write(
|
|
200
|
-
"total number of reset output files: ", bold=True
|
|
201
|
-
)
|
|
202
|
-
terminalreporter.line(str(len(self._reset_regtest_outputs)))
|
|
203
|
-
|
|
204
169
|
@pytest.hookimpl(hookwrapper=True)
|
|
205
170
|
def pytest_runtest_makereport(self, item, call):
|
|
206
171
|
outcome = yield
|
|
@@ -216,57 +181,185 @@ class PytestRegtestPlugin:
|
|
|
216
181
|
line = line.ljust(tw.fullwidth, "-")
|
|
217
182
|
tw.line(line, green=True)
|
|
218
183
|
tw.write(item.regtest_stream.get_output() + "\n", cyan=True)
|
|
219
|
-
|
|
220
|
-
snapshots = item.regtest_stream.snapshots
|
|
221
|
-
if snapshots:
|
|
222
|
-
tw.line()
|
|
223
|
-
line = "recorded snapshots: "
|
|
224
|
-
line = line.ljust(tw.fullwidth, "-")
|
|
184
|
+
line = "-" * tw.fullwidth
|
|
225
185
|
tw.line(line, green=True)
|
|
226
|
-
path = item.fspath.relto(item.session.fspath)
|
|
227
|
-
code_lines = item.fspath.readlines()
|
|
228
|
-
|
|
229
|
-
for obj, handler_options, line_no in snapshots:
|
|
230
|
-
info = code_lines[line_no - 1].strip()
|
|
231
|
-
tw.line(f"> {path} +{line_no}")
|
|
232
|
-
tw.line(f"> {info}")
|
|
233
|
-
handler = get_snapshot_handler(
|
|
234
|
-
obj, handler_options, item.config
|
|
235
|
-
)
|
|
236
|
-
lines = handler.show(obj)
|
|
237
|
-
for line in lines:
|
|
238
|
-
tw.line(line, cyan=True)
|
|
239
|
-
if output or snapshots:
|
|
240
|
-
tw.line("-" * tw.fullwidth, green=True)
|
|
241
|
-
tw.line()
|
|
242
|
-
tw.flush()
|
|
243
186
|
|
|
244
|
-
if call.when != "call" or not
|
|
187
|
+
if call.when != "call" or not getattr(item, "regtest", False):
|
|
245
188
|
return
|
|
246
189
|
|
|
247
190
|
result.keywords["uses-regtest"] = True
|
|
248
191
|
|
|
249
192
|
if call.excinfo is not None:
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
193
|
+
all_lines, all_colors = [], []
|
|
194
|
+
if call.excinfo.type is RecordedOutputException:
|
|
195
|
+
output_exception = call.excinfo
|
|
253
196
|
if output_exception is not None:
|
|
254
197
|
lines, colors = self._handle_regtest_exception(
|
|
255
|
-
item, output_exception.args, result
|
|
198
|
+
item, output_exception.value.args, result
|
|
256
199
|
)
|
|
257
200
|
all_lines.extend(lines)
|
|
258
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
|
|
259
350
|
if snapshot_exception is not None:
|
|
260
351
|
lines, colors = self._handle_snapshot_exception(
|
|
261
|
-
item, snapshot_exception.args, result
|
|
352
|
+
item, snapshot_exception.value.args, result
|
|
262
353
|
)
|
|
263
354
|
all_lines.extend(lines)
|
|
264
355
|
all_colors.extend(colors)
|
|
356
|
+
else:
|
|
357
|
+
return
|
|
265
358
|
|
|
266
|
-
|
|
359
|
+
result.longrepr = CollectErrorRepr(all_lines, all_colors)
|
|
267
360
|
|
|
268
361
|
def _handle_snapshot_exception(self, item, exc_args, result):
|
|
269
|
-
|
|
362
|
+
snapshot = item.snapshot
|
|
270
363
|
lines = []
|
|
271
364
|
colors = []
|
|
272
365
|
|
|
@@ -276,17 +369,12 @@ class PytestRegtestPlugin:
|
|
|
276
369
|
RED = dict(red=True, bold=True)
|
|
277
370
|
GREEN = dict(green=True, bold=False)
|
|
278
371
|
|
|
279
|
-
|
|
280
|
-
""
|
|
281
|
-
if regtest_stream.identifier is None
|
|
282
|
-
else "__" + regtest_stream.identifier
|
|
283
|
-
)
|
|
284
|
-
headline = "\nsnapshot error(s) for {}:".format(nodeid)
|
|
372
|
+
headline = "\nsnapshot error(s) for {}:".format(item.nodeid)
|
|
285
373
|
lines.append(headline)
|
|
286
374
|
colors.append(NO_COLOR)
|
|
287
375
|
|
|
288
376
|
for ok, snapshot, is_recorded, msg in exc_args[0]:
|
|
289
|
-
obj, kw, line_no = snapshot
|
|
377
|
+
obj, version, kw, line_no = snapshot
|
|
290
378
|
info = code_lines[line_no - 1].strip()
|
|
291
379
|
|
|
292
380
|
path = item.fspath.relto(item.session.fspath)
|
|
@@ -322,45 +410,8 @@ class PytestRegtestPlugin:
|
|
|
322
410
|
|
|
323
411
|
return lines, colors
|
|
324
412
|
|
|
325
|
-
def _handle_regtest_exception(self, item, exc_args, result):
|
|
326
|
-
(
|
|
327
|
-
current,
|
|
328
|
-
recorded,
|
|
329
|
-
recorded_output_path,
|
|
330
|
-
regtest_stream,
|
|
331
|
-
recorded_output_file_exists,
|
|
332
|
-
) = exc_args
|
|
333
|
-
|
|
334
|
-
nodeid = item.nodeid + (
|
|
335
|
-
""
|
|
336
|
-
if regtest_stream.identifier is None
|
|
337
|
-
else "__" + regtest_stream.identifier
|
|
338
|
-
)
|
|
339
|
-
if not recorded_output_file_exists:
|
|
340
|
-
msg = "\nregression test output not recorded yet for {}:\n".format(nodeid)
|
|
341
|
-
return (
|
|
342
|
-
[msg] + current,
|
|
343
|
-
[dict()] + len(current) * [dict(red=True, bold=True)],
|
|
344
|
-
)
|
|
345
413
|
|
|
346
|
-
|
|
347
|
-
diffs = list(
|
|
348
|
-
difflib.unified_diff(current, recorded, "current", "expected", lineterm="")
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
msg = "\nregression test output differences for {}:\n".format(nodeid)
|
|
352
|
-
|
|
353
|
-
if nodiff:
|
|
354
|
-
msg_diff = f" {len(diffs)} lines in diff"
|
|
355
|
-
else:
|
|
356
|
-
recorded_output_path = os.path.relpath(recorded_output_path)
|
|
357
|
-
msg += f" (recorded output from {recorded_output_path})\n"
|
|
358
|
-
msg_diff = " > " + "\n > ".join(diffs)
|
|
359
|
-
|
|
360
|
-
return [msg, msg_diff + "\n"], [dict(), dict(red=True, bold=True)]
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
def result_file_paths(test_folder, nodeid, identifier):
|
|
414
|
+
def result_file_paths(test_folder, nodeid, version):
|
|
364
415
|
file_path, __, test_function_name = nodeid.partition("::")
|
|
365
416
|
file_name = os.path.basename(file_path)
|
|
366
417
|
|
|
@@ -381,8 +432,8 @@ def result_file_paths(test_folder, nodeid, identifier):
|
|
|
381
432
|
|
|
382
433
|
test_function_name = test_function_name.replace(" ", "_")
|
|
383
434
|
stem, __ = os.path.splitext(file_name)
|
|
384
|
-
if
|
|
385
|
-
output_file_name = stem + "." + test_function_name + "__" +
|
|
435
|
+
if version is not None:
|
|
436
|
+
output_file_name = stem + "." + test_function_name + "__" + str(version)
|
|
386
437
|
else:
|
|
387
438
|
output_file_name = stem + "." + test_function_name
|
|
388
439
|
|
|
@@ -394,16 +445,14 @@ def result_file_paths(test_folder, nodeid, identifier):
|
|
|
394
445
|
class RegtestStream:
|
|
395
446
|
def __init__(self, request):
|
|
396
447
|
request.node.regtest_stream = self
|
|
448
|
+
request.node.regtest = True
|
|
397
449
|
self.request = request
|
|
398
450
|
self.buffer = StringIO()
|
|
451
|
+
self.version = None
|
|
399
452
|
self.identifier = None
|
|
400
453
|
|
|
401
454
|
self.snapshots = []
|
|
402
455
|
|
|
403
|
-
def check(self, obj, **kw):
|
|
404
|
-
line_no = inspect.currentframe().f_back.f_lineno
|
|
405
|
-
self.snapshots.append((obj, kw, line_no))
|
|
406
|
-
|
|
407
456
|
def write(self, what):
|
|
408
457
|
self.buffer.write(what)
|
|
409
458
|
|
|
@@ -430,6 +479,25 @@ class RegtestStream:
|
|
|
430
479
|
return False # dont suppress exception
|
|
431
480
|
|
|
432
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
|
+
|
|
433
501
|
def cleanup(output, request):
|
|
434
502
|
for converter in _converters_pre:
|
|
435
503
|
output = converter(output, request)
|
|
@@ -461,7 +529,8 @@ _converters_pre = []
|
|
|
461
529
|
_converters_post = []
|
|
462
530
|
|
|
463
531
|
|
|
464
|
-
def clear_converters():
|
|
532
|
+
def clear_converters() -> None:
|
|
533
|
+
"""Unregisters all converters, including the builtin converters."""
|
|
465
534
|
_converters_pre.clear()
|
|
466
535
|
_converters_post.clear()
|
|
467
536
|
|
|
@@ -474,7 +543,20 @@ def _fix_pre_v2_converter_function(function):
|
|
|
474
543
|
return fixed_converter_function
|
|
475
544
|
|
|
476
545
|
|
|
477
|
-
def register_converter_pre(
|
|
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
|
+
"""
|
|
478
560
|
if function not in _converters_pre:
|
|
479
561
|
signature = inspect.signature(function)
|
|
480
562
|
# keep downward compatibility:
|
|
@@ -483,7 +565,19 @@ def register_converter_pre(function):
|
|
|
483
565
|
_converters_pre.append(function)
|
|
484
566
|
|
|
485
567
|
|
|
486
|
-
def register_converter_post(
|
|
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
|
+
"""
|
|
487
581
|
if function not in _converters_post:
|
|
488
582
|
signature = inspect.signature(function)
|
|
489
583
|
# keep downward compatibility:
|
|
@@ -540,10 +634,3 @@ class CollectErrorRepr(TerminalRepr):
|
|
|
540
634
|
def toterminal(self, out):
|
|
541
635
|
for message, color in zip(self.messages, self.colors):
|
|
542
636
|
out.line(message, **color)
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
def get_snapshot_handler(obj, handler_options, pytest_config):
|
|
546
|
-
for check, handler in snapshot_handlers:
|
|
547
|
-
if check(obj):
|
|
548
|
-
return handler(handler_options, pytest_config, tw)
|
|
549
|
-
raise ValueError(f"cannot take snapshot for type {obj.__class__}")
|