diff --git a/src/syrupy/__init__.py b/src/syrupy/__init__.py index 004e5d10..7314d3b8 100644 --- a/src/syrupy/__init__.py +++ b/src/syrupy/__init__.py @@ -1,5 +1,4 @@ import argparse -import glob import sys from gettext import gettext from typing import ( @@ -15,7 +14,7 @@ from .constants import DISABLE_COLOR_ENV_VAR from .exceptions import FailedToLoadModuleMember from .extensions import DEFAULT_EXTENSION -from .location import TestLocation +from .location import PyTestLocation from .session import SnapshotSession from .terminal import ( received_style, @@ -101,18 +100,6 @@ def snapshot_name(name: str) -> str: return None -def __is_testpath(arg: str) -> bool: - return not arg.startswith("-") and bool(glob.glob(arg.split("::")[0])) - - -def __is_testnode(arg: str) -> bool: - return __is_testpath(arg) and "::" in arg - - -def __is_testmodule(arg: str) -> bool: - return arg == "--pyargs" - - def pytest_sessionstart(session: Any) -> None: """ Initialize snapshot session before tests are collected and ran. @@ -123,13 +110,7 @@ def pytest_sessionstart(session: Any) -> None: warn_unused_snapshots=config.option.warn_unused_snapshots, update_snapshots=config.option.update_snapshots, base_dir=config.rootdir, - is_providing_paths=any( - __is_testpath(arg) or __is_testmodule(arg) - for arg in config.invocation_params.args - ), - is_providing_nodes=any( - __is_testnode(arg) for arg in config.invocation_params.args - ), + invocation_args=config.invocation_params.args, ) config._syrupy.start() @@ -180,6 +161,6 @@ def snapshot(request: Any) -> "SnapshotAssertion": return SnapshotAssertion( update_snapshots=request.config.option.update_snapshots, extension_class=request.config.option.default_extension, - test_location=TestLocation(request.node), + test_location=PyTestLocation(request.node), session=request.session.config._syrupy, ) diff --git a/src/syrupy/assertion.py b/src/syrupy/assertion.py index 62843e88..db2bddb2 100644 --- a/src/syrupy/assertion.py +++ b/src/syrupy/assertion.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: from .extensions.base import AbstractSyrupyExtension - from .location import TestLocation + from .location import PyTestLocation from .session import SnapshotSession from .types import ( PropertyFilter, @@ -47,7 +47,7 @@ class SnapshotAssertion: name: str = attr.ib(default="snapshot") _session: "SnapshotSession" = attr.ib(kw_only=True) _extension_class: Type["AbstractSyrupyExtension"] = attr.ib(kw_only=True) - _test_location: "TestLocation" = attr.ib(kw_only=True) + _test_location: "PyTestLocation" = attr.ib(kw_only=True) _update_snapshots: bool = attr.ib(kw_only=True) _exclude: Optional["PropertyFilter"] = attr.ib( init=False, default=None, kw_only=True diff --git a/src/syrupy/constants.py b/src/syrupy/constants.py index 5cbe9da4..0cb5fd03 100644 --- a/src/syrupy/constants.py +++ b/src/syrupy/constants.py @@ -10,3 +10,5 @@ DISABLE_COLOR_ENV_VAR = "ANSI_COLORS_DISABLED" DISABLE_COLOR_ENV_VARS = {DISABLE_COLOR_ENV_VAR, "NO_COLOR"} + +PYTEST_NODE_SEP = "::" diff --git a/src/syrupy/extensions/base.py b/src/syrupy/extensions/base.py index 4c4eaacb..6a6a4824 100644 --- a/src/syrupy/extensions/base.py +++ b/src/syrupy/extensions/base.py @@ -42,7 +42,7 @@ from syrupy.utils import walk_snapshot_dir if TYPE_CHECKING: - from syrupy.location import TestLocation + from syrupy.location import PyTestLocation from syrupy.types import ( PropertyFilter, PropertyMatcher, @@ -70,7 +70,7 @@ def serialize( class SnapshotFossilizer(ABC): @property @abstractmethod - def test_location(self) -> "TestLocation": + def test_location(self) -> "PyTestLocation": raise NotImplementedError def get_snapshot_name(self, *, index: int = 0) -> str: @@ -81,7 +81,8 @@ def get_snapshot_name(self, *, index: int = 0) -> str: def get_location(self, *, index: int) -> str: """Returns full location where snapshot data is stored.""" basename = self._get_file_basename(index=index) - return str(Path(self._dirname).joinpath(f"{basename}.{self._file_extension}")) + fileext = f".{self._file_extension}" if self._file_extension else "" + return str(Path(self._dirname).joinpath(f"{basename}{fileext}")) def is_snapshot_location(self, *, location: str) -> bool: """Checks if supplied location is valid for this snapshot extension""" @@ -371,9 +372,9 @@ def __strip_ends(self, line: str) -> str: class AbstractSyrupyExtension(SnapshotSerializer, SnapshotFossilizer, SnapshotReporter): - def __init__(self, test_location: "TestLocation"): + def __init__(self, test_location: "PyTestLocation"): self._test_location = test_location @property - def test_location(self) -> "TestLocation": + def test_location(self) -> "PyTestLocation": return self._test_location diff --git a/src/syrupy/location.py b/src/syrupy/location.py index 4b2d9611..4102d841 100644 --- a/src/syrupy/location.py +++ b/src/syrupy/location.py @@ -5,8 +5,10 @@ Optional, ) +from syrupy.constants import PYTEST_NODE_SEP -class TestLocation: + +class PyTestLocation: def __init__(self, node: Any): self._node = node self.filepath = self._node.fspath @@ -17,8 +19,11 @@ def __init__(self, node: Any): @property def classname(self) -> Optional[str]: - _, __, qualname = self._node.location - return ".".join(list(self.__valid_ids(qualname))[:-1]) or None + """ + Pytest node names contain file path and module members delimited by `::` + Example tests/grouping/test_file.py::TestClass::TestSubClass::test_method + """ + return ".".join(self._node.nodeid.split(PYTEST_NODE_SEP)[1:-1]) or None @property def filename(self) -> str: @@ -31,6 +36,10 @@ def snapshot_name(self) -> str: return str(self.testname) def __valid_id(self, name: str) -> str: + """ + Take characters from the name while the result would be a valid python + identified. Example: "test_2[A]" returns "test_2" while "1_a" would return "" + """ valid_id = "" for char in name: new_valid_id = f"{valid_id}{char}" @@ -40,6 +49,10 @@ def __valid_id(self, name: str) -> str: return valid_id def __valid_ids(self, name: str) -> Iterator[str]: + """ + Break a name path into valid name parts stopping at the first non valid name. + Example "TestClass.test_method_[1]" would yield ("TestClass", "test_method_") + """ for n in name.split("."): valid_id = self.__valid_id(n) if valid_id: diff --git a/src/syrupy/report.py b/src/syrupy/report.py index ee203a47..1afed79d 100644 --- a/src/syrupy/report.py +++ b/src/syrupy/report.py @@ -1,3 +1,4 @@ +import importlib from gettext import ( gettext, ngettext, @@ -5,21 +6,26 @@ from pathlib import Path from typing import ( TYPE_CHECKING, + Callable, Dict, + FrozenSet, Iterator, List, + Optional, + Set, + Tuple, ) import attr -import pytest +from .constants import PYTEST_NODE_SEP from .data import ( Snapshot, SnapshotFossil, SnapshotFossils, SnapshotUnknownFossil, ) -from .location import TestLocation +from .location import PyTestLocation from .terminal import ( bold, error_style, @@ -29,17 +35,23 @@ ) if TYPE_CHECKING: + import pytest + from .assertion import SnapshotAssertion @attr.s class SnapshotReport: + """ + This class is responsible for determining the test summary and post execution + results. It will provide the lines of the report to be printed as well as the + information used for removal of unused or orphaned snapshots and fossils. + """ + base_dir: str = attr.ib() all_items: Dict["pytest.Item", bool] = attr.ib() ran_items: Dict["pytest.Item", bool] = attr.ib() update_snapshots: bool = attr.ib() - is_providing_paths: bool = attr.ib() - is_providing_nodes: bool = attr.ib() warn_unused_snapshots: bool = attr.ib() assertions: List["SnapshotAssertion"] = attr.ib() discovered: "SnapshotFossils" = attr.ib(factory=SnapshotFossils) @@ -48,8 +60,12 @@ class SnapshotReport: matched: "SnapshotFossils" = attr.ib(factory=SnapshotFossils) updated: "SnapshotFossils" = attr.ib(factory=SnapshotFossils) used: "SnapshotFossils" = attr.ib(factory=SnapshotFossils) + _invocation_args: Tuple[str, ...] = attr.ib(factory=tuple) + _provided_test_paths: Dict[str, List[str]] = attr.ib(factory=dict) + _keyword_expressions: Set["Expression"] = attr.ib(factory=set) def __attrs_post_init__(self) -> None: + self.__parse_invocation_args() for assertion in self.assertions: self.discovered.merge(assertion.extension.discover_snapshots()) for result in assertion.executions.values(): @@ -67,6 +83,56 @@ def __attrs_post_init__(self) -> None: else: self.failed.update(snapshot_fossil) + def __parse_invocation_args(self) -> None: + """ + Parse the invocation arguments to extract some information for test selection + This compiles and saves values from `-k`, `--pyargs` and test dir path + + https://docs.pytest.org/en/stable/reference.html#command-line-flags + https://docs.pytest.org/en/stable/reference.html#config + + Summary + -k: is evaluated and used to match against snapshot names when present + -m: is ignored for now as markers are not matched to snapshot names + --pyargs: arguments are imported to get their file locations + [args]: a path provided e.g. tests/test_file.py::TestClass::test_method + would result in `"tests/test_file.py"` being stored as the location in a + dictionary with `["TestClass", "test_method"]` being the test node path + """ + arg_groups: List[Tuple[Optional[str], str]] = [] + path_as_package = False + maybe_opt_arg = None + for arg in self._invocation_args: + if arg.strip() == "--pyargs": + path_as_package = True + elif arg.startswith("-"): + if "=" in arg: + arg0, arg1 = arg.split("=") + arg_groups.append((arg0.strip(), arg1.strip())) + elif maybe_opt_arg is None: + maybe_opt_arg = arg + continue # do not reset maybe_opt_arg + else: + arg_groups.append((maybe_opt_arg, arg.strip())) + + maybe_opt_arg = None + + for maybe_opt_arg, arg_value in arg_groups: + if maybe_opt_arg == "-k": # or maybe_opt_arg == "-m": + self._keyword_expressions.add(Expression.compose(arg_value)) + elif maybe_opt_arg is None: + parts = arg_value.split(PYTEST_NODE_SEP) + package_or_filepath = parts[0].strip() + filepath = Path( + importlib.import_module(package_or_filepath).__file__ + if path_as_package + else package_or_filepath + ) + filepath_abs = str( + filepath if filepath.is_absolute() else filepath.absolute() + ) + self._provided_test_paths[filepath_abs] = parts[1:] + @property def num_created(self) -> int: return self._count_snapshots(self.created) @@ -89,32 +155,42 @@ def num_unused(self) -> int: @property def ran_all_collected_tests(self) -> bool: - return self.all_items == self.ran_items and not self.is_providing_nodes + return self.all_items == self.ran_items @property def unused(self) -> "SnapshotFossils": + """ + Iterate over each snapshot that was discovered but never used and compute + if the snapshot was unused because the test attached to it was never run, + or if the snapshot is obsolete and therefore is a candidate for removal. + + Summary, if a snapshot was supposed to be run based on the invocation args + and it was not, then it should be marked as unused otherwise ignored. + """ unused_fossils = SnapshotFossils() for unused_snapshot_fossil in self._diff_snapshot_fossils( self.discovered, self.used ): snapshot_location = unused_snapshot_fossil.location - if self.is_providing_paths and not any( - TestLocation(node).matches_snapshot_location(snapshot_location) - for node in self.ran_items + if self._provided_test_paths and not self._selected_items_match_location( + snapshot_location ): + # Paths/Packages were provided to pytest and the snapshot location + # does not match therefore ignore this unused snapshot fossil file continue - if self.ran_all_collected_tests: + provided_nodes = self._get_matching_path_nodes(snapshot_location) + if self.ran_all_collected_tests and not any(provided_nodes): + # All collected tests were run and files were not filtered by ::node + # therefore the snapshot fossil file at this location can be deleted unused_snapshots = {*unused_snapshot_fossil} mark_for_removal = snapshot_location not in self.used else: unused_snapshots = { snapshot for snapshot in unused_snapshot_fossil - if any( - TestLocation(node).matches_snapshot_name(snapshot.name) - for node in self.ran_items - ) + if self._selected_items_match_name(snapshot.name) + or self._provided_nodes_match_name(snapshot.name, provided_nodes) } mark_for_removal = False @@ -131,6 +207,14 @@ def unused(self) -> "SnapshotFossils": @property def lines(self) -> Iterator[str]: + """ + These are the lines printed at the end of a test run. Example: + ``` + 2 snaphots passed. 5 snapshots generated. 1 unused snapshot deleted. + + Re-run pytest with --snapshot-update to delete unused snapshots. + ``` + """ summary_lines: List[str] = [] if self.num_failed: summary_lines.append( @@ -203,6 +287,13 @@ def lines(self) -> Iterator[str]: def _diff_snapshot_fossils( self, snapshot_fossils1: "SnapshotFossils", snapshot_fossils2: "SnapshotFossils" ) -> "SnapshotFossils": + """ + Find the difference between two collections of snapshot fossils. While + preserving the location site to all fossils in the first collections. That is + a collection with fossil sites {A{1,2}, B{3,4}, C{5,6}} with snapshot fossils + when diffed with another collection with snapshots {A{1,2}, B{3,4}, D{7,8}} + will result in a collection with the contents {A{}, B{}, C{5,6}}. + """ diffed_snapshot_fossils: "SnapshotFossils" = SnapshotFossils() for snapshot_fossil1 in snapshot_fossils1: snapshot_fossil2 = snapshot_fossils2.get( @@ -216,4 +307,98 @@ def _diff_snapshot_fossils( return diffed_snapshot_fossils def _count_snapshots(self, snapshot_fossils: "SnapshotFossils") -> int: + """ + Count all the snapshots at all the locations in the snapshot fossil collection + """ return sum(len(snapshot_fossil) for snapshot_fossil in snapshot_fossils) + + def _is_matching_path(self, snapshot_location: str, provided_path: str) -> bool: + """ + Check if a snapshot location matches the path provided by checking that the + provided path folder is in a parent position relative to the snapshot location + """ + path = Path(provided_path) + return str(path if path.is_dir() else path.parent) in snapshot_location + + def _get_matching_path_nodes(self, snapshot_location: str) -> List[List[str]]: + """ + For the snapshot location provided, get the nodes of the test paths provided to + pytest on invocation. If there were no paths provided then this list should be + empty. If there are paths without nodes provided then this is a list of empties + """ + return [ + self._provided_test_paths[path] + for path in self._provided_test_paths + if self._is_matching_path(snapshot_location, path) + ] + + def _provided_nodes_match_name( + self, snapshot_name: str, provided_nodes: List[List[str]] + ) -> bool: + """ + Check that a snapshot name matches the node paths provided + """ + return any(snapshot_name in ".".join(node_path) for node_path in provided_nodes) + + def _provided_keywords_match_name(self, snapshot_name: str) -> bool: + """ + Check that a snapshot name would have been included by the keyword + expression parsed from the invocation arguments + """ + names = snapshot_name.split(".") + return any( + expr.evaluate(lambda subname: any(subname in name for name in names)) + for expr in self._keyword_expressions + ) + + def _ran_items_match_name(self, snapshot_name: str) -> bool: + """ + Check that a snapshot name would match a test node using the Pytest location + """ + return any( + PyTestLocation(item).matches_snapshot_name(snapshot_name) + for item in self.ran_items + ) + + def _selected_items_match_name(self, snapshot_name: str) -> bool: + """ + Check that a snapshot name should be treated as selected by the current session + This being true means that if the snapshot was not used then it will be deleted + """ + if self._keyword_expressions: + return self._provided_keywords_match_name(snapshot_name) + return self._ran_items_match_name(snapshot_name) + + def _selected_items_match_location(self, snapshot_location: str) -> bool: + """ + Check that a snapshot fossil location should is selected by the current session + This being true means that if no snapshot in the fossil was used then it should + be discarded as obsolete + """ + return any( + PyTestLocation(item).matches_snapshot_location(snapshot_location) + for item in self.ran_items + ) + + +@attr.s(frozen=True) +class Expression: + """ + Dumbed down version of _pytest.mark.expression.Expression not available in < 6.0 + https://github.com/pytest-dev/pytest/blob/6.0.x/src/_pytest/mark/expression.py + Added for pared down support on older pytest version and because the expression + module is not public. This only supports inclusion based on simple string matching. + """ + + code: FrozenSet[str] = attr.ib(factory=frozenset) + + def evaluate(self, matcher: Callable[[str], bool]) -> bool: + return any(map(matcher, self.code)) + + @staticmethod + def compose(value: str) -> "Expression": + delim = " " + replace_str = {" or ", " and ", " not ", "(", ")"} + for r in replace_str: + value = value.replace(f" {r} ", delim) + return Expression(code=frozenset(value.split(delim))) diff --git a/src/syrupy/session.py b/src/syrupy/session.py index 92f839ff..1c18f625 100644 --- a/src/syrupy/session.py +++ b/src/syrupy/session.py @@ -5,6 +5,7 @@ Iterable, List, Optional, + Tuple, ) import attr @@ -24,8 +25,7 @@ class SnapshotSession: base_dir: str = attr.ib() update_snapshots: bool = attr.ib() warn_unused_snapshots: bool = attr.ib() - is_providing_paths: bool = attr.ib() - is_providing_nodes: bool = attr.ib() + _invocation_args: Tuple[str, ...] = attr.ib(factory=tuple) report: Optional["SnapshotReport"] = attr.ib(default=None) _all_items: Dict["pytest.Item", bool] = attr.ib(factory=dict) _ran_items: Dict["pytest.Item", bool] = attr.ib(factory=dict) @@ -48,8 +48,7 @@ def finish(self) -> int: assertions=self._assertions, update_snapshots=self.update_snapshots, warn_unused_snapshots=self.warn_unused_snapshots, - is_providing_paths=self.is_providing_paths, - is_providing_nodes=self.is_providing_nodes, + invocation_args=self._invocation_args, ) if self.report.num_unused: if self.update_snapshots: @@ -75,6 +74,10 @@ def remove_unused_snapshots( unused_snapshot_fossils: "SnapshotFossils", used_snapshot_fossils: "SnapshotFossils", ) -> None: + """ + Remove all unused snapshots using the registed extension for the fossil file + If there is not registered extension and the location is unused delete the file + """ for unused_snapshot_fossil in unused_snapshot_fossils: snapshot_location = unused_snapshot_fossil.location diff --git a/tests/__snapshots__/test_matchers.ambr b/tests/__snapshots__/test_matchers.ambr deleted file mode 100644 index 0ef5a4f5..00000000 --- a/tests/__snapshots__/test_matchers.ambr +++ /dev/null @@ -1,19 +0,0 @@ -# name: test_matches_expected_type - { - 'date_created': , - 'nested': { - 'id': , - }, - 'some_uuid': , - } ---- -# name: test_raises_unexpected_type - { - 'date_created': , - 'date_updated': datetime.date(2020, 6, 1), - 'nested': { - 'id': , - }, - 'some_uuid': , - } ---- diff --git a/tests/test_integration_pytest_extension.py b/tests/integration/test_pytest_extension.py similarity index 91% rename from tests/test_integration_pytest_extension.py rename to tests/integration/test_pytest_extension.py index dd8c7453..1915a7d9 100644 --- a/tests/test_integration_pytest_extension.py +++ b/tests/integration/test_pytest_extension.py @@ -26,6 +26,5 @@ def test_example(snapshot): testdir.makepyfile(conftest=conftest) testdir.makepyfile(test_file=testcase) result = testdir.runpytest("test_file.py", "-v", "--snapshot-update") - result_stdout = result.stdout.str() + result.stdout.re_match_lines((r".*test_file.py::CUSTOM.*")) assert result.ret == 0 - assert "test_file.py::CUSTOM" in result_stdout diff --git a/tests/integration/test_snapshot_option_defaults.py b/tests/integration/test_snapshot_option_defaults.py new file mode 100644 index 00000000..357bbf38 --- /dev/null +++ b/tests/integration/test_snapshot_option_defaults.py @@ -0,0 +1,25 @@ +import pytest + + +@pytest.fixture +def testcases(): + return { + "inject": ( + """ + def test_injection(snapshot): + assert snapshot is not None + """ + ), + } + + +@pytest.fixture +def run_testcases(testdir, testcases): + pyfile_content = "\n\n".join(testcases.values()) + testdir.makepyfile(test_file=pyfile_content) + return testdir.runpytest("-v") + + +def test_injected_fixture(run_testcases): + run_testcases.stdout.fnmatch_lines(["*::test_injection PASSED*"]) + assert run_testcases.ret == 0 diff --git a/tests/integration/test_snapshot_option_extension.py b/tests/integration/test_snapshot_option_extension.py new file mode 100644 index 00000000..546da3ff --- /dev/null +++ b/tests/integration/test_snapshot_option_extension.py @@ -0,0 +1,49 @@ +from pathlib import Path + +import pytest + + +@pytest.fixture +def testfile(testdir): + testdir.makepyfile( + test_file=( + """ + def test_default(snapshot): + assert b"default extension serializer" == snapshot + """ + ), + ) + return testdir + + +def test_snapshot_default_extension_option_success(testfile): + result = testfile.runpytest( + "-v", + "--snapshot-update", + "--snapshot-default-extension", + "syrupy.extensions.single_file.SingleFileSnapshotExtension", + ) + result.stdout.re_match_lines((r"1 snapshot generated\.")) + assert Path( + testfile.tmpdir, "__snapshots__", "test_file", "test_default.raw" + ).exists() + assert not result.ret + + +def test_snapshot_default_extension_option_failure(testfile): + result = testfile.runpytest( + "-v", + "--snapshot-update", + "--snapshot-default-extension", + "syrupy.extensions.amber.DoesNotExistExtension", + ) + result.stderr.re_match_lines( + ( + r".*error: argument --snapshot-default-extension" + r": Member 'DoesNotExistExtension' not found.*", + ) + ) + assert not Path( + testfile.tmpdir, "__snapshots__", "test_file", "test_default.raw" + ).exists() + assert result.ret diff --git a/tests/integration/test_snapshot_option_update.py b/tests/integration/test_snapshot_option_update.py new file mode 100644 index 00000000..2604ae93 --- /dev/null +++ b/tests/integration/test_snapshot_option_update.py @@ -0,0 +1,431 @@ +import sys +from pathlib import Path + +import pytest + + +@pytest.fixture +def testcases_initial(): + return { + "test_used": ( + """ + import pytest + + @pytest.mark.parametrize("actual", [1, 2, 3]) + def test_used(snapshot, actual): + assert snapshot == actual + + def test_used1(snapshot): + assert snapshot == 'unused' + assert 'unused' == snapshot + """ + ), + "test_updated_1": ( + """ + def test_updated_1(snapshot): + assert snapshot == ['this', 'will', 'be', 'updated'] + """ + ), + "test_updated_2": ( + """ + def test_updated_2(snapshot): + assert ['this', 'will', 'be', 'updated'] == snapshot + """ + ), + "test_updated_3": ( + """ + def test_updated_3(snapshot): + assert snapshot == ['this', 'will', 'be', 'updated'] + """ + ), + "test_updated_4": ( + """ + def test_updated_4(snapshot): + assert snapshot == "single line change" + """ + ), + "test_updated_5": ( + """ + def test_updated_5(snapshot): + assert snapshot == ''' + multiple line changes + with some lines staying the same + intermittent changes that have to be ignore by the differ output + because when there are a lot of changes you only want to see changes + you do not want to see this line + or this line + this line should not show up because new lines are normalised\\r\\n + \x1b[38;5;1mthis line should show up because it changes color\x1b[0m + ''' + """ + ), + } + + +@pytest.fixture +def testcases_updated(testcases_initial): + updates = { + "test_updated_1": ( + """ + def test_updated_1(snapshot): + assert snapshot == ['this', 'will', 'not', 'match'] + """ + ), + "test_updated_2": ( + """ + def test_updated_2(snapshot): + assert ['this', 'will', 'fail'] == snapshot + """ + ), + "test_updated_3": ( + """ + def test_updated_3(snapshot): + assert snapshot == ['this', 'will', 'be', 'too', 'much'] + """ + ), + "test_updated_4": ( + """ + def test_updated_4(snapshot): + assert snapshot == "sing line changeling" + """ + ), + "test_updated_5": ( + """ + def test_updated_5(snapshot): + assert snapshot == ''' + multiple line changes + with some lines not staying the same + intermittent changes so unchanged lines have to be ignored by the differ + cause when there are a lot of changes you only want to see what changed + you do not want to see this line + or this line + this line should not show up because new lines are normalised\\n + \x1b[38;5;3mthis line should show up because it changes color\x1b[0m + and this line does not exist in the first one + ''' + """ + ), + } + return {**testcases_initial, **updates} + + +@pytest.fixture +def run_testcases(testdir, testcases_initial): + sys.path.append(str(testdir.tmpdir)) + testdir.makepyfile(**testcases_initial) + result = testdir.runpytest("-v", "--snapshot-update") + result.stdout.re_match_lines((r"10 snapshots generated.")) + assert "Can not relate snapshot name" not in result.stdout.str() + + return result, testdir, testcases_initial + + +def test_update_failure_shows_snapshot_diff(run_testcases, testcases_updated): + testdir = run_testcases[1] + testdir.makepyfile(**testcases_updated) + result = testdir.runpytest("-vv") + result.stdout.re_match_lines( + ( + r".*assert snapshot == \['this', 'will', 'not', 'match'\]", + r".*AssertionError: assert \[- snapshot\] == \[\+ received\]", + r".* \[", + r".* ...", + r".* 'will',", + r".* - 'be',", + r".* - 'updated',", + r".* \+ 'not',", + r".* \+ 'match',", + r".* \]", + r".*assert \['this', 'will', 'fail'\] == snapshot", + r".*AssertionError: assert \[\+ received\] == \[- snapshot\]", + r".* \[", + r".* ...", + r".* 'will',", + r".* - 'be',", + r".* \+ 'fail',", + r".* - 'updated',", + r".* \]", + r".*assert snapshot == \['this', 'will', 'be', 'too', 'much'\]", + r".*AssertionError: assert \[- snapshot\] == \[\+ received\]", + r".* \[", + r".* ...", + r".* 'be',", + r".* - 'updated',", + r".* \+ 'too',", + r".* \+ 'much',", + r".* \]", + r".*assert snapshot == \"sing line changeling\"", + r".*AssertionError: assert \[- snapshot\] == \[\+ received\]", + r".* - 'single line change'", + r".* \+ 'sing line changeling'", + r".*AssertionError: assert \[- snapshot\] == \[\+ received\]", + r".* '", + r".* ...", + r".* multiple line changes", + r".* - with some lines staying the same", + r".* \+ with some lines not staying the same", + r".* - intermittent changes that have to be ignore by the differ out", + r".* \+ intermittent changes so unchanged lines have to be ignored b", + r".* - because when there are a lot of changes you only want to see ", + r".* \+ cause when there are a lot of changes you only want to see w", + r".* you do not want to see this line", + r".* ...", + r".* ", + r".* - \[38;5;1mthis line should show up because it changes color", + r".* \+ \[38;5;3mthis line should show up because it changes color", + r".* \+ and this line does not exist in the first one", + r".* ", + r".* '", + ) + ) + assert result.ret == 1 + + +def test_update_success_shows_snapshot_report(run_testcases, testcases_updated): + testdir = run_testcases[1] + testdir.makepyfile(**testcases_updated) + result = testdir.runpytest("-v", "--snapshot-update") + result.stdout.re_match_lines((r"5 snapshots passed\. 5 snapshots updated\.")) + assert result.ret == 0 + + +def test_update_targets_only_selected_parametrized_tests_for_update_dash_m( + run_testcases, +): + updated_tests = { + "test_used": ( + """ + import pytest + + @pytest.mark.parametrize("actual", [1, "2"]) + def test_used(snapshot, actual): + assert snapshot == actual + """ + ), + } + testdir = run_testcases[1] + testdir.makepyfile(**updated_tests) + result = testdir.runpytest("-v", "--snapshot-update", "-m", "parametrize") + result.stdout.re_match_lines( + ( + r"1 snapshot passed\. 1 snapshot updated\. 1 unused snapshot deleted\.", + r"Deleted test_used\[3\] \(__snapshots__[\\/]test_used.ambr\)", + ) + ) + snapshot_path = [testdir.tmpdir, "__snapshots__"] + assert Path(*snapshot_path, "test_used.ambr").exists() + assert Path(*snapshot_path, "test_updated_1.ambr").exists() + + +def test_update_targets_only_selected_parametrized_tests_for_update_dash_k( + run_testcases, +): + updated_tests = { + "test_used": ( + """ + import pytest + + @pytest.mark.parametrize("actual", [1, "2", 3]) + def test_used(snapshot, actual): + assert snapshot == actual + """ + ), + } + testdir = run_testcases[1] + testdir.makepyfile(**updated_tests) + result = testdir.runpytest("-v", "--snapshot-update", "-k", "test_used[2]") + result.stdout.re_match_lines((r"1 snapshot updated\.")) + assert "Deleted" not in result.stdout.str() + snapshot_path = [testdir.tmpdir, "__snapshots__"] + assert Path(*snapshot_path, "test_used.ambr").exists() + assert Path(*snapshot_path, "test_updated_1.ambr").exists() + + +def test_update_targets_only_selected_parametrized_tests_for_removal_dash_k( + run_testcases, +): + updated_tests = { + "test_used": ( + """ + import pytest + + @pytest.mark.parametrize("actual", [1, 2]) + def test_used(snapshot, actual): + assert snapshot == actual + """ + ), + } + testdir = run_testcases[1] + testdir.makepyfile(**updated_tests) + result = testdir.runpytest("-v", "--snapshot-update", "-k", "test_used[") + result.stdout.re_match_lines( + ( + r"2 snapshots passed\. 1 unused snapshot deleted\.", + r"Deleted test_used\[3\] \(__snapshots__[\\/]test_used\.ambr\)", + ) + ) + snapshot_path = [testdir.tmpdir, "__snapshots__"] + assert Path(*snapshot_path, "test_used.ambr").exists() + assert Path(*snapshot_path, "test_updated_1.ambr").exists() + + +def test_update_targets_only_selected_class_tests_dash_k(testdir): + test_content = """ + import pytest + + class TestClass: + def test_case_1(self, snapshot): + assert snapshot == 1 + + def test_case_2(self, snapshot): + assert snapshot == 2 + """ + + testdir.makepyfile(test_content=test_content) + testdir.runpytest("-v", "--snapshot-update") + assert Path(testdir.tmpdir, "__snapshots__", "test_content.ambr").exists() + + result = testdir.runpytest("test_content.py", "-v", "-k test_case_2") + result.stdout.re_match_lines((r"1 snapshot passed\.")) + assert "snaphot unused" not in result.stdout.str() + + +def test_update_targets_only_selected_module_tests_dash_k(testdir): + test_content = """ + import pytest + + def test_case_1(snapshot): + assert snapshot == 1 + + def test_case_2(snapshot): + assert snapshot == 2 + """ + + testdir.makepyfile(test_content=test_content) + testdir.runpytest("-v", "--snapshot-update") + assert Path(testdir.tmpdir, "__snapshots__", "test_content.ambr").exists() + + result = testdir.runpytest("test_content.py", "-v", "-k test_case_2") + result.stdout.re_match_lines((r"1 snapshot passed\.")) + assert "snaphot unused" not in result.stdout.str() + + +def test_update_targets_only_selected_module_tests_nodes(run_testcases): + testdir = run_testcases[1] + snapfile_empty = Path("__snapshots__", "empty_snapfile.ambr") + testdir.makefile(".ambr", **{str(snapfile_empty): ""}) + testfile = Path(testdir.tmpdir, "test_used.py") + result = testdir.runpytest(f"{testfile}::test_used", "-v", "--snapshot-update") + result.stdout.re_match_lines((r"3 snapshots passed\.")) + assert "unused" not in result.stdout.str() + assert "updated" not in result.stdout.str() + assert "deleted" not in result.stdout.str() + assert result.ret == 0 + assert snapfile_empty.exists() + + +def test_update_targets_only_selected_module_tests_nodes_pyargs(run_testcases): + testdir = run_testcases[1] + snapfile_empty = Path("__snapshots__", "empty_snapfile.ambr") + testdir.makefile(".ambr", **{str(snapfile_empty): ""}) + result = testdir.runpytest( + "-v", + "--snapshot-update", + "--pyargs", + "test_used::test_used", + ) + result.stdout.re_match_lines((r"3 snapshots passed\.")) + assert "unused" not in result.stdout.str() + assert "updated" not in result.stdout.str() + assert "deleted" not in result.stdout.str() + assert result.ret == 0 + assert snapfile_empty.exists() + + +def test_update_targets_only_selected_module_tests_file_for_update(run_testcases): + testdir = run_testcases[1] + snapfile_empty = Path("__snapshots__", "empty_snapfile.ambr") + testdir.makefile(".ambr", **{str(snapfile_empty): ""}) + testdir.makepyfile( + test_used=( + """ + import pytest + + @pytest.mark.parametrize("actual", [1, 2, 3]) + def test_used(snapshot, actual): + assert snapshot == actual + """ + ) + ) + result = testdir.runpytest("-v", "--snapshot-update", "test_used.py") + result.stdout.re_match_lines( + ( + r"3 snapshots passed\. 2 unused snapshots deleted\.", + r"Deleted test_used1, test_used1\.1 \(__snapshots__[\\/]test_used\.ambr\)", + ) + ) + assert result.ret == 0 + assert snapfile_empty.exists() + assert Path("__snapshots__", "test_used.ambr").exists() + + +def test_update_targets_only_selected_module_tests_file_for_removal(run_testcases): + testdir = run_testcases[1] + testdir.makepyfile( + test_used=( + """ + def test_used(snapshot): + assert True + """ + ), + ) + snapfile_empty = Path("__snapshots__", "empty_snapfile.ambr") + testdir.makefile(".ambr", **{str(snapfile_empty): ""}) + result = testdir.runpytest("-v", "--snapshot-update", "test_used.py") + result.stdout.re_match_lines( + ( + r"5 unused snapshots deleted\.", + r"Deleted test_used1, test_used1\.1, test_used\[1\], test_used\[2\]" + r", test_used\[3\] \(__snapshots__[\\/]test_used\.ambr\)", + ) + ) + assert result.ret == 0 + assert snapfile_empty.exists() + assert not Path("__snapshots__", "test_used.ambr").exists() + + +def test_update_removes_empty_snapshot_fossil_only(run_testcases): + testdir = run_testcases[1] + snapfile_empty = Path("__snapshots__", "empty_snapfile.ambr") + testdir.makefile(".ambr", **{str(snapfile_empty): ""}) + assert snapfile_empty.exists() + result = testdir.runpytest("-v", "--snapshot-update") + result.stdout.re_match_lines( + ( + r"10 snapshots passed\. 1 unused snapshot deleted\.", + r"Deleted empty snapshot fossil \(__snapshots__[\\/]empty_snapfile\.ambr\)", + ) + ) + assert result.ret == 0 + assert not snapfile_empty.exists() + assert Path("__snapshots__", "test_used.ambr").exists() + + +def test_update_removes_hanging_snapshot_fossil_file(run_testcases): + testdir = run_testcases[1] + snapfile_used = Path("__snapshots__", "test_used.ambr") + snapfile_hanging = Path("__snapshots__", "hanging_snapfile.abc") + testdir.makefile(".abc", **{str(snapfile_hanging): ""}) + assert snapfile_hanging.exists() + result = testdir.runpytest("-v", "--snapshot-update") + result.stdout.re_match_lines( + ( + r"10 snapshots passed\. 1 unused snapshot deleted\.", + r"Deleted unknown snapshot fossil " + r"\(__snapshots__[\\/]hanging_snapfile\.abc\)", + ) + ) + assert f"{snapfile_used}" not in result.stdout.str() + assert result.ret == 0 + assert snapfile_used.exists() + assert not snapfile_hanging.exists() diff --git a/tests/integration/test_snapshot_option_warn_unused.py b/tests/integration/test_snapshot_option_warn_unused.py new file mode 100644 index 00000000..f9a36134 --- /dev/null +++ b/tests/integration/test_snapshot_option_warn_unused.py @@ -0,0 +1,70 @@ +import pytest + + +@pytest.fixture +def testcases(): + return { + "used": ( + """ + def test_used(snapshot): + assert snapshot == 'used' + """ + ), + "unused": ( + """ + def test_unused(snapshot): + assert snapshot == 'unused' + """ + ), + } + + +@pytest.fixture +def run_testcases(testdir, testcases): + pyfile_content = "\n\n".join(testcases.values()) + 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_unused_snapshots_failure(run_testcases): + testdir, testcases = run_testcases + testdir.makepyfile(test_file=testcases["used"]) + + result = testdir.runpytest("-v") + result.stdout.re_match_lines( + ( + r"1 snapshot passed\. 1 snapshot unused\.", + r"Re-run pytest with --snapshot-update to delete unused snapshots\.", + ) + ) + assert result.ret == 1 + + +def test_unused_snapshots_warning(run_testcases): + testdir, testcases = run_testcases + testdir.makepyfile(test_file=testcases["used"]) + + result = testdir.runpytest("-v", "--snapshot-warn-unused") + result.stdout.re_match_lines( + ( + r"1 snapshot passed\. 1 snapshot unused\.", + r"Re-run pytest with --snapshot-update to delete unused snapshots\.", + ) + ) + assert result.ret == 0 + + +def test_unused_snapshots_deletion(run_testcases): + testdir, testcases = run_testcases + testdir.makepyfile(test_file=testcases["used"]) + + result = testdir.runpytest("-v", "--snapshot-update") + result.stdout.re_match_lines( + ( + r"1 snapshot passed\. 1 unused snapshot deleted\.", + r"Deleted test_unused \(__snapshots__[\\/]test_file\.ambr\)", + ) + ) + assert result.ret == 0 diff --git a/tests/integration/test_snapshot_use_extension.py b/tests/integration/test_snapshot_use_extension.py new file mode 100644 index 00000000..a111f881 --- /dev/null +++ b/tests/integration/test_snapshot_use_extension.py @@ -0,0 +1,146 @@ +import pytest + + +@pytest.fixture +def testcases_initial(testdir): + testdir.makeconftest( + """ + import pytest + + from syrupy.extensions.amber import AmberSnapshotExtension + from syrupy.extensions.image import ( + PNGImageSnapshotExtension, + SVGImageSnapshotExtension, + ) + from syrupy.extensions.single_file import SingleFileSnapshotExtension + + + class CustomSnapshotExtension(AmberSnapshotExtension): + @property + def _file_extension(self): + return "" + + def serialize(self, data, **kwargs): + return str(data) + + def get_snapshot_name(self, *, index = 0): + testname = self._test_location.testname[::-1] + return f"{testname}.{index}" + + def _get_file_basename(self, *, index = 0): + return self.test_location.filename[::-1] + + @pytest.fixture + def snapshot_custom(snapshot): + return snapshot.use_extension(CustomSnapshotExtension) + + + @pytest.fixture + def snapshot_single(snapshot): + return snapshot.use_extension(SingleFileSnapshotExtension) + + + @pytest.fixture + def snapshot_png(snapshot): + return snapshot.use_extension(PNGImageSnapshotExtension) + + + @pytest.fixture + def snapshot_svg(snapshot): + return snapshot.use_extension(SVGImageSnapshotExtension) + """ + ) + return { + "passed": ( + """ + def test_passed_custom(snapshot_custom): + assert snapshot_custom == 'passed1' + assert snapshot_custom == 'passed2' + + def test_passed_single(snapshot_single): + assert snapshot_single == b'passed1' + assert snapshot_single == b'passed2' + """ + ), + "failed": ( + """ + def test_failed_single(snapshot_single): + assert snapshot_single == 'failed' + + def test_failed_image(snapshot_png): + assert "not a byte string" == snapshot_png + """ + ), + } + + +@pytest.fixture +def testcases_updated(testcases_initial): + updated_testcases = { + "passed": ( + """ + def test_passed_single(snapshot_single): + assert snapshot_single == b'passed' + """ + ) + } + return {**testcases_initial, **updated_testcases} + + +@pytest.fixture +def generate_snapshots(testdir, testcases_initial): + testdir.makepyfile(test_file=testcases_initial["passed"]) + result = testdir.runpytest("-v", "--snapshot-update") + return result, testdir, testcases_initial + + +def test_unsaved_snapshots(testdir, testcases_initial): + testdir.makepyfile(test_file=testcases_initial["passed"]) + result = testdir.runpytest("-v") + result.stdout.re_match_lines( + (r".*Snapshot 'test_passed_single' does not exist!", r".*\+ b'passed1'") + ) + assert result.ret == 1 + + +def test_failed_snapshots(testdir, testcases_initial): + testdir.makepyfile(test_file=testcases_initial["failed"]) + result = testdir.runpytest("-v", "--snapshot-update") + result.stdout.re_match_lines((r"2 snapshots failed\.")) + assert result.ret == 1 + + +def test_generated_snapshots(generate_snapshots): + result = generate_snapshots[0] + result.stdout.re_match_lines((r"4 snapshots generated\.")) + assert "snapshots unused" not in result.stdout.str() + assert result.ret == 0 + + +def test_unmatched_snapshots(generate_snapshots, testcases_updated): + testdir = generate_snapshots[1] + testdir.makepyfile(test_file=testcases_updated["passed"]) + result = testdir.runpytest("-v") + result.stdout.re_match_lines((r"1 snapshot failed\. 2 snapshots unused\.")) + assert result.ret == 1 + + +def test_updated_snapshots(generate_snapshots, testcases_updated): + testdir = generate_snapshots[1] + testdir.makepyfile(test_file=testcases_updated["passed"]) + result = testdir.runpytest("-v", "--snapshot-update") + result.stdout.re_match_lines((r"1 snapshot updated\. 2 unused snapshots deleted\.")) + assert result.ret == 0 + + +def test_warns_on_snapshot_name(generate_snapshots): + result = generate_snapshots[0] + result.stdout.re_match_lines( + ( + r".*Warning:\s+", + r"\s+Can not relate snapshot location", + r"\s+Can not relate snapshot name", + r"4 snapshots generated\.", + ) + ) + assert result.ret == 0 diff --git a/tests/syrupy/__init__.py b/tests/syrupy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/syrupy/extensions/__init__.py b/tests/syrupy/extensions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/__snapshots__/test_extension_base.ambr b/tests/syrupy/extensions/__snapshots__/test_base.ambr similarity index 100% rename from tests/__snapshots__/test_extension_base.ambr rename to tests/syrupy/extensions/__snapshots__/test_base.ambr diff --git a/tests/__snapshots__/test_extension_single_file/TestClass.test_class_method_name.raw b/tests/syrupy/extensions/__snapshots__/test_single_file/TestClass.test_class_method_name.raw similarity index 100% rename from tests/__snapshots__/test_extension_single_file/TestClass.test_class_method_name.raw rename to tests/syrupy/extensions/__snapshots__/test_single_file/TestClass.test_class_method_name.raw diff --git a/tests/__snapshots__/test_extension_single_file/TestClass.test_class_method_parametrizedx.raw b/tests/syrupy/extensions/__snapshots__/test_single_file/TestClass.test_class_method_parametrizedx.raw similarity index 100% rename from tests/__snapshots__/test_extension_single_file/TestClass.test_class_method_parametrizedx.raw rename to tests/syrupy/extensions/__snapshots__/test_single_file/TestClass.test_class_method_parametrizedx.raw diff --git a/tests/__snapshots__/test_extension_single_file/TestClass.test_class_method_parametrizedy.raw b/tests/syrupy/extensions/__snapshots__/test_single_file/TestClass.test_class_method_parametrizedy.raw similarity index 100% rename from tests/__snapshots__/test_extension_single_file/TestClass.test_class_method_parametrizedy.raw rename to tests/syrupy/extensions/__snapshots__/test_single_file/TestClass.test_class_method_parametrizedy.raw diff --git a/tests/__snapshots__/test_extension_single_file/TestClass.test_class_method_parametrizedz.raw b/tests/syrupy/extensions/__snapshots__/test_single_file/TestClass.test_class_method_parametrizedz.raw similarity index 100% rename from tests/__snapshots__/test_extension_single_file/TestClass.test_class_method_parametrizedz.raw rename to tests/syrupy/extensions/__snapshots__/test_single_file/TestClass.test_class_method_parametrizedz.raw diff --git a/tests/__snapshots__/test_filters.ambr b/tests/syrupy/extensions/amber/__snapshots__/test_amber_filters.ambr similarity index 100% rename from tests/__snapshots__/test_filters.ambr rename to tests/syrupy/extensions/amber/__snapshots__/test_amber_filters.ambr diff --git a/tests/syrupy/extensions/amber/__snapshots__/test_amber_matchers.ambr b/tests/syrupy/extensions/amber/__snapshots__/test_amber_matchers.ambr new file mode 100644 index 00000000..a1683cb7 --- /dev/null +++ b/tests/syrupy/extensions/amber/__snapshots__/test_amber_matchers.ambr @@ -0,0 +1,45 @@ +# name: test_matches_expected_type + { + 'date_created': , + 'nested': { + 'id': , + }, + 'some_uuid': , + } +--- +# name: test_matches_non_deterministic_snapshots + { + 'a': UUID(...), + 'b': { + 'b_1': 'This is deterministic', + 'b_2': datetime.datetime(...), + }, + 'c': [ + 'Your wish is my command', + 'Do not replace this one', + ], + } +--- +# name: test_matches_non_deterministic_snapshots.1 + { + 'a': UUID('06335e84-2872-4914-8c5d-3ed07d2a2f16'), + 'b': { + 'b_1': 'This is deterministic', + 'b_2': datetime.datetime(2020, 5, 31, 0, 0), + }, + 'c': [ + 'Replace this one', + 'Do not replace this one', + ], + } +--- +# name: test_raises_unexpected_type + { + 'date_created': , + 'date_updated': datetime.date(2020, 6, 1), + 'nested': { + 'id': , + }, + 'some_uuid': , + } +--- diff --git a/tests/__snapshots__/test_extension_amber.ambr b/tests/syrupy/extensions/amber/__snapshots__/test_amber_serializer.ambr similarity index 90% rename from tests/__snapshots__/test_extension_amber.ambr rename to tests/syrupy/extensions/amber/__snapshots__/test_amber_serializer.ambr index ed4359e9..9dd204f2 100644 --- a/tests/__snapshots__/test_extension_amber.ambr +++ b/tests/syrupy/extensions/amber/__snapshots__/test_amber_serializer.ambr @@ -296,32 +296,6 @@ line 2 ' --- -# name: test_non_deterministic_snapshots - { - 'a': UUID(...), - 'b': { - 'b_1': 'This is deterministic', - 'b_2': datetime.datetime(...), - }, - 'c': [ - 'Your wish is my command', - 'Do not replace this one', - ], - } ---- -# name: test_non_deterministic_snapshots.1 - { - 'a': UUID('06335e84-2872-4914-8c5d-3ed07d2a2f16'), - 'b': { - 'b_1': 'This is deterministic', - 'b_2': datetime.datetime(2020, 5, 31, 0, 0), - }, - 'c': [ - 'Replace this one', - 'Do not replace this one', - ], - } ---- # name: test_numbers 3.5 --- diff --git a/tests/test_filters.py b/tests/syrupy/extensions/amber/test_amber_filters.py similarity index 94% rename from tests/test_filters.py rename to tests/syrupy/extensions/amber/test_amber_filters.py index d813ea95..726d95eb 100644 --- a/tests/test_filters.py +++ b/tests/syrupy/extensions/amber/test_amber_filters.py @@ -8,7 +8,7 @@ ) -def test_filters_path_noop(snapshot): +def test_filters_path_noop(): with pytest.raises(TypeError, match="required positional argument"): paths() @@ -23,7 +23,7 @@ def test_filters_expected_paths(snapshot): assert actual == snapshot(exclude=paths("0", "date", "nested.id", "list.0")) -def test_filters_prop_noop(snapshot): +def test_filters_prop_noop(): with pytest.raises(TypeError, match="required positional argument"): props() diff --git a/tests/test_matchers.py b/tests/syrupy/extensions/amber/test_amber_matchers.py similarity index 57% rename from tests/test_matchers.py rename to tests/syrupy/extensions/amber/test_amber_matchers.py index 8ada2e6a..93fd2780 100644 --- a/tests/test_matchers.py +++ b/tests/syrupy/extensions/amber/test_amber_matchers.py @@ -3,6 +3,7 @@ import pytest +from syrupy.extensions.amber.serializer import Repr from syrupy.matchers import ( PathTypeError, path_type, @@ -44,3 +45,28 @@ def test_raises_unexpected_type(snapshot): assert actual == snapshot(matcher=path_type(**kwargs, strict=False)) with pytest.raises(PathTypeError, match="does not match any of the expected"): assert actual == snapshot(matcher=path_type(**kwargs)) + + +def test_matches_non_deterministic_snapshots(snapshot): + def matcher(data, path): + if isinstance(data, uuid.UUID): + return Repr("UUID(...)") + if isinstance(data, datetime.datetime): + return Repr("datetime.datetime(...)") + if tuple(p for p, _ in path[-2:]) == ("c", 0): + return "Your wish is my command" + return data + + assert { + "a": uuid.uuid4(), + "b": {"b_1": "This is deterministic", "b_2": datetime.datetime.now()}, + "c": ["Replace this one", "Do not replace this one"], + } == snapshot(matcher=matcher) + assert { + "a": uuid.UUID("06335e84-2872-4914-8c5d-3ed07d2a2f16"), + "b": { + "b_1": "This is deterministic", + "b_2": datetime.datetime(year=2020, month=5, day=31), + }, + "c": ["Replace this one", "Do not replace this one"], + } == snapshot diff --git a/tests/test_extension_amber.py b/tests/syrupy/extensions/amber/test_amber_serializer.py similarity index 83% rename from tests/test_extension_amber.py rename to tests/syrupy/extensions/amber/test_amber_serializer.py index 02a3c008..df464c74 100644 --- a/tests/test_extension_amber.py +++ b/tests/syrupy/extensions/amber/test_amber_serializer.py @@ -1,11 +1,7 @@ -import uuid from collections import namedtuple -from datetime import datetime import pytest -from syrupy.extensions.amber.serializer import Repr - def test_non_snapshots(snapshot): with pytest.raises(AssertionError): @@ -203,28 +199,3 @@ def test_parameter_with_dot(parameter_with_dot, snapshot): def test_doubly_parametrized(parameter_1, parameter_2, snapshot): assert parameter_1 == snapshot assert parameter_2 == snapshot - - -def test_non_deterministic_snapshots(snapshot): - def matcher(data, path): - if isinstance(data, uuid.UUID): - return Repr("UUID(...)") - if isinstance(data, datetime): - return Repr("datetime.datetime(...)") - if tuple(p for p, _ in path[-2:]) == ("c", 0): - return "Your wish is my command" - return data - - assert { - "a": uuid.uuid4(), - "b": {"b_1": "This is deterministic", "b_2": datetime.now()}, - "c": ["Replace this one", "Do not replace this one"], - } == snapshot(matcher=matcher) - assert { - "a": uuid.UUID("06335e84-2872-4914-8c5d-3ed07d2a2f16"), - "b": { - "b_1": "This is deterministic", - "b_2": datetime(year=2020, month=5, day=31), - }, - "c": ["Replace this one", "Do not replace this one"], - } == snapshot diff --git a/tests/syrupy/extensions/image/__init__.py b/tests/syrupy/extensions/image/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/syrupy/extensions/image/__snapshots__/test_image_png.ambr b/tests/syrupy/extensions/image/__snapshots__/test_image_png.ambr new file mode 100644 index 00000000..850ded2d --- /dev/null +++ b/tests/syrupy/extensions/image/__snapshots__/test_image_png.ambr @@ -0,0 +1,3 @@ +# name: test_multiple_snapshot_extensions.1 + b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x002\x00\x00\x002\x04\x03\x00\x00\x00\xec\x11\x95\x82\x00\x00\x00\x1bPLTE\xcc\xcc\xcc\x96\x96\x96\xaa\xaa\xaa\xb7\xb7\xb7\xb1\xb1\xb1\x9c\x9c\x9c\xbe\xbe\xbe\xa3\xa3\xa3\xc5\xc5\xc5\x05\xa4\xf2?\x00\x00\x00\tpHYs\x00\x00\x0e\xc4\x00\x00\x0e\xc4\x01\x95+\x0e\x1b\x00\x00\x00AIDAT8\x8dc`\x18\x05\xa3\x80\xfe\x80I\xd9\xdc\x00F\xa2\x02\x16\x86\x88\x00\xa6\x16\x10\x89.\xc3\x1a" \xc0\x11\x01"\xd1e\xd8\x12#\x028"@$\x86=*\xe6\x06L- \x92zn\x1f\x05\xc3\x1b\x00\x00\xe5\xfb\x08g\r"af\x00\x00\x00\x00IEND\xaeB`\x82' +--- diff --git a/tests/__snapshots__/test_extension_image/test_image.png b/tests/syrupy/extensions/image/__snapshots__/test_image_png/test_image.png similarity index 100% rename from tests/__snapshots__/test_extension_image/test_image.png rename to tests/syrupy/extensions/image/__snapshots__/test_image_png/test_image.png diff --git a/tests/__snapshots__/test_extension_image/test_multiple_snapshot_extensions.2.png b/tests/syrupy/extensions/image/__snapshots__/test_image_png/test_multiple_snapshot_extensions.2.png similarity index 100% rename from tests/__snapshots__/test_extension_image/test_multiple_snapshot_extensions.2.png rename to tests/syrupy/extensions/image/__snapshots__/test_image_png/test_multiple_snapshot_extensions.2.png diff --git a/tests/syrupy/extensions/image/__snapshots__/test_image_png/test_multiple_snapshot_extensions.png b/tests/syrupy/extensions/image/__snapshots__/test_image_png/test_multiple_snapshot_extensions.png new file mode 100644 index 00000000..7eb2b9ad Binary files /dev/null and b/tests/syrupy/extensions/image/__snapshots__/test_image_png/test_multiple_snapshot_extensions.png differ diff --git a/tests/__snapshots__/test_extension_image.ambr b/tests/syrupy/extensions/image/__snapshots__/test_image_svg.ambr similarity index 100% rename from tests/__snapshots__/test_extension_image.ambr rename to tests/syrupy/extensions/image/__snapshots__/test_image_svg.ambr diff --git a/tests/__snapshots__/test_extension_image/test_image_vector.svg b/tests/syrupy/extensions/image/__snapshots__/test_image_svg/test_image.svg similarity index 100% rename from tests/__snapshots__/test_extension_image/test_image_vector.svg rename to tests/syrupy/extensions/image/__snapshots__/test_image_svg/test_image.svg diff --git a/tests/__snapshots__/test_extension_image/test_multiple_snapshot_extensions.3.svg b/tests/syrupy/extensions/image/__snapshots__/test_image_svg/test_multiple_snapshot_extensions.2.svg similarity index 100% rename from tests/__snapshots__/test_extension_image/test_multiple_snapshot_extensions.3.svg rename to tests/syrupy/extensions/image/__snapshots__/test_image_svg/test_multiple_snapshot_extensions.2.svg diff --git a/tests/__snapshots__/test_extension_image/test_multiple_snapshot_extensions.svg b/tests/syrupy/extensions/image/__snapshots__/test_image_svg/test_multiple_snapshot_extensions.svg similarity index 100% rename from tests/__snapshots__/test_extension_image/test_multiple_snapshot_extensions.svg rename to tests/syrupy/extensions/image/__snapshots__/test_image_svg/test_multiple_snapshot_extensions.svg diff --git a/tests/syrupy/extensions/image/test_image_png.py b/tests/syrupy/extensions/image/test_image_png.py new file mode 100644 index 00000000..c2b6a450 --- /dev/null +++ b/tests/syrupy/extensions/image/test_image_png.py @@ -0,0 +1,33 @@ +import base64 + +import pytest + +from syrupy.extensions.image import PNGImageSnapshotExtension + +actual_png = base64.b64decode( + b"iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAAG1BMVEXMzMy" + b"Wlpaqqqq3t7exsbGcnJy+vr6jo6PFxcUFpPI/AAAACXBIWXMAAA7EAAAOxA" + b"GVKw4bAAAAQUlEQVQ4jWNgGAWjgP6ASdncAEaiAhaGiACmFhCJLsMaIiDAE" + b"QEi0WXYEiMCOCJAJIY9KuYGTC0gknpuHwXDGwAA5fsIZw0iYWYAAAAASUVO" + b"RK5CYII=" +) + + +@pytest.fixture +def snapshot_png(snapshot): + return snapshot.use_extension(PNGImageSnapshotExtension) + + +def test_image(snapshot_png): + assert actual_png == snapshot_png + + +def test_multiple_snapshot_extensions(snapshot): + """ + Example of switching extension classes on the fly. + These should be indexed in order of assertion. + """ + assert actual_png == snapshot(extension_class=PNGImageSnapshotExtension) + assert actual_png == snapshot() # uses initial extension class + assert snapshot._extension is not None + assert actual_png == snapshot(extension_class=PNGImageSnapshotExtension) diff --git a/tests/test_extension_image.py b/tests/syrupy/extensions/image/test_image_svg.py similarity index 54% rename from tests/test_extension_image.py rename to tests/syrupy/extensions/image/test_image_svg.py index d2b94a84..0d70e502 100644 --- a/tests/test_extension_image.py +++ b/tests/syrupy/extensions/image/test_image_svg.py @@ -1,19 +1,7 @@ -import base64 - import pytest -from syrupy.extensions.image import ( - PNGImageSnapshotExtension, - SVGImageSnapshotExtension, -) +from syrupy.extensions.image import SVGImageSnapshotExtension -actual_png = base64.b64decode( - b"iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAAG1BMVEXMzMy" - b"Wlpaqqqq3t7exsbGcnJy+vr6jo6PFxcUFpPI/AAAACXBIWXMAAA7EAAAOxA" - b"GVKw4bAAAAQUlEQVQ4jWNgGAWjgP6ASdncAEaiAhaGiACmFhCJLsMaIiDAE" - b"QEi0WXYEiMCOCJAJIY9KuYGTC0gknpuHwXDGwAA5fsIZw0iYWYAAAAASUVO" - b"RK5CYII=" -) actual_svg = ( '' '' @@ -28,19 +16,12 @@ @pytest.fixture -def snapshot_png(snapshot): - return snapshot.use_extension(PNGImageSnapshotExtension) - +def snapshot_svg(snapshot): + return snapshot.use_extension(SVGImageSnapshotExtension) -def test_image(snapshot_png): - assert actual_png == snapshot_png - -def test_image_vector(snapshot): - """ - Example of creating a previewable svg snapshot - """ - assert snapshot(extension_class=SVGImageSnapshotExtension) == actual_svg +def test_image(snapshot_svg): + assert actual_svg == snapshot_svg def test_multiple_snapshot_extensions(snapshot): @@ -51,5 +32,4 @@ def test_multiple_snapshot_extensions(snapshot): assert actual_svg == snapshot(extension_class=SVGImageSnapshotExtension) assert actual_svg == snapshot() # uses initial extension class assert snapshot._extension is not None - assert actual_png == snapshot(extension_class=PNGImageSnapshotExtension) assert actual_svg == snapshot(extension_class=SVGImageSnapshotExtension) diff --git a/tests/test_extension_base.py b/tests/syrupy/extensions/test_base.py similarity index 100% rename from tests/test_extension_base.py rename to tests/syrupy/extensions/test_base.py diff --git a/tests/test_extension_single_file.py b/tests/syrupy/extensions/test_single_file.py similarity index 100% rename from tests/test_extension_single_file.py rename to tests/syrupy/extensions/test_single_file.py diff --git a/tests/syrupy/test_location.py b/tests/syrupy/test_location.py new file mode 100644 index 00000000..71625597 --- /dev/null +++ b/tests/syrupy/test_location.py @@ -0,0 +1,112 @@ +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +from syrupy.location import PyTestLocation + + +def mock_pytest_item(node_id: str, method_name: str) -> "pytest.Item": + mock_node = MagicMock(spec=pytest.Item) + mock_node.nodeid = node_id + [filepath, *_, nodename] = node_id.split("::") + mock_node.name = nodename + mock_node.fspath = filepath + mock_node.obj = MagicMock() + mock_node.obj.__module__ = Path(filepath).stem + mock_node.obj.__name__ = method_name + return mock_node + + +@pytest.mark.parametrize( + "node_id, method_name, expected_filename," + "expected_classname,expected_snapshotname", + ( + ( + "/tests/module/test_file.py::TestClass::method_name", + "method_name", + "test_file", + "TestClass", + "TestClass.method_name", + ), + ( + "/tests/module/test_file.py::TestClass::method_name[1]", + "method_name", + "test_file", + "TestClass", + "TestClass.method_name[1]", + ), + ( + "/tests/module/nest/test_file.py::TestClass::TestSubClass::method_name", + "method_name", + "test_file", + "TestClass.TestSubClass", + "TestClass.TestSubClass.method_name", + ), + ), +) +def test_location_properties( + node_id, + method_name, + expected_filename, + expected_classname, + expected_snapshotname, +): + location = PyTestLocation(mock_pytest_item(node_id, method_name)) + assert location.classname == expected_classname + assert location.filename == expected_filename + assert location.snapshot_name == expected_snapshotname + + +@pytest.mark.parametrize( + "node_id, method_name," + "expected_location_matches, expected_location_misses," + "expected_snapshot_matches, expected_snapshot_misses", + ( + ( + "/tests/module/test_file.py::TestClass::method_name", + "method_name", + ("test_file.snap", "__snapshots__/test_file", "test_file/1.snap"), + ("test.snap", "__others__/test/file.snap"), + ( + "TestClass.method_name", + "TestClass.method_name[1]", + "TestClass.method_name.1", + ), + ("method_name", "TestClass.method_names"), + ), + ( + "/tests/module/test_file.py::TestClass::method_name[1]", + "method_name", + ("test_file.snap", "__snapshots__/test_file", "test_file/1.snap"), + ("test.snap", "__others__/test/file.snap"), + ( + "TestClass.method_name", + "TestClass.method_name[1]", + "TestClass.method_name.1", + ), + ("method_name", "TestClass.method_names"), + ), + ), +) +def test_location_matching( + node_id, + method_name, + expected_location_matches, + expected_location_misses, + expected_snapshot_matches, + expected_snapshot_misses, +): + location = PyTestLocation(mock_pytest_item(node_id, method_name)) + + for location_match in expected_location_matches: + assert location.matches_snapshot_location(location_match) + + for location_miss in expected_location_misses: + assert not location.matches_snapshot_location(location_miss) + + for snapshot_match in expected_snapshot_matches: + assert location.matches_snapshot_name(snapshot_match) + + for snapshot_miss in expected_snapshot_misses: + assert not location.matches_snapshot_name(snapshot_miss) diff --git a/tests/test_utils.py b/tests/syrupy/test_utils.py similarity index 100% rename from tests/test_utils.py rename to tests/syrupy/test_utils.py diff --git a/tests/test_integration_custom.py b/tests/test_integration_custom.py deleted file mode 100644 index 1837f6bf..00000000 --- a/tests/test_integration_custom.py +++ /dev/null @@ -1,64 +0,0 @@ -import pytest - - -@pytest.fixture -def testcases(testdir): - testdir.makeconftest( - """ - import pytest - - from syrupy.extensions.base import AbstractSyrupyExtension - - class CustomSnapshotExtension(AbstractSyrupyExtension): - def _file_extension(self): - return "" - - def serialize(self, data, **kwargs): - return str(data) - - def get_snapshot_name(self, *, index = 0): - testname = self._test_location.testname[::-1] - return f"{testname}.{index}" - - def _read_snapshot_fossil(self, **kwargs): - pass - - def _read_snapshot_data_from_location(self, **kwargs): - pass - - def _write_snapshot_fossil(self, **kwargs): - pass - - def delete_snapshots(self, **kwargs): - pass - - def _get_file_basename(self, *, index = 0): - return self.test_location.filename[::-1] - - - @pytest.fixture - def snapshot_custom(snapshot): - return snapshot.use_extension(CustomSnapshotExtension) - """ - ) - return { - "passed": ( - """ - def test_passed_custom(snapshot_custom): - assert snapshot_custom == 'passed1' - assert snapshot_custom == 'passed2' - """ - ) - } - - -def test_warns_on_snapshot_name(testdir, testcases): - testdir.makepyfile(test_file=testcases["passed"]) - result = testdir.runpytest("-v", "--snapshot-update", "--snapshot-no-colors") - result_stdout = result.stdout.str() - assert "2 snapshots generated" in result_stdout - assert "Warning:" in result_stdout - assert "Can not relate snapshot name" in result_stdout - assert "Can not relate snapshot location" in result_stdout - assert "test_passed_custom" in result_stdout - assert result.ret == 0 diff --git a/tests/test_integration_default.py b/tests/test_integration_default.py deleted file mode 100644 index 941be0e1..00000000 --- a/tests/test_integration_default.py +++ /dev/null @@ -1,570 +0,0 @@ -from pathlib import Path - -import pytest - - -@pytest.fixture -def collection(testdir): - tests = { - "test_collected": ( - """ - import pytest - - @pytest.mark.parametrize("actual", [1, 2, 3]) - def test_collected(snapshot, actual): - assert snapshot == actual - """ - ), - "test_not_collected": ( - """ - def test_collected1(snapshot): - assert snapshot == "hello" - """ - ), - } - testdir.makepyfile(**tests) - result = testdir.runpytest("-v", "--snapshot-update") - result.stdout.re_match_lines((r"4 snapshots generated.")) - testdir.makefile(".ambr", **{str(Path("__snapshots__", "other_snapfile")): ""}) - return testdir - - -def test_unused_snapshots_ignored_if_not_targeted_using_dash_m(collection): - updated_tests = { - "test_collected": ( - """ - import pytest - - @pytest.mark.parametrize("actual", [1, "2"]) - def test_collected(snapshot, actual): - assert snapshot == actual - """ - ), - } - collection.makepyfile(**updated_tests) - result = collection.runpytest("-v", "--snapshot-update", "-m", "parametrize") - result.stdout.re_match_lines( - ( - r"1 snapshot passed\. 1 snapshot updated\. 1 unused snapshot deleted\.", - r"Deleted test_collected\[3\] \(__snapshots__[\\/]test_collected.ambr\)", - ) - ) - snapshot_path = [collection.tmpdir, "__snapshots__"] - assert Path(*snapshot_path, "test_not_collected.ambr").exists() - assert Path(*snapshot_path, "other_snapfile.ambr").exists() - - -def test_unused_snapshots_ignored_if_not_targeted_using_dash_k(collection): - updated_tests = { - "test_collected": ( - """ - import pytest - - @pytest.mark.parametrize("actual", [1, "2"]) - def test_collected(snapshot, actual): - assert snapshot == actual - """ - ), - } - collection.makepyfile(**updated_tests) - result = collection.runpytest("-v", "--snapshot-update", "-k", "test_collected[") - result.stdout.re_match_lines( - ( - r"1 snapshot passed\. 1 snapshot updated\. 1 unused snapshot deleted\.", - r"Deleted test_collected\[3\] \(__snapshots__[\\/]test_collected.ambr\)", - ) - ) - snapshot_path = [collection.tmpdir, "__snapshots__"] - assert Path(*snapshot_path, "test_not_collected.ambr").exists() - assert Path(*snapshot_path, "other_snapfile.ambr").exists() - - -def test_unused_parameterized_ignored_if_not_targeted_using_dash_k(collection): - updated_tests = { - "test_collected": ( - """ - import pytest - - @pytest.mark.parametrize("actual", [1, 2]) - def test_collected(snapshot, actual): - assert snapshot == actual - """ - ), - } - collection.makepyfile(**updated_tests) - result = collection.runpytest("-v", "--snapshot-update", "-k", "test_collected[") - result.stdout.re_match_lines( - ( - r"2 snapshots passed\. 1 unused snapshot deleted\.", - r"Deleted test_collected\[3\] \(__snapshots__[\\/]test_collected\.ambr\)", - ) - ) - snapshot_path = [collection.tmpdir, "__snapshots__"] - assert Path(*snapshot_path, "test_not_collected.ambr").exists() - assert Path(*snapshot_path, "other_snapfile.ambr").exists() - - -def test_only_updates_targeted_snapshot_in_class_for_single_file(testdir): - test_content = """ - import pytest - - class TestClass: - def test_case_1(self, snapshot): - assert snapshot == 1 - - def test_case_2(self, snapshot): - assert snapshot == 2 - """ - - testdir.makepyfile(test_content=test_content) - testdir.runpytest("-v", "--snapshot-update") - - snapshot_path = Path(testdir.tmpdir, "__snapshots__") - assert snapshot_path.joinpath("test_content.ambr").exists() - - test_filepath = Path(testdir.tmpdir, "test_content.py") - result = testdir.runpytest(str(test_filepath), "-v", "-k test_case_2") - result_stdout = result.stdout.str() - assert "1 snapshot passed" in result_stdout - assert "snapshot unused" not in result_stdout - - -def test_only_updates_targeted_snapshot_for_single_file(testdir): - test_content = """ - import pytest - - def test_case_1(snapshot): - assert snapshot == 1 - - def test_case_2(snapshot): - assert snapshot == 2 - """ - - testdir.makepyfile(test_content=test_content) - testdir.runpytest("-v", "--snapshot-update") - - snapshot_path = Path(testdir.tmpdir, "__snapshots__") - assert snapshot_path.joinpath("test_content.ambr").exists() - - test_filepath = Path(testdir.tmpdir, "test_content.py") - result = testdir.runpytest(str(test_filepath), "-v", "-k test_case_2") - result_stdout = result.stdout.str() - assert "1 snapshot passed" in result_stdout - assert "snapshot unused" not in result_stdout - - -def test_multiple_snapshots(testdir): - test_content = """ - import pytest - - def test_case_1(snapshot): - assert snapshot == 1 - assert snapshot == 2 - """ - - testdir.makepyfile(test_content=test_content) - result = testdir.runpytest("-v", "--snapshot-update") - - result_stdout = result.stdout.str() - assert "Can not relate snapshot name" not in result_stdout - - -@pytest.fixture -def testcases(): - return { - "inject": ( - """ - def test_injection(snapshot): - assert snapshot is not None - """ - ), - "used": ( - """ - def test_used(snapshot): - assert snapshot == 'used' - """ - ), - "unused": ( - """ - def test_unused(snapshot): - assert snapshot == 'unused' - """ - ), - "updated_1": ( - """ - def test_updated_1(snapshot): - assert snapshot == ['this', 'will', 'be', 'updated'] - """ - ), - "updated_2": ( - """ - def test_updated_2(snapshot): - assert ['this', 'will', 'be', 'updated'] == snapshot - """ - ), - "updated_3": ( - """ - def test_updated_3(snapshot): - assert snapshot == ['this', 'will', 'be', 'updated'] - """ - ), - "updated_4": ( - """ - def test_updated_4(snapshot): - assert snapshot == "single line change" - """ - ), - "updated_5": ( - """ - def test_updated_5(snapshot): - assert snapshot == ''' - multiple line changes - with some lines staying the same - intermittent changes that have to be ignore by the differ output - because when there are a lot of changes you only want to see changes - you do not want to see this line - or this line - this line should not show up because new lines are normalised\\r\\n - \x1b[38;5;1mthis line should show up because it changes color\x1b[0m - ''' - """ - ), - } - - -@pytest.fixture -def testcases_updated(testcases): - updated_testcases = { - "updated_1": ( - """ - def test_updated_1(snapshot): - assert snapshot == ['this', 'will', 'not', 'match'] - """ - ), - "updated_2": ( - """ - def test_updated_2(snapshot): - assert ['this', 'will', 'fail'] == snapshot - """ - ), - "updated_3": ( - """ - def test_updated_3(snapshot): - assert snapshot == ['this', 'will', 'be', 'too', 'much'] - """ - ), - "updated_4": ( - """ - def test_updated_4(snapshot): - assert snapshot == "sing line changeling" - """ - ), - "updated_5": ( - """ - def test_updated_5(snapshot): - assert snapshot == ''' - multiple line changes - with some lines not staying the same - intermittent changes so unchanged lines have to be ignored by the differ - cause when there are a lot of changes you only want to see what changed - you do not want to see this line - or this line - this line should not show up because new lines are normalised\\n - \x1b[38;5;3mthis line should show up because it changes color\x1b[0m - and this line does not exist in the first one - ''' - """ - ), - } - return {**testcases, **updated_testcases} - - -def test_missing_snapshots(testdir, testcases): - testdir.makepyfile(test_file=testcases["used"]) - result = testdir.runpytest("-v") - result.stdout.re_match_lines((r"1 snapshot failed\.")) - assert result.ret == 1 - - -@pytest.fixture -def stubs(testdir, testcases): - pyfile_content = "\n\n".join(testcases.values()) - testdir.makepyfile(test_file=pyfile_content) - filepath = Path(testdir.tmpdir, "__snapshots__", "test_file.ambr") - return testdir.runpytest("-v", "--snapshot-update"), testdir, testcases, filepath - - -def test_injected_fixture(stubs): - result = stubs[0] - result.stdout.fnmatch_lines(["*::test_injection PASSED*"]) - assert result.ret == 0 - - -def test_generated_snapshots(stubs): - result = stubs[0] - result.stdout.re_match_lines((r"7 snapshots generated\.")) - assert result.ret == 0 - - -def test_failing_snapshots_diff(stubs, testcases_updated): - testdir = stubs[1] - testdir.makepyfile(test_file="\n\n".join(testcases_updated.values())) - result = testdir.runpytest("-vv", "--snapshot-no-colors") - result.stdout.re_match_lines( - ( - r".*assert snapshot == \['this', 'will', 'not', 'match'\]", - r".*AssertionError: assert \[- snapshot\] == \[\+ received\]", - r".* \[", - r".* ...", - r".* 'will',", - r".* - 'be',", - r".* - 'updated',", - r".* \+ 'not',", - r".* \+ 'match',", - r".* \]", - r".*assert \['this', 'will', 'fail'\] == snapshot", - r".*AssertionError: assert \[\+ received\] == \[- snapshot\]", - r".* \[", - r".* ...", - r".* 'will',", - r".* - 'be',", - r".* \+ 'fail',", - r".* - 'updated',", - r".* \]", - r".*assert snapshot == \['this', 'will', 'be', 'too', 'much'\]", - r".*AssertionError: assert \[- snapshot\] == \[\+ received\]", - r".* \[", - r".* ...", - r".* 'be',", - r".* - 'updated',", - r".* \+ 'too',", - r".* \+ 'much',", - r".* \]", - r".*assert snapshot == \"sing line changeling\"", - r".*AssertionError: assert \[- snapshot\] == \[\+ received\]", - r".* - 'single line change'", - r".* \+ 'sing line changeling'", - r".*AssertionError: assert \[- snapshot\] == \[\+ received\]", - r".* '", - r".* ...", - r".* multiple line changes", - r".* - with some lines staying the same", - r".* \+ with some lines not staying the same", - r".* - intermittent changes that have to be ignore by the differ out", - r".* \+ intermittent changes so unchanged lines have to be ignored b", - r".* - because when there are a lot of changes you only want to see ", - r".* \+ cause when there are a lot of changes you only want to see w", - r".* you do not want to see this line", - r".* ...", - r".* ", - r".* - \[38;5;1mthis line should show up because it changes color", - r".* \+ \[38;5;3mthis line should show up because it changes color", - r".* \+ and this line does not exist in the first one", - r".* ", - r".* '", - ) - ) - assert result.ret == 1 - - -def test_updated_snapshots(stubs, testcases_updated): - testdir = stubs[1] - testdir.makepyfile(test_file="\n\n".join(testcases_updated.values())) - result = testdir.runpytest("-v", "--snapshot-update") - result.stdout.re_match_lines((r"2 snapshots passed\. 5 snapshots updated\.")) - assert result.ret == 0 - - -def test_unused_snapshots(stubs): - _, testdir, tests, _ = stubs - testdir.makepyfile(test_file="\n\n".join(tests[k] for k in tests if k != "unused")) - result = testdir.runpytest("-v") - result.stdout.re_match_lines( - ( - r"6 snapshots passed\. 1 snapshot unused\.", - r"Re-run pytest with --snapshot-update to delete unused snapshots\.", - ) - ) - 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.re_match_lines( - ( - r"6 snapshots passed\. 1 snapshot unused\.", - r"Re-run pytest with --snapshot-update to delete unused snapshots\.", - ) - ) - assert result.ret == 0 - - -def test_unused_snapshots_ignored_if_not_targeted_by_testnode_ids(testdir): - path_to_snap = Path("__snapshots__", "other_snapfile") - testdir.makefile(".ambr", **{str(path_to_snap): ""}) - testdir.makefile( - ".py", - test_life_uhh_finds_a_way=( - """ - def test_life_always_finds_a_way(snapshot): - assert snapshot == snapshot - - def test_clever_girl(snapshot): - assert snapshot == snapshot - """ - ), - ) - testfile = Path(testdir.tmpdir, "test_life_uhh_finds_a_way.py") - testdir.runpytest(str(testfile), "-v", "--snapshot-update") - result = testdir.runpytest( - f"{testfile}::test_life_always_finds_a_way", "-v", "--snapshot-update" - ) - result.stdout.re_match_lines((r"1 snapshot passed\.")) - assert result.ret == 0 - assert Path(f"{path_to_snap}.ambr").exists() - - -def test_unused_snapshots_ignored_if_not_targeted_by_module_testfiles(stubs): - _, testdir, tests, _ = stubs - path_to_snap = Path("__snapshots__", "other_snapfile") - testdir.makepyfile(test_file="\n\n".join(tests[k] for k in tests if k != "unused")) - testdir.makefile(".ambr", **{str(path_to_snap): ""}) - result = testdir.runpytest("-v", "--snapshot-update", "test_file.py") - result.stdout.re_match_lines( - ( - r"6 snapshots passed\. 1 unused snapshot deleted\.", - r"Deleted test_unused \(__snapshots__[\\/]test_file\.ambr\)", - ) - ) - assert result.ret == 0 - assert Path(f"{path_to_snap}.ambr").exists() - - -def test_unused_snapshots_cleaned_up_when_targeting_specific_testfiles(stubs): - _, testdir, _, _ = stubs - path_to_snap = Path("__snapshots__", "other_snapfile") - testdir.makepyfile( - test_file=( - """ - def test_used(snapshot): - assert True - """ - ), - ) - testdir.makefile(".ambr", **{str(path_to_snap): ""}) - result = testdir.runpytest("-v", "--snapshot-update", "test_file.py") - result.stdout.re_match_lines_random( - ( - r"7 unused snapshots deleted\.", - r"Deleted test_unused, test_updated_1, test_updated_2, test_updated_3", - r".*test_updated_3, test_updated_4, test_updated_5, test_used", - r".*test_used \(__snapshots__[\\/]test_file.ambr\)", - ) - ) - assert result.ret == 0 - assert Path(f"{path_to_snap}.ambr").exists() - - -def test_removed_snapshots(stubs): - _, testdir, tests, filepath = stubs - assert Path(filepath).exists() - testdir.makepyfile(test_file="\n\n".join(tests[k] for k in tests if k != "unused")) - result = testdir.runpytest("-v", "--snapshot-update") - result.stdout.re_match_lines( - ( - r"6 snapshots passed\. 1 unused snapshot deleted\.", - r"Deleted test_unused \(__snapshots__[\\/]test_file\.ambr\)", - ) - ) - assert result.ret == 0 - assert Path(filepath).exists() - - -def test_removed_snapshot_fossil(stubs): - _, testdir, tests, filepath = stubs - assert Path(filepath).exists() - testdir.makepyfile(test_file=tests["inject"]) - result = testdir.runpytest("-v", "--snapshot-update") - result.stdout.re_match_lines_random( - ( - r"7 unused snapshots deleted\.", - r"Deleted test_unused, test_updated_1, test_updated_2, test_updated_3", - r".*test_updated_3, test_updated_4, test_updated_5, test_used", - r".*test_used \(__snapshots__[\\/]test_file\.ambr\)", - ) - ) - assert result.ret == 0 - assert not Path(filepath).exists() - - -def test_removed_empty_snapshot_fossil_only(stubs): - _, testdir, _, filepath = stubs - path_to_snap = Path("__snapshots__", "test_empty") - testdir.makefile(".ambr", **{str(path_to_snap): ""}) - empty_filepath = Path(testdir.tmpdir, f"{path_to_snap}.ambr") - assert empty_filepath.exists() - result = testdir.runpytest("-v", "--snapshot-update") - result.stdout.re_match_lines( - ( - r"7 snapshots passed\. 1 unused snapshot deleted\.", - r"Deleted empty snapshot fossil \(__snapshots__[\\/]test_empty\.ambr\)", - ) - ) - assert result.ret == 0 - assert Path(filepath).exists() - assert not Path(empty_filepath).exists() - - -def test_removed_hanging_snapshot_fossil(stubs): - _, testdir, _, filepath = stubs - path_to_snap = Path("__snapshots__", "test_hanging") - testdir.makefile(".abc", **{str(path_to_snap): ""}) - hanging_filepath = Path(testdir.tmpdir, f"{path_to_snap}.abc") - assert hanging_filepath.exists() - result = testdir.runpytest("-v", "--snapshot-update") - result_stdout = result.stdout.str() - assert str(Path(filepath).relative_to(Path.cwd())) not in result_stdout - assert "1 unused snapshot deleted" in result_stdout - assert "unknown snapshot" in result_stdout - assert str(hanging_filepath.relative_to(Path.cwd())) in result_stdout - assert result.ret == 0 - assert Path(filepath).exists() - assert not Path(hanging_filepath).exists() - - -def test_snapshot_default_extension_option(testdir): - testdir.makepyfile( - test_file=( - """ - def test_default(snapshot): - assert b"default extension serializer" == snapshot - """ - ), - ) - result = testdir.runpytest( - "-v", - "--snapshot-update", - "--snapshot-default-extension", - "syrupy.extensions.single_file.SingleFileSnapshotExtension", - ) - result.stdout.re_match_lines((r"1 snapshot generated\.")) - assert Path( - testdir.tmpdir, "__snapshots__", "test_file", "test_default.raw" - ).exists() - assert result.ret == 0 - - -def test_snapshot_default_extension_option_failure(testdir, testcases): - testdir.makepyfile(test_file=testcases["used"]) - result = testdir.runpytest( - "-v", - "--snapshot-update", - "--snapshot-default-extension", - "syrupy.extensions.amber.DoesNotExistExtension", - ) - result_stderr = result.stderr.str() - assert "error: argument --snapshot-default-extension" in result_stderr - assert "Member 'DoesNotExistExtension' not found" in result_stderr - assert result.ret diff --git a/tests/test_integration_single_file.py b/tests/test_integration_single_file.py deleted file mode 100644 index b7bb0eb6..00000000 --- a/tests/test_integration_single_file.py +++ /dev/null @@ -1,112 +0,0 @@ -import pytest - - -@pytest.fixture -def testcases(testdir): - testdir.makeconftest( - """ - import pytest - - from syrupy.extensions.single_file import SingleFileSnapshotExtension - from syrupy.extensions.image import ( - PNGImageSnapshotExtension, - SVGImageSnapshotExtension, - ) - - - @pytest.fixture - def snapshot_single(snapshot): - return snapshot.use_extension(SingleFileSnapshotExtension) - - - @pytest.fixture - def snapshot_png(snapshot): - return snapshot.use_extension(PNGImageSnapshotExtension) - - - @pytest.fixture - def snapshot_svg(snapshot): - return snapshot.use_extension(SVGImageSnapshotExtension) - """ - ) - return { - "passed": ( - """ - def test_passed_single(snapshot_single): - assert snapshot_single == b'passed1' - assert snapshot_single == b'passed2' - """ - ), - "failed": ( - """ - def test_failed_single(snapshot_single): - assert snapshot_single == 'failed' - - def test_failed_image(snapshot_png): - assert "not a byte string" == snapshot_png - """ - ), - } - - -@pytest.fixture -def testcases_updated(testcases): - updated_testcases = { - "passed": ( - """ - def test_passed_single(snapshot_single): - assert snapshot_single == b'passed' - """ - ) - } - return {**testcases, **updated_testcases} - - -def test_unsaved_snapshots(testdir, testcases): - testdir.makepyfile(test_file=testcases["passed"]) - result = testdir.runpytest("-v") - output = result.stdout.str() - assert "Snapshot 'test_passed_single' does not exist" in output - assert "+ b'passed1'" in output - assert result.ret == 1 - - -def test_failed_snapshots(testdir, testcases): - testdir.makepyfile(test_file=testcases["failed"]) - result = testdir.runpytest("-v", "--snapshot-update") - assert "2 snapshots failed" in result.stdout.str() - assert result.ret == 1 - - -@pytest.fixture -def stubs(testdir, testcases): - testdir.makepyfile(test_file=testcases["passed"]) - return testdir.runpytest("-v", "--snapshot-update"), testdir, testcases - - -def test_generated_snapshots(stubs): - result = stubs[0] - result_stdout = result.stdout.str() - assert "2 snapshots generated" in result_stdout - assert "snapshots unused" not in result_stdout - assert result.ret == 0 - - -def test_unmatched_snapshots(stubs, testcases_updated): - testdir = stubs[1] - testdir.makepyfile(test_file=testcases_updated["passed"]) - result = testdir.runpytest("-v") - result_stdout = result.stdout.str() - assert "1 snapshot failed" in result_stdout - assert "1 snapshot unused" in result_stdout - assert result.ret == 1 - - -def test_updated_snapshots(stubs, testcases_updated): - testdir = stubs[1] - testdir.makepyfile(test_file=testcases_updated["passed"]) - result = testdir.runpytest("-v", "--snapshot-update") - result_stdout = result.stdout.str() - assert "1 snapshot updated" in result_stdout - assert "1 unused snapshot deleted" in result_stdout - assert result.ret == 0