1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
from collections.abc import Callable
from logging import Logger
from pathlib import Path
from typing import Any
from collections.abc import Generator
import pytest
from _pytest.config import Config
from _pytest.fixtures import FixtureRequest
from testlib.environ import environ
def pytest_addoption(parser: pytest.Parser) -> None:
"""
Adds an --accept-tests flag to pytest, which updates .exp files handled by snapshots
:param parser: pytest parser, which handles the arguments
"""
group = parser.getgroup("snapshots")
group.addoption(
"--accept-tests",
action="store_true",
help="if expected output should be updated",
dest="accept-tests",
)
@pytest.fixture(scope="session")
def do_snapshot_update(request: FixtureRequest) -> bool:
return request.config.getoption("accept-tests") or (environ.get("_NIX_TEST_ACCEPT") is not None)
class Snapshot:
def __init__(self, expected_output_path: Path, do_update: bool, logger: Logger):
"""
A snapshot keeps track of the expected output within the given file.
If the given file does not exist, an empty string will be assumed as the expected output
If the `--accept-tests` flag is set or (for legacy reasons) the `_NIX_TEST_ACCEPT` environment variable is set
The expected output will be updated with the current actual output
The snapshot must be the **left hand operator** of the equal check.
Example usage: `snapshot("eval_out.exp") == actual_out`
:param expected_output_path: where the expected output file is located
:param do_update: if the output should be updated
:param logger: logger for debugging stuff, provided by pytest
"""
self.expected_output_path = expected_output_path
# assume empty string, if there is no expected output file (yet)
self.content = expected_output_path.read_text() if expected_output_path.exists() else ""
self.do_update = do_update
self.logger = logger
self.did_update = False
def __eq__(self, other: Any) -> bool:
"""
Checks if the expected output equals the actual output.
If the `--accept-tests` flag is set to `1`, the expected output will be updated with the actual output
:param other: value to compare against, must survive the str() operator
:return: True if equals or were updated, False if unequal
"""
are_equal = self.content == str(other)
if self.do_update and not are_equal:
self.expected_output_path.write_text(str(other))
if not self.expected_output_path.is_symlink():
self.logger.warning(
"snapshot didn't propagate, the updated file can be found here: %s",
self.expected_output_path.absolute(),
)
self.did_update = True
return True
return are_equal
def __repr__(self) -> str:
return f"{self.content!r}"
def __str__(self) -> str:
return self.content
@pytest.fixture
def snapshot(
do_snapshot_update: bool, logger: Logger, tmp_path: Path
) -> Generator[Callable[[str], Snapshot], Any, None]:
"""
create a snapshot for the given output file
the snapshot must be the **left hand operator** of the equal check.
Example usage: `snapshot("eval_out.exp") == actual_out`
When either the `--accept-tests` flag or the `_NIX_TEST_ACCEPT` environment variable is set,
the snapshot will update the expected output with the actual output.
The given path is relative to the temporary test dir (provided by either `files` or `tmp_path`)
It is recommended to make the file a symlink back to the file within functional2
as otherwise the expected output won't be updated.
:return: a snapshot object which one can use `==` on
"""
tmp_path = tmp_path / "test-home"
tmp_path.mkdir(parents=True, exist_ok=True)
snaps: list[Snapshot] = []
def create_snapshot(expected_output_path: str) -> Snapshot:
exp_path = tmp_path / expected_output_path
if not exp_path.is_symlink():
logger.warning(
"expected output file isn't a symlink. When accepting the tests output, the update might not propagate"
)
obj = Snapshot(exp_path, do_snapshot_update, logger)
snaps.append(obj)
return obj
yield create_snapshot
if any(s.did_update for s in snaps):
pytest.skip("Updated Golden Files")
def pytest_assertrepr_compare(config: Config, op: str, left: Any, right: Any) -> list[str] | None:
if not isinstance(left, Snapshot) or op != "==" or not isinstance(right, str):
return None
left: Snapshot
right: str
exp_lines = str(left).splitlines()
act_lines = right.splitlines()
expl = [
f"snapshot({left.expected_output_path.name}) == output. Consider using `--accept-tests` to update the golden files."
]
if config.get_verbosity():
expl.append("The following lines were mismatched:")
for i, (exp, act) in enumerate(zip(exp_lines, act_lines)):
if exp == act:
continue
expl.append(f"{i + 1}: - {exp}")
expl.append(f"{i + 1}: + {act}")
return expl
|