Skip to content

Commit

Permalink
fix: ignore unused snapshots for skipped test (#862)
Browse files Browse the repository at this point in the history
  • Loading branch information
AllanChain authored Aug 21, 2024
1 parent 2105b52 commit 3f6e301
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 11 deletions.
11 changes: 6 additions & 5 deletions src/syrupy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,15 @@ def pytest_collection_finish(session: Any) -> None:
session.config._syrupy.select_items(session.items)


def pytest_runtest_logfinish(nodeid: str) -> None:
def pytest_runtest_logreport(report: pytest.TestReport) -> None:
"""
At the end of running the runtest protocol for a single item.
https://docs.pytest.org/en/latest/reference.html#_pytest.hookspec.pytest_runtest_logfinish
After each of the setup, call and teardown runtest phases of an item.
https://docs.pytest.org/en/8.0.x/reference/reference.html#pytest.hookspec.pytest_runtest_logreport
"""
global _syrupy
if _syrupy:
_syrupy.ran_item(nodeid)
# The outcome will be passed in the teardown phase even if skipped
if _syrupy and report.when != "teardown":
_syrupy.ran_item(report.nodeid, report.outcome)


@pytest.hookimpl(tryfirst=True)
Expand Down
37 changes: 35 additions & 2 deletions src/syrupy/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import pytest

from .assertion import SnapshotAssertion
from .session import ItemStatus


@dataclass
Expand All @@ -59,7 +60,7 @@ class SnapshotReport:
# Initial arguments to the report
base_dir: Path
collected_items: Set["pytest.Item"]
selected_items: Dict[str, bool]
selected_items: Dict[str, "ItemStatus"]
options: "argparse.Namespace"
assertions: List["SnapshotAssertion"]

Expand Down Expand Up @@ -196,6 +197,14 @@ def num_unused(self) -> int:
def selected_all_collected_items(self) -> bool:
return self._collected_items_by_nodeid.keys() == self.selected_items.keys()

@property
def skipped_items(self) -> Iterator["pytest.Item"]:
return (
self._collected_items_by_nodeid[nodeid]
for nodeid in self.selected_items
if self.selected_items[nodeid].value == "skipped"
)

@property
def ran_items(self) -> Iterator["pytest.Item"]:
return (
Expand Down Expand Up @@ -230,7 +239,13 @@ def unused(self) -> "SnapshotCollections":
if self.selected_all_collected_items and not any(provided_nodes):
# All collected tests were run and files were not filtered by ::node
# therefore the snapshot collection file at this location can be deleted
unused_snapshots = {*unused_snapshot_collection}
unused_snapshots = {
snapshot
for snapshot in unused_snapshot_collection
if not self._skipped_items_match_name(
snapshot_location=snapshot_location, snapshot_name=snapshot.name
)
}
mark_for_removal = snapshot_location not in self.used
else:
unused_snapshots = {
Expand All @@ -244,6 +259,9 @@ def unused(self) -> "SnapshotCollections":
snapshot_name=snapshot.name,
provided_nodes=provided_nodes,
)
and not self._skipped_items_match_name(
snapshot_location=snapshot_location, snapshot_name=snapshot.name
)
}
mark_for_removal = False

Expand Down Expand Up @@ -451,6 +469,21 @@ def _ran_items_match_name(self, snapshot_location: str, snapshot_name: str) -> b
return True
return False

def _skipped_items_match_name(
self, snapshot_location: str, snapshot_name: str
) -> bool:
"""
Check that a snapshot name should be treated as skipped by the current session
This being true means that it will not be deleted even if the it is unused
"""
for item in self.skipped_items:
location = PyTestLocation(item)
if location.matches_snapshot_location(
snapshot_location
) and location.matches_snapshot_name(snapshot_name):
return True
return False

def _selected_items_match_name(
self, snapshot_location: str, snapshot_name: str
) -> bool:
Expand Down
21 changes: 17 additions & 4 deletions src/syrupy/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
dataclass,
field,
)
from enum import Enum
from pathlib import Path
from typing import (
TYPE_CHECKING,
Expand All @@ -11,6 +12,7 @@
Dict,
Iterable,
List,
Literal,
Optional,
Set,
Tuple,
Expand All @@ -37,6 +39,13 @@
from .extensions.base import AbstractSyrupyExtension


class ItemStatus(Enum):
NOT_RUN = False
PASSED = "passed"
FAILED = "failed"
SKIPPED = "skipped"


@dataclass
class SnapshotSession:
pytest_session: "pytest.Session"
Expand All @@ -45,7 +54,7 @@ class SnapshotSession:
# All the collected test items
_collected_items: Set["pytest.Item"] = field(default_factory=set)
# All the selected test items. Will be set to False until the test item is run.
_selected_items: Dict[str, bool] = field(default_factory=dict)
_selected_items: Dict[str, ItemStatus] = field(default_factory=dict)
_assertions: List["SnapshotAssertion"] = field(default_factory=list)
_extensions: Dict[str, "AbstractSyrupyExtension"] = field(default_factory=dict)

Expand Down Expand Up @@ -97,7 +106,9 @@ def collect_items(self, items: List["pytest.Item"]) -> None:

def select_items(self, items: List["pytest.Item"]) -> None:
for item in self.filter_valid_items(items):
self._selected_items[getattr(item, "nodeid")] = False # noqa: B009
self._selected_items[getattr(item, "nodeid")] = ( # noqa: B009
ItemStatus.NOT_RUN
)

def start(self) -> None:
self.report = None
Expand All @@ -107,9 +118,11 @@ def start(self) -> None:
self._extensions = {}
self._locations_discovered = defaultdict(set)

def ran_item(self, nodeid: str) -> None:
def ran_item(
self, nodeid: str, outcome: Literal["passed", "skipped", "failed"]
) -> None:
if nodeid in self._selected_items:
self._selected_items[nodeid] = True
self._selected_items[nodeid] = ItemStatus(outcome)

def finish(self) -> int:
exitstatus = 0
Expand Down
74 changes: 74 additions & 0 deletions tests/integration/test_snapshot_skipped.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import pytest


@pytest.fixture
def testcases():
return {
"used": (
"""
def test_used(snapshot):
assert snapshot == 'used'
"""
),
"raise-skipped": (
"""
import pytest
def test_skipped(snapshot):
pytest.skip("Skipping...")
assert snapshot == 'unused'
"""
),
"mark-skipped": (
"""
import pytest
@pytest.mark.skip
def test_skipped(snapshot):
assert snapshot == 'unused'
"""
),
"not-skipped": (
"""
def test_skipped(snapshot):
assert snapshot == 'unused'
"""
),
}


@pytest.fixture
def run_testcases(testdir, testcases):
pyfile_content = "\n\n".join([testcases["used"], testcases["not-skipped"]])
testdir.makepyfile(test_file=pyfile_content)
result = testdir.runpytest("-v", "--snapshot-update")
result.stdout.re_match_lines(r"2 snapshots generated\.")
return testdir, testcases


def test_mark_skipped_snapshots(run_testcases):
testdir, testcases = run_testcases
pyfile_content = "\n\n".join([testcases["used"], testcases["mark-skipped"]])
testdir.makepyfile(test_file=pyfile_content)

result = testdir.runpytest("-v")
result.stdout.re_match_lines(r"1 snapshot passed\.$")
assert result.ret == 0


def test_raise_skipped_snapshots(run_testcases):
testdir, testcases = run_testcases
pyfile_content = "\n\n".join([testcases["used"], testcases["raise-skipped"]])
testdir.makepyfile(test_file=pyfile_content)

result = testdir.runpytest("-v")
result.stdout.re_match_lines(r"1 snapshot passed\.$")
assert result.ret == 0


def test_skipped_snapshots_update(run_testcases):
testdir, testcases = run_testcases
pyfile_content = "\n\n".join([testcases["used"], testcases["raise-skipped"]])
testdir.makepyfile(test_file=pyfile_content)

result = testdir.runpytest("-v", "--snapshot-update")
result.stdout.re_match_lines(r"1 snapshot passed\.$")
assert result.ret == 0

0 comments on commit 3f6e301

Please sign in to comment.