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.
@@ -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)