Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ignore unused snapshots for skipped test #862

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/syrupy/__init__.py
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
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
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
@@ -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