pytest-regtest 2.2.0a2__py2.py3-none-any.whl → 2.2.1__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 +38 -6
- pytest_regtest/numpy_handler.py +13 -0
- pytest_regtest/pytest_regtest.py +285 -194
- pytest_regtest/register_third_party_handlers.py +6 -22
- pytest_regtest/snapshot_handler.py +130 -20
- pytest_regtest-2.2.1.dist-info/METADATA +90 -0
- pytest_regtest-2.2.1.dist-info/RECORD +12 -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.2.1.dist-info}/WHEEL +0 -0
- {pytest_regtest-2.2.0a2.dist-info → pytest_regtest-2.2.1.dist-info}/entry_points.txt +0 -0
- {pytest_regtest-2.2.0a2.dist-info → pytest_regtest-2.2.1.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,187 @@ 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
|
+
obj, version, handler_options, _ = snapshot
|
|
275
|
+
handler = get_snapshot_handler(obj, handler_options, item.config)
|
|
276
|
+
|
|
277
|
+
test_folder = item.fspath.dirname
|
|
278
|
+
if version is not None:
|
|
279
|
+
identifier = str(version) + "__" + str(idx)
|
|
280
|
+
else:
|
|
281
|
+
identifier = str(idx)
|
|
282
|
+
|
|
283
|
+
config = item.config
|
|
284
|
+
|
|
285
|
+
orig_identifer, recorded_output_path = result_file_paths(
|
|
286
|
+
test_folder, item.nodeid, identifier
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
reset = config.getvalue("--regtest-reset")
|
|
290
|
+
|
|
291
|
+
if reset:
|
|
292
|
+
os.makedirs(recorded_output_path, exist_ok=True)
|
|
293
|
+
handler.save(recorded_output_path, obj)
|
|
294
|
+
if orig_identifer is not None:
|
|
295
|
+
self.recorder._reset_snapshots.append(recorded_output_path + ".item")
|
|
296
|
+
with open(recorded_output_path + ".item", "w") as fh:
|
|
297
|
+
print(orig_identifer, file=fh)
|
|
298
|
+
self.recorder._reset_snapshots.append(recorded_output_path)
|
|
299
|
+
return True, True, None
|
|
300
|
+
|
|
301
|
+
has_markup = item.config.get_terminal_writer().hasmarkup
|
|
302
|
+
if os.path.exists(recorded_output_path):
|
|
303
|
+
recorded_obj = handler.load(recorded_output_path)
|
|
304
|
+
ok = handler.compare(obj, recorded_obj)
|
|
305
|
+
if ok:
|
|
306
|
+
return True, True, None
|
|
307
|
+
msg = handler.show_differences(obj, recorded_obj, has_markup)
|
|
308
|
+
return True, False, msg
|
|
309
|
+
|
|
310
|
+
msg = handler.show(obj)
|
|
311
|
+
return False, False, msg
|
|
312
|
+
|
|
313
|
+
@pytest.hookimpl(hookwrapper=True)
|
|
314
|
+
def pytest_runtest_makereport(self, item, call):
|
|
315
|
+
outcome = yield
|
|
316
|
+
result = outcome.get_result()
|
|
317
|
+
if call.when == "teardown" and hasattr(item, "snapshot"):
|
|
318
|
+
if item.config.getvalue("--regtest-tee"):
|
|
319
|
+
tw = TerminalWriter()
|
|
320
|
+
snapshots = item.snapshot.snapshots
|
|
321
|
+
if not snapshots:
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
tw.line()
|
|
325
|
+
line = "recorded snapshots: "
|
|
326
|
+
line = line.ljust(tw.fullwidth, "-")
|
|
327
|
+
tw.line(line, green=True)
|
|
328
|
+
path = item.fspath.relto(item.session.fspath)
|
|
329
|
+
code_lines = item.fspath.readlines()
|
|
330
|
+
|
|
331
|
+
for obj, version, handler_options, line_no in snapshots:
|
|
332
|
+
info = code_lines[line_no - 1].strip()
|
|
333
|
+
tw.line(f"> {path} +{line_no}")
|
|
334
|
+
tw.line(f"> {info}")
|
|
335
|
+
handler = get_snapshot_handler(obj, handler_options, item.config)
|
|
336
|
+
lines = handler.show(obj)
|
|
337
|
+
for line in lines:
|
|
338
|
+
tw.line(line, cyan=True)
|
|
339
|
+
tw.line("-" * tw.fullwidth, green=True)
|
|
340
|
+
tw.line()
|
|
341
|
+
tw.flush()
|
|
342
|
+
|
|
343
|
+
if call.when != "call" or not hasattr(item, "snapshot"):
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
result.keywords["uses-regtest"] = True
|
|
347
|
+
|
|
348
|
+
if call.excinfo is not None:
|
|
349
|
+
all_lines, all_colors = [], []
|
|
350
|
+
if call.excinfo.type is SnapshotException:
|
|
351
|
+
snapshot_exception = call.excinfo
|
|
259
352
|
if snapshot_exception is not None:
|
|
260
353
|
lines, colors = self._handle_snapshot_exception(
|
|
261
|
-
item, snapshot_exception.args, result
|
|
354
|
+
item, snapshot_exception.value.args, result
|
|
262
355
|
)
|
|
263
356
|
all_lines.extend(lines)
|
|
264
357
|
all_colors.extend(colors)
|
|
358
|
+
else:
|
|
359
|
+
return
|
|
265
360
|
|
|
266
|
-
|
|
361
|
+
result.longrepr = CollectErrorRepr(all_lines, all_colors)
|
|
267
362
|
|
|
268
363
|
def _handle_snapshot_exception(self, item, exc_args, result):
|
|
269
|
-
|
|
364
|
+
snapshot = item.snapshot
|
|
270
365
|
lines = []
|
|
271
366
|
colors = []
|
|
272
367
|
|
|
@@ -276,17 +371,12 @@ class PytestRegtestPlugin:
|
|
|
276
371
|
RED = dict(red=True, bold=True)
|
|
277
372
|
GREEN = dict(green=True, bold=False)
|
|
278
373
|
|
|
279
|
-
|
|
280
|
-
""
|
|
281
|
-
if regtest_stream.identifier is None
|
|
282
|
-
else "__" + regtest_stream.identifier
|
|
283
|
-
)
|
|
284
|
-
headline = "\nsnapshot error(s) for {}:".format(nodeid)
|
|
374
|
+
headline = "\nsnapshot error(s) for {}:".format(item.nodeid)
|
|
285
375
|
lines.append(headline)
|
|
286
376
|
colors.append(NO_COLOR)
|
|
287
377
|
|
|
288
378
|
for ok, snapshot, is_recorded, msg in exc_args[0]:
|
|
289
|
-
obj, kw, line_no = snapshot
|
|
379
|
+
obj, version, kw, line_no = snapshot
|
|
290
380
|
info = code_lines[line_no - 1].strip()
|
|
291
381
|
|
|
292
382
|
path = item.fspath.relto(item.session.fspath)
|
|
@@ -322,45 +412,8 @@ class PytestRegtestPlugin:
|
|
|
322
412
|
|
|
323
413
|
return lines, colors
|
|
324
414
|
|
|
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
415
|
|
|
334
|
-
|
|
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
|
-
|
|
346
|
-
nodiff = item.config.getvalue("--regtest-nodiff")
|
|
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):
|
|
416
|
+
def result_file_paths(test_folder, nodeid, version):
|
|
364
417
|
file_path, __, test_function_name = nodeid.partition("::")
|
|
365
418
|
file_name = os.path.basename(file_path)
|
|
366
419
|
|
|
@@ -381,8 +434,8 @@ def result_file_paths(test_folder, nodeid, identifier):
|
|
|
381
434
|
|
|
382
435
|
test_function_name = test_function_name.replace(" ", "_")
|
|
383
436
|
stem, __ = os.path.splitext(file_name)
|
|
384
|
-
if
|
|
385
|
-
output_file_name = stem + "." + test_function_name + "__" +
|
|
437
|
+
if version is not None:
|
|
438
|
+
output_file_name = stem + "." + test_function_name + "__" + str(version)
|
|
386
439
|
else:
|
|
387
440
|
output_file_name = stem + "." + test_function_name
|
|
388
441
|
|
|
@@ -394,16 +447,14 @@ def result_file_paths(test_folder, nodeid, identifier):
|
|
|
394
447
|
class RegtestStream:
|
|
395
448
|
def __init__(self, request):
|
|
396
449
|
request.node.regtest_stream = self
|
|
450
|
+
request.node.regtest = True
|
|
397
451
|
self.request = request
|
|
398
452
|
self.buffer = StringIO()
|
|
453
|
+
self.version = None
|
|
399
454
|
self.identifier = None
|
|
400
455
|
|
|
401
456
|
self.snapshots = []
|
|
402
457
|
|
|
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
458
|
def write(self, what):
|
|
408
459
|
self.buffer.write(what)
|
|
409
460
|
|
|
@@ -430,6 +481,20 @@ class RegtestStream:
|
|
|
430
481
|
return False # dont suppress exception
|
|
431
482
|
|
|
432
483
|
|
|
484
|
+
class Snapshot:
|
|
485
|
+
def __init__(self, request):
|
|
486
|
+
request.node.snapshot = self
|
|
487
|
+
request.node.regtest = True
|
|
488
|
+
self.request = request
|
|
489
|
+
self.buffer = StringIO()
|
|
490
|
+
|
|
491
|
+
self.snapshots = []
|
|
492
|
+
|
|
493
|
+
def check(self, obj, *, version=None, **kw):
|
|
494
|
+
line_no = inspect.currentframe().f_back.f_lineno
|
|
495
|
+
self.snapshots.append((obj, version, kw, line_no))
|
|
496
|
+
|
|
497
|
+
|
|
433
498
|
def cleanup(output, request):
|
|
434
499
|
for converter in _converters_pre:
|
|
435
500
|
output = converter(output, request)
|
|
@@ -461,7 +526,8 @@ _converters_pre = []
|
|
|
461
526
|
_converters_post = []
|
|
462
527
|
|
|
463
528
|
|
|
464
|
-
def clear_converters():
|
|
529
|
+
def clear_converters() -> None:
|
|
530
|
+
"""Unregisters all converters, including the builtin converters."""
|
|
465
531
|
_converters_pre.clear()
|
|
466
532
|
_converters_post.clear()
|
|
467
533
|
|
|
@@ -474,7 +540,20 @@ def _fix_pre_v2_converter_function(function):
|
|
|
474
540
|
return fixed_converter_function
|
|
475
541
|
|
|
476
542
|
|
|
477
|
-
def register_converter_pre(
|
|
543
|
+
def register_converter_pre(
|
|
544
|
+
function: Callable[[str, Optional[_pytest.fixtures.FixtureRequest]], None],
|
|
545
|
+
) -> None:
|
|
546
|
+
"""Registers a new conversion function at the head of the list
|
|
547
|
+
of existing converters.
|
|
548
|
+
|
|
549
|
+
Parameters:
|
|
550
|
+
function: Function to cleanup given string and remove data which can change
|
|
551
|
+
between test runs without affecting the correctness of the test.
|
|
552
|
+
The second argument is optional and is a `pytest` object which holds
|
|
553
|
+
information about the current `config` or the current test function.
|
|
554
|
+
This argument can be ignored in many situations.
|
|
555
|
+
|
|
556
|
+
"""
|
|
478
557
|
if function not in _converters_pre:
|
|
479
558
|
signature = inspect.signature(function)
|
|
480
559
|
# keep downward compatibility:
|
|
@@ -483,7 +562,19 @@ def register_converter_pre(function):
|
|
|
483
562
|
_converters_pre.append(function)
|
|
484
563
|
|
|
485
564
|
|
|
486
|
-
def register_converter_post(
|
|
565
|
+
def register_converter_post(
|
|
566
|
+
function: Callable[[str, Optional[_pytest.fixtures.FixtureRequest]], None],
|
|
567
|
+
) -> None:
|
|
568
|
+
"""Registers a new conversion function at the head of the list
|
|
569
|
+
of existing converters
|
|
570
|
+
|
|
571
|
+
Parameters:
|
|
572
|
+
function: Function to cleanup given string and remove data which can change
|
|
573
|
+
between test runs without affecting the correctness of the test.
|
|
574
|
+
The second argument is optional and is a `pytest` object which holds
|
|
575
|
+
information about the current `config` or the current test function.
|
|
576
|
+
This argument can be ignored in many situations.
|
|
577
|
+
"""
|
|
487
578
|
if function not in _converters_post:
|
|
488
579
|
signature = inspect.signature(function)
|
|
489
580
|
# keep downward compatibility:
|
|
@@ -543,7 +634,7 @@ class CollectErrorRepr(TerminalRepr):
|
|
|
543
634
|
|
|
544
635
|
|
|
545
636
|
def get_snapshot_handler(obj, handler_options, pytest_config):
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
637
|
+
handler = SnapshotHandlerRegistry.get_handler(obj)
|
|
638
|
+
if handler is None:
|
|
639
|
+
raise ValueError(f"cannot take snapshot for type {obj.__class__}")
|
|
640
|
+
return handler(handler_options, pytest_config, tw)
|
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import numpy as np # noqa: F401
|
|
3
|
-
|
|
4
|
-
HAS_NUMPY = True
|
|
5
|
-
except ImportError:
|
|
6
|
-
HAS_NUMPY = False
|
|
7
|
-
|
|
8
|
-
try:
|
|
9
|
-
import pandas as pd # noqa: F401
|
|
10
|
-
|
|
11
|
-
HAS_PANDAS = True
|
|
12
|
-
except ImportError:
|
|
13
|
-
HAS_PANDAS = False
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if HAS_PANDAS and HAS_NUMPY:
|
|
17
|
-
|
|
1
|
+
def register_pandas_handler():
|
|
18
2
|
def is_dataframe(obj):
|
|
19
3
|
try:
|
|
20
4
|
import pandas as pd
|
|
@@ -24,12 +8,12 @@ if HAS_PANDAS and HAS_NUMPY:
|
|
|
24
8
|
return False
|
|
25
9
|
|
|
26
10
|
from .pandas_handler import DataFrameHandler
|
|
27
|
-
from .snapshot_handler import
|
|
11
|
+
from .snapshot_handler import SnapshotHandlerRegistry
|
|
28
12
|
|
|
29
|
-
|
|
13
|
+
SnapshotHandlerRegistry.add_handler(is_dataframe, DataFrameHandler)
|
|
30
14
|
|
|
31
|
-
if HAS_NUMPY:
|
|
32
15
|
|
|
16
|
+
def register_numpy_handler():
|
|
33
17
|
def is_numpy(obj):
|
|
34
18
|
try:
|
|
35
19
|
import numpy as np
|
|
@@ -39,6 +23,6 @@ if HAS_NUMPY:
|
|
|
39
23
|
return False
|
|
40
24
|
|
|
41
25
|
from .numpy_handler import NumpyHandler
|
|
42
|
-
from .snapshot_handler import
|
|
26
|
+
from .snapshot_handler import SnapshotHandlerRegistry
|
|
43
27
|
|
|
44
|
-
|
|
28
|
+
SnapshotHandlerRegistry.add_handler(is_numpy, NumpyHandler)
|