Skip to content

Commit 596b29b

Browse files
authored
fix: improve reporting around xfailed snapshots, close #736 (#769)
1 parent 6a93c87 commit 596b29b

File tree

5 files changed

+96
-17
lines changed

5 files changed

+96
-17
lines changed

src/syrupy/assertion.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class AssertionResult:
4545
updated: bool
4646
success: bool
4747
exception: Optional[Exception]
48+
test_location: "PyTestLocation"
4849

4950
@property
5051
def final_data(self) -> Optional["SerializedData"]:
@@ -303,14 +304,15 @@ def _assert(self, data: "SerializableData") -> bool:
303304
snapshot_updated = matches is False and assertion_success
304305
self._execution_name_index[self.index] = self._executions
305306
self._execution_results[self._executions] = AssertionResult(
307+
asserted_data=serialized_data,
308+
created=snapshot_created,
309+
exception=assertion_exception,
310+
recalled_data=snapshot_data,
306311
snapshot_location=snapshot_location,
307312
snapshot_name=snapshot_name,
308-
recalled_data=snapshot_data,
309-
asserted_data=serialized_data,
310313
success=assertion_success,
311-
created=snapshot_created,
314+
test_location=self.test_location,
312315
updated=snapshot_updated,
313-
exception=assertion_exception,
314316
)
315317
self._executions += 1
316318
self._post_assert()

src/syrupy/location.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
@dataclass
1717
class PyTestLocation:
18-
_node: "pytest.Item"
18+
item: "pytest.Item"
1919
nodename: Optional[str] = field(init=False)
2020
testname: str = field(init=False)
2121
methodname: str = field(init=False)
@@ -28,16 +28,16 @@ def __post_init__(self) -> None:
2828
self.__attrs_post_init_def__()
2929

3030
def __attrs_post_init_def__(self) -> None:
31-
node_path: Path = getattr(self._node, "path") # noqa: B009
31+
node_path: Path = getattr(self.item, "path") # noqa: B009
3232
self.filepath = str(node_path.absolute())
33-
obj = getattr(self._node, "obj") # noqa: B009
33+
obj = getattr(self.item, "obj") # noqa: B009
3434
self.modulename = obj.__module__
3535
self.methodname = obj.__name__
36-
self.nodename = getattr(self._node, "name", None)
36+
self.nodename = getattr(self.item, "name", None)
3737
self.testname = self.nodename or self.methodname
3838

3939
def __attrs_post_init_doc__(self) -> None:
40-
doctest = getattr(self._node, "dtest") # noqa: B009
40+
doctest = getattr(self.item, "dtest") # noqa: B009
4141
self.filepath = doctest.filename
4242
test_relfile, test_node = self.nodeid.split(PYTEST_NODE_SEP)
4343
test_relpath = Path(test_relfile)
@@ -64,7 +64,7 @@ def nodeid(self) -> str:
6464
:raises: `AttributeError` if node has no node id
6565
:return: test node id
6666
"""
67-
return str(getattr(self._node, "nodeid")) # noqa: B009
67+
return str(getattr(self.item, "nodeid")) # noqa: B009
6868

6969
@property
7070
def basename(self) -> str:
@@ -78,7 +78,7 @@ def snapshot_name(self) -> str:
7878

7979
@property
8080
def is_doctest(self) -> bool:
81-
return self.__is_doctest(self._node)
81+
return self.__is_doctest(self.item)
8282

8383
def __is_doctest(self, node: "pytest.Item") -> bool:
8484
return hasattr(node, "dtest")

src/syrupy/report.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
Set,
2323
)
2424

25+
from _pytest.skipping import xfailed_key
26+
2527
from .constants import PYTEST_NODE_SEP
2628
from .data import (
2729
Snapshot,
@@ -70,6 +72,7 @@ class SnapshotReport:
7072
used: "SnapshotCollections" = field(default_factory=SnapshotCollections)
7173
_provided_test_paths: Dict[str, List[str]] = field(default_factory=dict)
7274
_keyword_expressions: Set["Expression"] = field(default_factory=set)
75+
_num_xfails: int = field(default=0)
7376

7477
@property
7578
def update_snapshots(self) -> bool:
@@ -89,6 +92,14 @@ def _collected_items_by_nodeid(self) -> Dict[str, "pytest.Item"]:
8992
getattr(item, "nodeid"): item for item in self.collected_items # noqa: B009
9093
}
9194

95+
def _has_xfail(self, item: "pytest.Item") -> bool:
96+
# xfailed_key is 'private'. I'm open to a better way to do this:
97+
if xfailed_key in item.stash:
98+
result = item.stash[xfailed_key]
99+
if result:
100+
return result.run
101+
return False
102+
92103
def __post_init__(self) -> None:
93104
self.__parse_invocation_args()
94105

@@ -113,13 +124,17 @@ def __post_init__(self) -> None:
113124
Snapshot(name=result.snapshot_name, data=result.final_data)
114125
)
115126
self.used.update(snapshot_collection)
127+
116128
if result.created:
117129
self.created.update(snapshot_collection)
118130
elif result.updated:
119131
self.updated.update(snapshot_collection)
120132
elif result.success:
121133
self.matched.update(snapshot_collection)
122134
else:
135+
has_xfail = self._has_xfail(item=result.test_location.item)
136+
if has_xfail:
137+
self._num_xfails += 1
123138
self.failed.update(snapshot_collection)
124139

125140
def __parse_invocation_args(self) -> None:
@@ -161,7 +176,7 @@ def __parse_invocation_args(self) -> None:
161176
def num_created(self) -> int:
162177
return self._count_snapshots(self.created)
163178

164-
@property
179+
@cached_property
165180
def num_failed(self) -> int:
166181
return self._count_snapshots(self.failed)
167182

@@ -256,14 +271,22 @@ def lines(self) -> Iterator[str]:
256271
```
257272
"""
258273
summary_lines: List[str] = []
259-
if self.num_failed:
274+
if self.num_failed and self._num_xfails < self.num_failed:
260275
summary_lines.append(
261276
ngettext(
262277
"{} snapshot failed.",
263278
"{} snapshots failed.",
264-
self.num_failed,
265-
).format(error_style(self.num_failed))
279+
self.num_failed - self._num_xfails,
280+
).format(error_style(self.num_failed - self._num_xfails)),
266281
)
282+
if self._num_xfails:
283+
summary_lines.append(
284+
ngettext(
285+
"{} snapshot xfailed.",
286+
"{} snapshots xfailed.",
287+
self._num_xfails,
288+
).format(warning_style(self._num_xfails)),
289+
)
267290
if self.num_matched:
268291
summary_lines.append(
269292
ngettext(

tests/integration/test_xfail.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
def test_no_failure_printed_if_all_failures_xfailed(testdir):
2+
testdir.makepyfile(
3+
test_file=(
4+
"""
5+
import pytest
6+
7+
@pytest.mark.xfail(reason="Failure expected.")
8+
def test_a(snapshot):
9+
assert snapshot == 'does-not-exist'
10+
"""
11+
)
12+
)
13+
result = testdir.runpytest("-v")
14+
result.stdout.no_re_match_line((r".*snapshot failed*"))
15+
assert result.ret == 0
16+
17+
18+
def test_failures_printed_if_only_some_failures_xfailed(testdir):
19+
testdir.makepyfile(
20+
test_file=(
21+
"""
22+
import pytest
23+
24+
@pytest.mark.xfail(reason="Failure expected.")
25+
def test_a(snapshot):
26+
assert snapshot == 'does-not-exist'
27+
28+
def test_b(snapshot):
29+
assert snapshot == 'other'
30+
"""
31+
)
32+
)
33+
result = testdir.runpytest("-v")
34+
result.stdout.re_match_lines((r".*1 snapshot failed*"))
35+
result.stdout.re_match_lines((r".*1 snapshot xfailed*"))
36+
assert result.ret == 1
37+
38+
39+
def test_failure_printed_if_xfail_does_not_run(testdir):
40+
testdir.makepyfile(
41+
test_file=(
42+
"""
43+
import pytest
44+
45+
@pytest.mark.xfail(False, reason="Failure expected.")
46+
def test_a(snapshot):
47+
assert snapshot == 'does-not-exist'
48+
"""
49+
)
50+
)
51+
result = testdir.runpytest("-v")
52+
result.stdout.re_match_lines((r".*1 snapshot failed*"))
53+
result.stdout.no_re_match_line((r".*1 snapshot xfailed*"))
54+
assert result.ret == 1

tests/syrupy/extensions/amber/test_amber_snapshot_diff.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ def test_snapshot_diff_id(snapshot):
5151
assert dictCase3 == snapshot(name="case3", diff="large snapshot")
5252

5353

54+
@pytest.mark.xfail(reason="Asserting snapshot does not exist")
5455
def test_snapshot_no_diff_raises_exception(snapshot):
5556
my_dict = {
5657
"field_0": "value_0",
5758
}
58-
with pytest.raises(AssertionError, match="SnapshotDoesNotExist"):
59-
assert my_dict == snapshot(diff="does not exist index")
59+
assert my_dict == snapshot(diff="does not exist index")

0 commit comments

Comments
 (0)