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

feat: fail unused snapshots #77

Merged
merged 14 commits into from
Dec 29, 2019
14 changes: 6 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ These are mostly guidelines, not rules. Use your best judgment, and feel free to

- [Commit Messages](#commit-messages)
- [Code Styleguide](#code-styleguide)
- [Specs Styleguide](#specs-styleguide)
- [Docs Styleguide](#docs-styleguide)

[Additional Notes](#additional-notes)

Expand All @@ -40,14 +38,14 @@ This project and everyone participating in it is governed by our [Code of Conduc

## What should I know before I get started

### Snapshot Testing

- Javascript snapshot testing [jest](https:/a/jestjs.io/docs/en/snapshot-testing)

### Python 3

- Python typing and hints: [typing](https://docs.python.org/3/library/typing.html)

### Snapshot Testing

- Javascript snapshot testing [jest](https://jestjs.io/docs/en/snapshot-testing)

### Releases

- Semantic versioning: [semver](https://semver.org/spec/v2.0.0.html)
Expand Down Expand Up @@ -91,12 +89,12 @@ Fill in the relevant sections, clearly linking the issue the change is attemping

## Styleguides

### Git Commit Messages
### Commit Messages

Provide semantic commit messages following this [convention](https://www.conventionalcommits.org/en/v1.0.0/#summary).
This informs the semantic versioning we use to control our [releases](#releases).

### Specs Styleguide
### Code Styleguide

A linter is available to catch most of our styling concerns.
This is provided in a pre-commit hook when setting up [local development](#local-development).
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ pytest --snapshot-update

A snapshot file should be generated under a `__snapshots__` directory in the same directory as `test_file.py`. The `__snapshots__` directory and all its children should be committed along with your test code.

### Options

These are the cli options exposed to `pytest` by the plugin.

| Option | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `--snapshot-update` | When supplied updates existing snapshots of any run tests, as well as deleting unused and generating new snapshots. |
| `--snapshot-warn-unused` | Syrupy default behaviour is to fail the test session when there any unused snapshots. This instructs the plugin not to fail. |

### Serializers

Syrupy comes with a few built-in serializers for you to choose from. You should also feel free to extend the AbstractSnapshotSerializer if your project has a need not captured by one our built-ins.
Expand Down
16 changes: 13 additions & 3 deletions src/syrupy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ def pytest_addoption(parser: Any) -> None:
dest="update_snapshots",
help="Update snapshots",
)
group.addoption(
"--snapshot-warn-unused",
action="store_true",
default=False,
dest="warn_unused_snapshots",
help="Do not fail on unused snapshots",
)


def pytest_assertrepr_compare(op: str, left: Any, right: Any) -> Optional[List[str]]:
Expand All @@ -48,7 +55,9 @@ def pytest_sessionstart(session: Any) -> None:
"""
config = session.config
session._syrupy = SnapshotSession(
update_snapshots=config.option.update_snapshots, base_dir=config.rootdir
warn_unused_snapshots=config.option.warn_unused_snapshots,
update_snapshots=config.option.update_snapshots,
base_dir=config.rootdir,
)
session._syrupy.start()

Expand All @@ -69,15 +78,16 @@ def pytest_collection_finish(session: Any) -> None:
session._syrupy._ran_items.update(session.items)


def pytest_sessionfinish(session: Any) -> None:
def pytest_sessionfinish(session: Any, exitstatus: int) -> None:
"""
Add syrupy report to pytest after whole test run finished, before exiting.
https://docs.pytest.org/en/latest/reference.html#_pytest.hookspec.pytest_sessionfinish
"""
reporter = session.config.pluginmanager.get_plugin("terminalreporter")
session._syrupy.finish()
syrupy_exitstatus = session._syrupy.finish()
for line in session._syrupy.report:
reporter.write_line(line)
session.exitstatus |= syrupy_exitstatus


@pytest.fixture
Expand Down
2 changes: 2 additions & 0 deletions src/syrupy/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
SNAPSHOT_DIRNAME = "__snapshots__"
SNAPSHOT_EMPTY_FILE = {"empty snapshot file"}
SNAPSHOT_UNKNOWN_FILE = {"unknown snapshot file"}

EXIT_STATUS_FAIL_UNUSED = 1
39 changes: 27 additions & 12 deletions src/syrupy/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
Set,
)

from .constants import SNAPSHOT_UNKNOWN_FILE
from .constants import (
EXIT_STATUS_FAIL_UNUSED,
SNAPSHOT_UNKNOWN_FILE,
)
from .location import TestLocation
from .terminal import (
bold,
Expand Down Expand Up @@ -42,7 +45,10 @@ def empty_snapshot_groups() -> "SnapshotGroups":


class SnapshotSession:
def __init__(self, *, update_snapshots: bool, base_dir: str):
def __init__(
self, *, warn_unused_snapshots: bool, update_snapshots: bool, base_dir: str
):
self.warn_unused_snapshots = warn_unused_snapshots
self.update_snapshots = update_snapshots
self.base_dir = base_dir
self.report: List[str] = []
Expand All @@ -60,7 +66,8 @@ def start(self) -> None:
self._serializers = {}
self._snapshot_groups = empty_snapshot_groups()

def finish(self) -> None:
def finish(self) -> int:
exitstatus = 0
self._collate_snapshots()
n_unused = self._count_snapshots(self._snapshot_groups.unused)
n_written = self._count_snapshots(self._snapshot_groups.created)
Expand Down Expand Up @@ -97,12 +104,15 @@ def finish(self) -> None:
]
if n_unused:
if self.update_snapshots:
text_singular = "{} snapshot deleted."
text_plural = "{} snapshots deleted."
text_singular = "{} unused snapshot deleted."
text_plural = "{} unused snapshots deleted."
else:
text_singular = "{} snapshot unused."
text_plural = "{} snapshots unused."
text_count = warning_style(n_unused)
if self.update_snapshots or self.warn_unused_snapshots:
text_count = warning_style(n_unused)
else:
text_count = error_style(n_unused)
summary_lines += [
ngettext(text_singular, text_plural, n_unused).format(text_count)
]
Expand All @@ -118,15 +128,20 @@ def finish(self) -> None:
path_to_file = os.path.relpath(filepath, self.base_dir)
deleted_snapshots = ", ".join(map(bold, sorted(snapshots)))
self.add_report_line(
f"Deleted {deleted_snapshots} ({path_to_file})"
gettext(f"Deleted {deleted_snapshots} ({path_to_file})")
)
else:
self.add_report_line(
gettext(
"Re-run pytest with --snapshot-update"
" to delete the unused snapshots."
)
message = gettext(
"Re-run pytest with --snapshot-update"
" to delete the unused snapshots."
)
if self.warn_unused_snapshots:
message = warning_style(message)
else:
message = error_style(message)
exitstatus |= EXIT_STATUS_FAIL_UNUSED
self.add_report_line(message)
return exitstatus

def add_report_line(self, line: str = "") -> None:
self.report += [line]
Expand Down
23 changes: 17 additions & 6 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def test_collected(snapshot):
result_stdout = clean_output(result.stdout.str())
assert "1 snapshot passed" in result_stdout
assert "1 snapshot updated" in result_stdout
assert "1 snapshot deleted" in result_stdout
assert "1 unused snapshot deleted" in result_stdout


def test_collection_parametrized(testdir):
Expand Down Expand Up @@ -83,7 +83,7 @@ def test_collected(snapshot, actual):
result_stdout = clean_output(result.stdout.str())
assert "2 snapshots passed" in result_stdout
assert "snapshot updated" not in result_stdout
assert "1 snapshot deleted" in result_stdout
assert "1 unused snapshot deleted" in result_stdout


@pytest.fixture
Expand Down Expand Up @@ -227,6 +227,17 @@ def test_unused_snapshots(stubs):
assert "snapshots generated" not in result_stdout
assert "4 snapshots passed" in result_stdout
assert "1 snapshot unused" in result_stdout
assert result.ret == 1


def test_unused_snapshots_warning(stubs):
_, testdir, tests, _ = stubs
testdir.makepyfile(test_file="\n\n".join(tests[k] for k in tests if k != "unused"))
result = testdir.runpytest("-v", "--snapshot-warn-unused")
result_stdout = clean_output(result.stdout.str())
assert "snapshots generated" not in result_stdout
assert "4 snapshots passed" in result_stdout
assert "1 snapshot unused" in result_stdout
assert result.ret == 0


Expand All @@ -237,7 +248,7 @@ def test_removed_snapshots(stubs):
result = testdir.runpytest("-v", "--snapshot-update")
result_stdout = clean_output(result.stdout.str())
assert "snapshot unused" not in result_stdout
assert "1 snapshot deleted" in result_stdout
assert "1 unused snapshot deleted" in result_stdout
assert result.ret == 0
assert os.path.isfile(filepath)

Expand All @@ -249,7 +260,7 @@ def test_removed_snapshot_file(stubs):
result = testdir.runpytest("-v", "--snapshot-update")
result_stdout = clean_output(result.stdout.str())
assert "snapshots unused" not in result_stdout
assert "5 snapshots deleted" in result_stdout
assert "5 unused snapshots deleted" in result_stdout
assert result.ret == 0
assert not os.path.isfile(filepath)

Expand All @@ -263,7 +274,7 @@ def test_removed_empty_snapshot_file_only(stubs):
result = testdir.runpytest("-v", "--snapshot-update")
result_stdout = clean_output(result.stdout.str())
assert os.path.relpath(filepath) not in result_stdout
assert "1 snapshot deleted" in result_stdout
assert "1 unused snapshot deleted" in result_stdout
assert "empty snapshot file" in result_stdout
assert os.path.relpath(empty_filepath) in result_stdout
assert result.ret == 0
Expand All @@ -280,7 +291,7 @@ def test_removed_hanging_snapshot_file(stubs):
result = testdir.runpytest("-v", "--snapshot-update")
result_stdout = clean_output(result.stdout.str())
assert os.path.relpath(filepath) not in result_stdout
assert "1 snapshot deleted" in result_stdout
assert "1 unused snapshot deleted" in result_stdout
assert "unknown snapshot file" in result_stdout
assert os.path.relpath(hanging_filepath) in result_stdout
assert result.ret == 0
Expand Down
2 changes: 1 addition & 1 deletion tests/test_single_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,5 @@ def test_updated_snapshots(stubs, testcases_updated):
result = testdir.runpytest("-v", "--snapshot-update")
result_stdout = clean_output(result.stdout.str())
assert "1 snapshot updated" in result_stdout
assert "1 snapshot deleted" in result_stdout
assert "1 unused snapshot deleted" in result_stdout
assert result.ret == 0