Skip to content

Commit

Permalink
Merge pull request #5 from mam-dev/speed
Browse files Browse the repository at this point in the history
Speed
  • Loading branch information
bunny-therapist authored Aug 8, 2023
2 parents 164896f + 9f0810a commit 5ec27d3
Show file tree
Hide file tree
Showing 12 changed files with 317 additions and 143 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ namespaces = false
[tool.pytest.ini_options]
addopts = "--random-order -p no:pytest-litter -p pytester -vv"
testpaths = ["tests"]
pytester_example_dir = "tests/pytester"
pytester_example_dir = "tests/suite"

[tool.coverage.run]
branch = true
Expand Down
1 change: 1 addition & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pytest
pytest-cov
coverage>=5.3
pytest-random-order
pytest-integration
18 changes: 13 additions & 5 deletions src/pytest_litter/plugin/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@

from pytest_litter.plugin.utils import (
COMPARATOR_KEY,
SNAPSHOT_FACTORY_KEY,
SNAPSHOT_KEY,
raise_test_error_from_comparison,
run_snapshot_comparison,
)
from pytest_litter.snapshots import (
DirectoryIgnoreSpec,
IgnoreSpec,
RegexIgnoreSpec,
LitterConfig,
NameIgnoreSpec,
SnapshotComparator,
TreeSnapshot,
TreeSnapshotFactory,
)


Expand All @@ -29,9 +31,15 @@ def pytest_configure(config: pytest.Config) -> None:
directory=config.rootpath / basetemp,
)
)
ignore_specs.append(RegexIgnoreSpec(regex=r".*/__pycache__.*"))
config.stash[SNAPSHOT_KEY] = TreeSnapshot(root=config.rootpath)
config.stash[COMPARATOR_KEY] = SnapshotComparator(ignore_specs=ignore_specs)
ignore_specs.append(NameIgnoreSpec(name="__pycache__"))
ignore_specs.append(NameIgnoreSpec(name="venv"))
ignore_specs.append(NameIgnoreSpec(name=".venv"))
ignore_specs.append(NameIgnoreSpec(name=".pytest_cache"))
litter_config = LitterConfig(ignore_specs=ignore_specs)
snapshot_factory = TreeSnapshotFactory(config=litter_config)
config.stash[SNAPSHOT_FACTORY_KEY] = snapshot_factory
config.stash[SNAPSHOT_KEY] = snapshot_factory.create_snapshot(root=config.rootpath)
config.stash[COMPARATOR_KEY] = SnapshotComparator(config=litter_config)


@pytest.hookimpl(hookwrapper=True) # type: ignore[misc]
Expand Down
13 changes: 11 additions & 2 deletions src/pytest_litter/plugin/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@

import pytest

from pytest_litter.snapshots import SnapshotComparator, SnapshotComparison, TreeSnapshot
from pytest_litter.snapshots import (
SnapshotComparator,
SnapshotComparison,
TreeSnapshot,
TreeSnapshotFactory,
)

SNAPSHOT_FACTORY_KEY = pytest.StashKey[TreeSnapshotFactory]()
SNAPSHOT_KEY = pytest.StashKey[TreeSnapshot]()
COMPARATOR_KEY = pytest.StashKey[SnapshotComparator]()

Expand Down Expand Up @@ -50,7 +56,10 @@ def run_snapshot_comparison(
) -> None:
"""Compare current and old snapshots and call mismatch_cb if there is a mismatch."""
original_snapshot: TreeSnapshot = config.stash[SNAPSHOT_KEY]
new_snapshot: TreeSnapshot = TreeSnapshot(root=original_snapshot.root)
snapshot_factory: TreeSnapshotFactory = config.stash[SNAPSHOT_FACTORY_KEY]
new_snapshot: TreeSnapshot = snapshot_factory.create_snapshot(
root=original_snapshot.root
)
config.stash[SNAPSHOT_KEY] = new_snapshot

comparator: SnapshotComparator = config.stash[COMPARATOR_KEY]
Expand Down
160 changes: 113 additions & 47 deletions src/pytest_litter/snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,71 @@ class UnexpectedLitterError(Exception):
"""Error that should not occur normally, indicative of some programming error."""


class IgnoreSpec(abc.ABC):
"""Specification about paths to ignore in comparisons."""

__slots__ = ()

@abc.abstractmethod
def matches(self, path: Path) -> bool:
... # pragma: no cover


class DirectoryIgnoreSpec(IgnoreSpec):
"""Specification to ignore everything in a given directory."""

__slots__ = ("_directory",)

def __init__(self, directory: Path) -> None:
self._directory = directory

def matches(self, path: Path) -> bool:
return self._directory == path or self._directory in path.parents


class NameIgnoreSpec(IgnoreSpec):
"""Specification to ignore all directories/files with a given name."""

__slots__ = ("_name",)

def __init__(self, name: str) -> None:
self._name = name

def matches(self, path: Path) -> bool:
return self._name in path.parts


class RegexIgnoreSpec(IgnoreSpec):
"""Regex-based specification about paths to ignore in comparisons."""

__slots__ = ("_regex",)

def __init__(self, regex: Union[str, re.Pattern[str]]) -> None:
self._regex: re.Pattern[str] = re.compile(regex)

def matches(self, path: Path) -> bool:
return self._regex.fullmatch(str(path)) is not None


class LitterConfig:
"""Configuration for pytest-litter."""

__slots__ = ("_ignore_specs",)

def __init__(self, ignore_specs: Optional[Iterable[IgnoreSpec]] = None) -> None:
"""Initialize.
Args:
ignore_specs: Specifies paths to ignore when doing the comparison.
"""
self._ignore_specs: frozenset[IgnoreSpec] = frozenset(ignore_specs or [])

@property
def ignore_specs(self) -> frozenset[IgnoreSpec]:
return self._ignore_specs


class PathSnapshot:
"""A snapshot of a path."""

Expand Down Expand Up @@ -42,18 +107,19 @@ class TreeSnapshot:

__slots__ = ("_root", "_paths")

def __init__(self, root: Path) -> None:
def __init__(self, root: Path, paths: Iterable[Path]) -> None:
"""Initialize.
Args:
root: The root directory of the tree.
paths: All paths in the snapshot.
"""
if not root.is_dir():
raise UnexpectedLitterError(f"'{root}' is not a directory.")
self._root: Path = root
self._paths: frozenset[PathSnapshot] = frozenset(
PathSnapshot(path=path) for path in self._root.rglob("*")
PathSnapshot(path=path) for path in paths
)

@property
Expand All @@ -67,6 +133,44 @@ def paths(self) -> frozenset[PathSnapshot]:
return self._paths


class TreeSnapshotFactory:
"""Factory class for TreeSnapshotFactory."""

__slots__ = ("_ignore_specs",)

def __init__(self, config: LitterConfig) -> None:
"""Initialize.
Args:
config: pytest-litter configuration.
"""
self._ignore_specs: frozenset[IgnoreSpec] = frozenset(config.ignore_specs or [])

def _should_be_ignored(self, path: Path) -> bool:
return path.name == "." or any(
ignore_spec.matches(path) for ignore_spec in self._ignore_specs
)

def create_snapshot(self, root: Path) -> TreeSnapshot:
paths: set[Path] = set()

def traverse(current: Path) -> None:
sub_paths = {
p for p in current.glob("*") if not self._should_be_ignored(path=p)
}
paths.update(sub_paths)
for sub_path in sub_paths:
traverse(sub_path)

traverse(root)

return TreeSnapshot(
root=root,
paths=paths,
)


class SnapshotComparison:
"""A comparison of two TreeSnapshots."""

Expand Down Expand Up @@ -101,59 +205,23 @@ def matches(self) -> bool:
return not self._only_a and not self._only_b


class IgnoreSpec(abc.ABC):
"""Specification about paths to ignore in comparisons."""

__slots__ = ()

@abc.abstractmethod
def matches(self, path: PathSnapshot) -> bool:
... # pragma: no cover


class DirectoryIgnoreSpec(IgnoreSpec):
"""Specification to ignore everything in a given directory."""

__slots__ = ("_directory",)

def __init__(self, directory: Path) -> None:
self._directory = directory

def matches(self, path: PathSnapshot) -> bool:
return self._directory == path.path or self._directory in path.path.parents


class RegexIgnoreSpec(IgnoreSpec):
"""Regex-based specification about paths to ignore in comparisons."""

__slots__ = ("_regex",)

def __init__(self, regex: Union[str, re.Pattern[str]]) -> None:
self._regex: re.Pattern[str] = re.compile(regex)

def matches(self, path: PathSnapshot) -> bool:
return self._regex.fullmatch(str(path)) is not None


class SnapshotComparator:
"""Compare TreeSnapshots with each other."""

__slots__ = ("_ignore_specs",)

def __init__(self, ignore_specs: Optional[Iterable[IgnoreSpec]] = None) -> None:
def __init__(self, config: LitterConfig) -> None:
"""Initialize.
Args:
ignore_specs: Glob patterns to ignore when doing the comparison.
config: pytest-litter configuration.
"""
self._ignore_specs: frozenset[IgnoreSpec] = frozenset(ignore_specs or [])

def _should_be_ignored(self, path: PathSnapshot) -> bool:
return any(ignore_spec.matches(path) for ignore_spec in self._ignore_specs)
self._ignore_specs: frozenset[IgnoreSpec] = frozenset(config.ignore_specs or [])

@staticmethod
def compare(
self, snapshot_a: TreeSnapshot, snapshot_b: TreeSnapshot
snapshot_a: TreeSnapshot, snapshot_b: TreeSnapshot
) -> SnapshotComparison:
"""Compare snapshot_a and snapshot_b to produce a SnapshotComparison."""
if snapshot_a.root != snapshot_b.root:
Expand All @@ -163,9 +231,7 @@ def compare(
common_paths: frozenset[PathSnapshot] = snapshot_a.paths.intersection(
snapshot_b.paths
)
only_in_a: frozenset[PathSnapshot] = snapshot_a.paths - common_paths
only_in_b: frozenset[PathSnapshot] = snapshot_b.paths - common_paths
return SnapshotComparison(
only_a=(path for path in only_in_a if not self._should_be_ignored(path)),
only_b=(path for path in only_in_b if not self._should_be_ignored(path)),
only_a=snapshot_a.paths - common_paths,
only_b=snapshot_b.paths - common_paths,
)
7 changes: 0 additions & 7 deletions tests/pytester/pytest.ini

This file was deleted.

9 changes: 9 additions & 0 deletions tests/suite/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[pytest]
required_plugins = "pytest-litter"
python_files = "suite_*.py"
python_functions = "tc_*"
testpaths = "."
addopts =
"-p pytest-litter"
"-p no:pytest-random-order"
"-p no:pytest-cov"
14 changes: 7 additions & 7 deletions tests/pytester/pytester_tests.py → tests/suite/suite_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@
import pytest


def pytester_should_pass() -> None:
def tc_should_pass() -> None:
"""Do not create any litter."""


@pytest.mark.xfail
def pytester_should_fail() -> None:
def tc_should_fail() -> None:
"""Create a file 'litter' in cwd which is not cleaned up."""
(Path.cwd() / "litter").touch()


def pytester_should_also_pass(tmp_path: Path) -> None:
def tc_should_also_pass(tmp_path: Path) -> None:
"""Create litter in tmp_path only."""
(tmp_path / "litter").touch()
(tmp_path / "more_litter").touch()


@pytest.mark.xfail
def pytester_should_also_fail() -> None:
"""Create a file 'more_litter' in cwd which is not cleaned up."""
(Path.cwd() / "more_litter").touch()
def tc_should_also_fail() -> None:
"""Remove a file 'litter' in cwd."""
(Path.cwd() / "litter").unlink()
2 changes: 1 addition & 1 deletion tests/system_test/system_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
echo "Preparing system test..."
WORK_DIR=$(mktemp --directory)
ROOT_DIR="$(git rev-parse --show-toplevel)"
TEST_DIR="${ROOT_DIR}/tests/pytester"
TEST_DIR="${ROOT_DIR}/tests/suite"
cp "${TEST_DIR}"/* "${WORK_DIR}"
pushd "${WORK_DIR}" &> /dev/null || exit 1
VENV="${WORK_DIR}/venv"
Expand Down
Loading

0 comments on commit 5ec27d3

Please sign in to comment.