Skip to content

Commit

Permalink
Merge pull request #7 from mam-dev/add-option
Browse files Browse the repository at this point in the history
Only run when --check-litter is given
  • Loading branch information
bunny-therapist authored Nov 23, 2023
2 parents 5ec27d3 + 5d2f32c commit a725640
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 45 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ disallow_untyped_decorators = true
no_implicit_optional = true
no_implicit_reexport = true
strict_equality = true
strict_concatenate = true
extra_checks = true

[tool.ruff]
src = ["src", "tests"]
Expand Down
1 change: 1 addition & 0 deletions requirements-lint.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
ruff
black
mypy
pytest>=7.4.1 # Added pluggy typing
29 changes: 23 additions & 6 deletions src/pytest_litter/plugin/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,25 @@
TreeSnapshotFactory,
)

PARSER_GROUP = "pytest-litter"
RUN_CHECK_OPTION_DEST_NAME = "check_litter"


def pytest_addoption(parser: pytest.Parser) -> None:
"""Add options to pytest (pytest hook function)."""
group = parser.getgroup(PARSER_GROUP)
group.addoption(
"--check-litter",
action="store_true",
dest=RUN_CHECK_OPTION_DEST_NAME,
help="Fail if tests create/remove files.",
)


def pytest_configure(config: pytest.Config) -> None:
"""Configure pytest-litter plugin (pytest hook function)."""
if not config.getoption(RUN_CHECK_OPTION_DEST_NAME):
return
ignore_specs: list[IgnoreSpec] = []
basetemp: Optional[str] = config.getoption("basetemp", None)
if basetemp is not None:
Expand All @@ -42,11 +58,12 @@ def pytest_configure(config: pytest.Config) -> None:
config.stash[COMPARATOR_KEY] = SnapshotComparator(config=litter_config)


@pytest.hookimpl(hookwrapper=True) # type: ignore[misc]
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item: pytest.Item): # type: ignore[no-untyped-def]
yield
run_snapshot_comparison(
test_name=item.name,
config=item.config,
mismatch_cb=raise_test_error_from_comparison,
)
if item.config.getoption(RUN_CHECK_OPTION_DEST_NAME):
run_snapshot_comparison(
test_name=item.name,
config=item.config,
mismatch_cb=raise_test_error_from_comparison,
)
2 changes: 1 addition & 1 deletion tests/system_test/system_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ python3 -m venv "${VENV}"
"${VENV}/bin/python3" -m pip install --quiet --editable "${ROOT_DIR}" || exit 1

echo "Executing system test..."
pytest -p pytest-litter --basetemp=tmp || exit 1
pytest -p pytest-litter --basetemp=tmp --check-litter || exit 1

popd &> /dev/null || exit 1

Expand Down
137 changes: 100 additions & 37 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Tests for the plugin module."""
import itertools
from collections.abc import Iterable
from pathlib import Path
from typing import TYPE_CHECKING, Optional
from unittest.mock import Mock, call
from unittest.mock import ANY, Mock, call

import pytest

Expand Down Expand Up @@ -137,8 +138,26 @@ def fake_cb(tc: str, comparison: snapshots.SnapshotComparison) -> None:
mock_cb.assert_called_once_with(test_name, mock_comparator.compare.return_value)


@pytest.mark.parametrize("basetemp", [None, Path("tmp")])
def test_pytest_configure(monkeypatch: "MonkeyPatch", basetemp: Optional[Path]) -> None:
def test_pytest_addoption() -> None:
mock_parser = Mock(spec=pytest.Parser)
plugin.pytest_addoption(mock_parser)
assert mock_parser.mock_calls == [
call.getgroup("pytest-litter"),
call.getgroup().addoption(
"--check-litter",
action="store_true",
dest=plugin.RUN_CHECK_OPTION_DEST_NAME,
help=ANY,
),
]


@pytest.mark.parametrize(
"run_check, basetemp", itertools.product([True, False], [None, Path("tmp")])
)
def test_pytest_configure(
monkeypatch: "MonkeyPatch", run_check: bool, basetemp: Optional[Path]
) -> None:
mock_snapshot_factory_cls = Mock(spec=snapshots.TreeSnapshotFactory)
monkeypatch.setattr(
"pytest_litter.plugin.plugin.TreeSnapshotFactory",
Expand All @@ -153,7 +172,7 @@ def test_pytest_configure(monkeypatch: "MonkeyPatch", basetemp: Optional[Path])
spec=pytest.Config,
rootpath=Path("rootpath"),
stash={},
getoption=Mock(spec=pytest.Config.getoption, return_value=basetemp),
getoption=Mock(spec=pytest.Config.getoption, side_effect=[run_check, basetemp]),
)
mock_litter_config_cls = Mock(spec=snapshots.LitterConfig)
monkeypatch.setattr(
Expand All @@ -179,40 +198,55 @@ def test_pytest_configure(monkeypatch: "MonkeyPatch", basetemp: Optional[Path])

plugin.pytest_configure(mock_config)

mock_config.getoption.assert_called_once_with("basetemp", None)
mock_litter_config_cls.assert_called_once_with(
ignore_specs=expected_ignore_specs,
)
mock_snapshot_factory_cls.assert_called_once_with(
config=mock_litter_config_cls.return_value
)
mock_snapshot_factory_cls.return_value.create_snapshot.assert_called_once_with(
root=mock_config.rootpath
)
assert (
mock_config.stash[utils.SNAPSHOT_FACTORY_KEY]
is mock_snapshot_factory_cls.return_value
)
assert (
mock_config.stash[utils.SNAPSHOT_KEY]
is mock_snapshot_factory_cls.return_value.create_snapshot.return_value
)
assert mock_config.stash[utils.COMPARATOR_KEY] is mock_comparator_cls.return_value
expected_getoption_calls = [
call(plugin.RUN_CHECK_OPTION_DEST_NAME),
]
if run_check:
expected_getoption_calls.append(call("basetemp", None))
assert mock_config.getoption.mock_calls == expected_getoption_calls

if basetemp is not None:
mock_dir_ignore_spec.assert_has_calls(
[call(directory=mock_config.rootpath / basetemp)]
if run_check:
mock_litter_config_cls.assert_called_once_with(
ignore_specs=expected_ignore_specs,
)
mock_snapshot_factory_cls.assert_called_once_with(
config=mock_litter_config_cls.return_value
)
mock_snapshot_factory_cls.return_value.create_snapshot.assert_called_once_with(
root=mock_config.rootpath
)
assert (
mock_config.stash[utils.SNAPSHOT_FACTORY_KEY]
is mock_snapshot_factory_cls.return_value
)
assert (
mock_config.stash[utils.SNAPSHOT_KEY]
is mock_snapshot_factory_cls.return_value.create_snapshot.return_value
)
assert (
mock_config.stash[utils.COMPARATOR_KEY] is mock_comparator_cls.return_value
)

if basetemp is not None:
mock_dir_ignore_spec.assert_has_calls(
[call(directory=mock_config.rootpath / basetemp)]
)
else:
mock_dir_ignore_spec.assert_not_called()
mock_name_ignore_spec.assert_has_calls(
[
call(name="__pycache__"),
call(name="venv"),
call(name=".venv"),
call(name=".pytest_cache"),
]
)
else:
mock_dir_ignore_spec.assert_not_called()
mock_name_ignore_spec.assert_has_calls(
[
call(name="__pycache__"),
call(name="venv"),
call(name=".venv"),
call(name=".pytest_cache"),
]
)
mock_litter_config_cls.assert_not_called()
mock_snapshot_factory_cls.assert_not_called()
assert utils.SNAPSHOT_FACTORY_KEY not in mock_config.stash
assert utils.SNAPSHOT_KEY not in mock_config.stash
assert utils.COMPARATOR_KEY not in mock_config.stash


def test_pytest_runtest_call(monkeypatch: "MonkeyPatch") -> None:
Expand Down Expand Up @@ -240,11 +274,40 @@ def test_pytest_runtest_call(monkeypatch: "MonkeyPatch") -> None:
)


def test_pytest_runtest_call__no_check_litter(monkeypatch: "MonkeyPatch") -> None:
mock_run_comparison = Mock(spec=utils.run_snapshot_comparison)
monkeypatch.setattr(
"pytest_litter.plugin.plugin.run_snapshot_comparison",
mock_run_comparison,
)
mock_item = Mock(spec=pytest.Item)
mock_item.config = Mock(spec=pytest.Config)
mock_item.config.getoption.return_value = False

# The list comprehension is just to get past the yield statement#
_ = list(plugin.pytest_runtest_call(mock_item))

mock_item.config.getoption.assert_called_once_with(
plugin.RUN_CHECK_OPTION_DEST_NAME
)
mock_run_comparison.assert_not_called()


@pytest.mark.integration_test
def test_plugin_with_pytester(pytester: pytest.Pytester) -> None:
def test_plugin_with_pytester__check_litter(pytester: pytest.Pytester) -> None:
# pytester uses basetemp internally, so the case without basetemp
# cannot be tested using pytester.
pytester.copy_example("pytest.ini")
pytester.copy_example("suite_tests.py")
result: pytest.RunResult = pytester.runpytest()
result: pytest.RunResult = pytester.runpytest("--check-litter")
result.assert_outcomes(passed=2, xfailed=2)


@pytest.mark.integration_test
def test_plugin_with_pytester__no_check_litter(pytester: pytest.Pytester) -> None:
# pytester uses basetemp internally, so the case without basetemp
# cannot be tested using pytester.
pytester.copy_example("pytest.ini")
pytester.copy_example("suite_tests.py")
result: pytest.RunResult = pytester.runpytest()
result.assert_outcomes(passed=2, xpassed=2)

0 comments on commit a725640

Please sign in to comment.