diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index d68cf79d..289e0728 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -8,14 +8,14 @@ env: jobs: analysis: name: Code Analysis - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Set up Python 3.8 uses: actions/setup-python@v1 with: python-version: 3.8 - - name: Install lint & packaging dependencies. + - name: Install project dependencies run: | python -m pip install -U pip --no-cache-dir python -m pip install -e . -r dev_requirements.txt @@ -23,18 +23,19 @@ jobs: run: invoke lint tests: name: Tests - runs-on: ubuntu-18.04 + runs-on: ${{ matrix.os }} strategy: matrix: + os: [ubuntu-latest, windows-latest] python-version: [3.6, 3.7, 3.8] - fail-fast: true + fail-fast: false steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - - name: Install test dependencies. + - name: Install project dependencies run: | python -m pip install -U pip --no-cache-dir python -m pip install -e . -r dev_requirements.txt @@ -44,23 +45,19 @@ jobs: run: invoke test --coverage build: name: Build - runs-on: ubuntu-18.04 - strategy: - matrix: - python-version: [3.8] - fail-fast: true + runs-on: ubuntu-latest needs: [analysis, tests] steps: - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} + - name: Set up Python 3.8 uses: actions/setup-python@v1 with: - python-version: ${{ matrix.python-version }} - - name: Install build dependencies. + python-version: 3.8 + - name: Install project dependencies run: | python -m pip install -U pip --no-cache-dir python -m pip install -e . -r dev_requirements.txt - - name: Release Python ${{ matrix.python-version }} + - name: Release Python 3.8 env: TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} diff --git a/benchmarks/__init__.py b/benchmarks/__init__.py index ca0d8026..97f587de 100644 --- a/benchmarks/__init__.py +++ b/benchmarks/__init__.py @@ -103,7 +103,7 @@ def measure_perf( return repo = github.get_repo(GH_REPO) commit_sha = get_req_env("GITHUB_SHA") - with open(BENCH_PERF_FILE, "r") as bench_file: + with open(BENCH_PERF_FILE, "r", encoding="utf-8") as bench_file: try: repo.create_file( path=get_commit_bench_path(commit_sha), @@ -144,7 +144,7 @@ def fetch_ref_bench_json(github: "Github", ref_branch: str = GH_BRANCH_REF) -> b if not ref_json: return False - with open(BENCH_REF_FILE, "w") as ref_json_file: + with open(BENCH_REF_FILE, "w", encoding="utf-8") as ref_json_file: ref_json_file.write(ref_json) return True diff --git a/setup.py b/setup.py index e95c1f9b..79060fef 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ def readme() -> str: - with open("README.md") as f: + with open("README.md", encoding="utf-8") as f: return f.read() diff --git a/src/syrupy/__init__.py b/src/syrupy/__init__.py index 88850ed1..004e5d10 100644 --- a/src/syrupy/__init__.py +++ b/src/syrupy/__init__.py @@ -87,15 +87,16 @@ def pytest_assertrepr_compare( https://docs.pytest.org/en/latest/reference.html#_pytest.hookspec.pytest_assertrepr_compare """ with __terminal_color(config): + received_name = received_style("[+ received]") + + def snapshot_name(name: str) -> str: + return snapshot_style(f"[- {name}]") + if isinstance(left, SnapshotAssertion): - assert_msg = reset( - f"{snapshot_style(left.name)} {op} {received_style('received')}" - ) + assert_msg = reset(f"{snapshot_name(left.name)} {op} {received_name}") return [assert_msg] + left.get_assert_diff() elif isinstance(right, SnapshotAssertion): - assert_msg = reset( - f"{received_style('received')} {op} {snapshot_style(right.name)}" - ) + assert_msg = reset(f"{received_name} {op} {snapshot_name(right.name)}") return [assert_msg] + right.get_assert_diff() return None diff --git a/src/syrupy/assertion.py b/src/syrupy/assertion.py index f0396dec..57d69698 100644 --- a/src/syrupy/assertion.py +++ b/src/syrupy/assertion.py @@ -106,7 +106,11 @@ def get_assert_diff(self) -> List[str]: serialized_data = assertion_result.asserted_data or "" diff: List[str] = [] if snapshot_data is None: - diff.append(gettext("Snapshot does not exist!")) + diff.append( + gettext("Snapshot '{}' does not exist!").format( + assertion_result.snapshot_name + ) + ) if not assertion_result.success: diff.extend(self.extension.diff_lines(serialized_data, snapshot_data or "")) return diff diff --git a/src/syrupy/extensions/amber/serializer.py b/src/syrupy/extensions/amber/serializer.py index 3f58ef03..9b0203d0 100644 --- a/src/syrupy/extensions/amber/serializer.py +++ b/src/syrupy/extensions/amber/serializer.py @@ -1,3 +1,4 @@ +import os from types import GeneratorType from typing import ( TYPE_CHECKING, @@ -46,6 +47,7 @@ class DataSerializer: _max_depth: int = 99 _marker_divider: str = "---" _marker_name: str = "# name:" + _marker_crn: str = "\r\n" @classmethod def write_file(cls, snapshot_fossil: "SnapshotFossil") -> None: @@ -53,13 +55,13 @@ def write_file(cls, snapshot_fossil: "SnapshotFossil") -> None: Writes the snapshot data into the snapshot file that be read later. """ filepath = snapshot_fossil.location - with open(filepath, "w", encoding="utf-8", newline="") as f: + with open(filepath, "w", encoding="utf-8", newline=None) as f: for snapshot in sorted(snapshot_fossil, key=lambda s: s.name): snapshot_data = str(snapshot.data) if snapshot_data is not None: f.write(f"{cls._marker_name} {snapshot.name}\n") for data_line in snapshot_data.splitlines(keepends=True): - f.write(f"{cls._indent}{data_line}") + f.write(cls.with_indent(data_line, 1)) f.write(f"\n{cls._marker_divider}\n") @classmethod @@ -73,12 +75,12 @@ def read_file(cls, filepath: str) -> "SnapshotFossil": indent_len = len(cls._indent) snapshot_fossil = SnapshotFossil(location=filepath) try: - with open(filepath, "r", encoding="utf-8", newline="") as f: + with open(filepath, "r", encoding="utf-8", newline=None) as f: test_name = None snapshot_data = "" for line in f: if line.startswith(cls._marker_name): - test_name = line[name_marker_len:-1].strip(" \r\n") + test_name = line[name_marker_len:].strip(f" {cls._marker_crn}") snapshot_data = "" continue elif test_name is not None: @@ -86,7 +88,10 @@ def read_file(cls, filepath: str) -> "SnapshotFossil": snapshot_data += line[indent_len:] elif line.startswith(cls._marker_divider) and snapshot_data: snapshot_fossil.add( - Snapshot(name=test_name, data=snapshot_data[:-1]) + Snapshot( + name=test_name, + data=snapshot_data.rstrip(os.linesep), + ) ) except FileNotFoundError: pass @@ -95,6 +100,23 @@ def read_file(cls, filepath: str) -> "SnapshotFossil": @classmethod def serialize( + cls, + data: "SerializableData", + *, + exclude: Optional["PropertyFilter"] = None, + matcher: Optional["PropertyMatcher"] = None, + ) -> str: + """ + After serializing, new line control characters are normalised. This is needed + for interoperablity of snapshot matching between systems that do not use the + same new line control characters. Example snapshots generated on windows os + should not break when running the tests on a unix based system and vice versa. + """ + serialized = cls._serialize(data, exclude=exclude, matcher=matcher) + return serialized.replace(cls._marker_crn, "\n").replace("\r", "\n") + + @classmethod + def _serialize( cls, data: "SerializableData", *, @@ -137,26 +159,27 @@ def serialize( def serialize_number( cls, data: "SerializableData", *, depth: int = 0, **kwargs: Any ) -> str: - return cls.with_indent(repr(data), depth) + return cls.__serialize_plain(data=data, depth=depth) @classmethod def serialize_string( cls, data: "SerializableData", *, depth: int = 0, **kwargs: Any ) -> str: - if "\n" in data: - return cls.__serialize_lines( - data=data, - lines=( - cls.with_indent(line, depth + 1 if depth else depth) - for line in str(data).splitlines(keepends=True) - ), - depth=depth, - open_tag="'", - close_tag="'", - include_type=False, - ends="", - ) - return cls.with_indent(repr(data), depth) + if all(c not in data for c in cls._marker_crn): + return cls.__serialize_plain(data=data, depth=depth) + + return cls.__serialize_lines( + data=data, + lines=( + cls.with_indent(line, depth + 1 if depth else depth) + for line in str(data).splitlines(keepends=True) + ), + depth=depth, + open_tag="'", + close_tag="'", + include_type=False, + ends="", + ) @classmethod def serialize_iterable(cls, data: "SerializableData", **kwargs: Any) -> str: @@ -214,7 +237,7 @@ def serialize_dict(cls, data: "SerializableData", **kwargs: Any) -> str: @classmethod def serialize_unknown(cls, data: Any, *, depth: int = 0, **kwargs: Any) -> str: if data.__class__.__repr__ != object.__repr__: - return cls.with_indent(repr(data), depth) + return cls.__serialize_plain(data=data, depth=depth) return cls.__serialize_iterable( data=data, @@ -239,7 +262,7 @@ def sort(cls, iterable: Iterable[Any]) -> Iterable[Any]: try: return sorted(iterable) except TypeError: - return sorted(iterable, key=cls.serialize) + return sorted(iterable, key=cls._serialize) @classmethod def object_type(cls, data: "SerializableData") -> str: @@ -251,6 +274,15 @@ def __is_namedtuple(cls, obj: Any) -> bool: type(n) == str for n in getattr(obj, "_fields", [None]) ) + @classmethod + def __serialize_plain( + cls, + *, + data: "SerializableData", + depth: int = 0, + ) -> str: + return cls.with_indent(repr(data), depth) + @classmethod def __serialize_iterable( cls, @@ -284,13 +316,13 @@ def key_str(key: "PropertyName") -> str: if separator is None: return "" return ( - cls.serialize(data=key, **kwargs) + cls._serialize(data=key, **kwargs) if serialize_key else cls.with_indent(str(key), depth=depth + 1) ) + separator def value_str(key: "PropertyName", value: "SerializableData") -> str: - serialized = cls.serialize( + serialized = cls._serialize( data=value, exclude=exclude, path=(*path, (key, type(value))), **kwargs ) return serialized if separator is None else serialized.lstrip(cls._indent) @@ -317,10 +349,7 @@ def __serialize_lines( ) -> str: lines = ends.join(lines) lines_end = "\n" if lines else "" - formatted_open_tag = ( - open_tag if include_type else cls.with_indent(open_tag, depth) - ) + maybe_obj_type = f"{cls.object_type(data)} " if include_type else "" + formatted_open_tag = cls.with_indent(f"{maybe_obj_type}{open_tag}", depth) formatted_close_tag = cls.with_indent(close_tag, depth) - return ( - f"{cls.with_indent(cls.object_type(data), depth)} " if include_type else "" - ) + f"{formatted_open_tag}\n{lines}{lines_end}{formatted_close_tag}" + return f"{formatted_open_tag}\n{lines}{lines_end}{formatted_close_tag}" diff --git a/src/syrupy/extensions/base.py b/src/syrupy/extensions/base.py index dea39b52..7bd4dffb 100644 --- a/src/syrupy/extensions/base.py +++ b/src/syrupy/extensions/base.py @@ -140,16 +140,24 @@ def write_snapshot(self, *, data: "SerializedData", index: int) -> None: snapshot_location = self.get_location(index=index) if not self.test_location.matches_snapshot_location(snapshot_location): warning_msg = gettext( - "\nCan not relate snapshot location '{}' to the test location." - "\nConsider adding '{}' to the generated location." - ).format(snapshot_location, self.test_location.filename) + "{line_end}Can not relate snapshot location '{}' to the test location." + "{line_end}Consider adding '{}' to the generated location." + ).format( + snapshot_location, + self.test_location.filename, + line_end="\n", + ) warnings.warn(warning_msg) snapshot_name = self.get_snapshot_name(index=index) if not self.test_location.matches_snapshot_name(snapshot_name): warning_msg = gettext( - "\nCan not relate snapshot name '{}' to the test location." - "\nConsider adding '{}' to the generated name." - ).format(snapshot_name, self.test_location.testname) + "{line_end}Can not relate snapshot name '{}' to the test location." + "{line_end}Consider adding '{}' to the generated name." + ).format( + snapshot_name, + self.test_location.testname, + line_end="\n", + ) warnings.warn(warning_msg) snapshot_fossil = SnapshotFossil(location=snapshot_location) snapshot_fossil.add(Snapshot(name=snapshot_name, data=data)) diff --git a/tasks.py b/tasks.py index 6e3e3438..ea6bfb87 100644 --- a/tasks.py +++ b/tasks.py @@ -8,12 +8,17 @@ import benchmarks +def ctx_run(ctx, *args, **kwargs): + kwargs["pty"] = os.name == "posix" + return ctx.run(*args, **kwargs) + + @task def clean(ctx): """ Remove build files e.g. package, distributable, compiled etc. """ - ctx.run("rm -rf *.egg-info dist build __pycache__ .pytest_cache artifacts/*") + ctx_run(ctx, "rm -rf *.egg-info dist build __pycache__ .pytest_cache artifacts/*") @task @@ -29,10 +34,10 @@ def requirements(ctx, upgrade=False): ] if upgrade: args.append("--upgrade") - ctx.run( + ctx_run( + ctx, f"echo '-e .[dev]' | python -m piptools compile " f"{' '.join(args)} - -qo- | sed '/^-e / d' > dev_requirements.txt", - pty=True, ) @@ -51,7 +56,7 @@ def lint(ctx, fix=False): for section, command in lint_commands.items(): print(f"\033[1m[{section}]\033[0m") try: - ctx.run(command, pty=True) + ctx_run(ctx, command) except exceptions.Failure as ex: last_error = ex print() @@ -64,7 +69,7 @@ def install(ctx): """ Install the current development version of syrupy """ - ctx.run("python -m pip install -U .", pty=True) + ctx_run(ctx, "python -m pip install -U .") @task( @@ -95,12 +100,12 @@ def test( } coverage_module = "coverage run -m " if coverage else "" test_flags = " ".join(flag for flag, enabled in flags.items() if enabled) - ctx.run(f"python -m {coverage_module}pytest {test_flags} .", env=env, pty=True) + ctx_run(ctx, f"python -m {coverage_module}pytest {test_flags} .", env=env) if coverage: if not os.environ.get("CI"): - ctx.run("coverage report", pty=True) + ctx_run(ctx, "coverage report") else: - ctx.run("codecov", pty=True) + ctx_run(ctx, "codecov") @task(help={"report": "Publish report as github status"}) @@ -113,7 +118,7 @@ def build(ctx): """ Generate version from scm and build package distributable """ - ctx.run("python setup.py sdist bdist_wheel") + ctx_run(ctx, "python setup.py sdist bdist_wheel") @task @@ -122,7 +127,7 @@ def publish(ctx, dry_run=True): Upload built package to pypi """ repo_url = "--repository-url https://test.pypi.org/legacy/" if dry_run else "" - ctx.run(f"twine upload --skip-existing {repo_url} dist/*") + ctx_run(ctx, f"twine upload --skip-existing {repo_url} dist/*") @task(pre=[build]) @@ -137,7 +142,7 @@ def release(ctx, dry_run=True): exit(1) # get version created in build - with open("version.txt", "r") as f: + with open("version.txt", "r", encoding="utf-8") as f: version = str(f.read()) try: diff --git a/tests/__snapshots__/test_extension_amber.ambr b/tests/__snapshots__/test_extension_amber.ambr index 259c9636..ed4359e9 100644 --- a/tests/__snapshots__/test_extension_amber.ambr +++ b/tests/__snapshots__/test_extension_amber.ambr @@ -266,17 +266,36 @@ --- # name: test_newline_control_characters.1 ' - line 1 + line 1 line 2 ' --- # name: test_newline_control_characters.2 ' - line 1 - line 2 + line 1 + line 2 ' --- +# name: test_newline_control_characters.3 + ' + line 1 + line 2 + ' +--- +# name: test_newline_control_characters.4 + ' + line 1 + line 2 + + ' +--- +# name: test_newline_control_characters.5 + ' + line 1 + line 2 + ' +--- # name: test_non_deterministic_snapshots { 'a': UUID(...), diff --git a/tests/__snapshots__/test_extension_base.ambr b/tests/__snapshots__/test_extension_base.ambr index 3f5517c4..a2db256d 100644 --- a/tests/__snapshots__/test_extension_base.ambr +++ b/tests/__snapshots__/test_extension_base.ambr @@ -43,3 +43,29 @@ line 7 ' --- +# name: TestSnapshotReporter.test_diff_lines[-2-SnapshotReporterNoContext] + ' + - line 0␍ + + line 0␤ + ... + - line 2␍␤ + + line 2␤ + - line 3␤ + + line 3␍␤ + ... + ' +--- +# name: TestSnapshotReporter.test_diff_lines[-2-SnapshotReporter] + ' + - line 0␍ + + line 0␤ + line 1 + - line 2␍␤ + + line 2␤ + - line 3␤ + + line 3␍␤ + line 4 + ... + line 7 + ' +--- diff --git a/tests/__snapshots__/test_integration_default.ambr b/tests/__snapshots__/test_integration_default.ambr deleted file mode 100644 index a224ed1e..00000000 --- a/tests/__snapshots__/test_integration_default.ambr +++ /dev/null @@ -1,198 +0,0 @@ -# name: test_failing_snapshots_diff - ' - - ________________________________ test_updated_1 ________________________________ - - snapshot = SnapshotAssertion(name='snapshot', num_executions=1) - - def test_updated_1(snapshot): - > assert snapshot == ['this', 'will', 'not', 'match'] - E AssertionError: assert snapshot == received - E [ - E ... - E 'will', - E - 'be', - E - 'updated', - E + 'not', - E + 'match', - E ] - - test_file.py:17: AssertionError - ________________________________ test_updated_2 ________________________________ - - snapshot = SnapshotAssertion(name='snapshot', num_executions=1) - - def test_updated_2(snapshot): - > assert ['this', 'will', 'fail'] == snapshot - E AssertionError: assert received == snapshot - E [ - E ... - E 'will', - E - 'be', - E + 'fail', - E - 'updated', - E ] - - test_file.py:22: AssertionError - ________________________________ test_updated_3 ________________________________ - - snapshot = SnapshotAssertion(name='snapshot', num_executions=1) - - def test_updated_3(snapshot): - > assert snapshot == ['this', 'will', 'be', 'too', 'much'] - E AssertionError: assert snapshot == received - E [ - E ... - E 'be', - E - 'updated', - E + 'too', - E + 'much', - E ] - - test_file.py:27: AssertionError - ________________________________ test_updated_4 ________________________________ - - snapshot = SnapshotAssertion(name='snapshot', num_executions=1) - - def test_updated_4(snapshot): - > assert snapshot == "sing line changeling" - E AssertionError: assert snapshot == received - E - 'single line change' - E + 'sing line changeling' - - test_file.py:32: AssertionError - ________________________________ test_updated_5 ________________________________ - - snapshot = SnapshotAssertion(name='snapshot', num_executions=1) - - 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 show up because it changes\n - this line should show up because it changes color - and this line does not exist in the first one - ''' - E AssertionError: assert snapshot == received - E ' - E ... - E multiple line changes - E - with some lines staying the same - E + with some lines not staying the same - E - intermittent changes that have to be ignore by the differ output - E + intermittent changes so unchanged lines have to be ignored by the differ - E - because when there are a lot of changes you only want to see changes - E + cause when there are a lot of changes you only want to see what changed - E you do not want to see this line - E or this line - E - this line should show up because it changes␍␤ - E + this line should show up because it changes␤ - E - E - this line should show up because it changes color - E + this line should show up because it changes color - E + and this line does not exist in the first one - E - E ' - - test_file.py:37: AssertionError - --------------------------- snapshot report summary ---------------------------- - ' ---- -# name: test_generated_snapshots - '7 snapshots generated.' ---- -# name: test_missing_snapshots - '1 snapshot failed.' ---- -# name: test_removed_empty_snapshot_fossil_only - ' - 7 snapshots passed. 1 unused snapshot deleted. - - Deleted empty snapshot fossil (__snapshots__/test_empty.ambr) - ' ---- -# name: test_removed_snapshot_fossil - ' - 7 unused snapshots deleted. - - Deleted test_unused, test_updated_1, test_updated_2, test_updated_3, test_updated_4, test_updated_5, test_used (__snapshots__/test_file.ambr) - ' ---- -# name: test_removed_snapshots - ' - 6 snapshots passed. 1 unused snapshot deleted. - - Deleted test_unused (__snapshots__/test_file.ambr) - ' ---- -# name: test_snapshot_default_extension_option - '1 snapshot generated.' ---- -# name: test_unused_parameterized_ignored_if_not_targeted_using_dash_k - '4 snapshots generated.' ---- -# name: test_unused_parameterized_ignored_if_not_targeted_using_dash_k.1 - ' - 2 snapshots passed. 1 unused snapshot deleted. - - Deleted test_collected[3] (__snapshots__/test_collected.ambr) - ' ---- -# name: test_unused_snapshots - ' - 6 snapshots passed. 1 snapshot unused. - - Re-run pytest with --snapshot-update to delete unused snapshots. - ' ---- -# name: test_unused_snapshots_cleaned_up_when_targeting_specific_testfiles - ' - 7 unused snapshots deleted. - - Deleted test_unused, test_updated_1, test_updated_2, test_updated_3, test_updated_4, test_updated_5, test_used (__snapshots__/test_file.ambr) - ' ---- -# name: test_unused_snapshots_ignored_if_not_targeted_by_module_testfiles - ' - 6 snapshots passed. 1 unused snapshot deleted. - - Deleted test_unused (__snapshots__/test_file.ambr) - ' ---- -# name: test_unused_snapshots_ignored_if_not_targeted_by_testnode_ids - '1 snapshot passed.' ---- -# name: test_unused_snapshots_ignored_if_not_targeted_using_dash_k - '4 snapshots generated.' ---- -# name: test_unused_snapshots_ignored_if_not_targeted_using_dash_k.1 - ' - 1 snapshot passed. 1 snapshot updated. 1 unused snapshot deleted. - - Deleted test_collected[3] (__snapshots__/test_collected.ambr) - ' ---- -# name: test_unused_snapshots_ignored_if_not_targeted_using_dash_m - '4 snapshots generated.' ---- -# name: test_unused_snapshots_ignored_if_not_targeted_using_dash_m.1 - ' - 1 snapshot passed. 1 snapshot updated. 1 unused snapshot deleted. - - Deleted test_collected[3] (__snapshots__/test_collected.ambr) - ' ---- -# name: test_unused_snapshots_warning - ' - 6 snapshots passed. 1 snapshot unused. - - Re-run pytest with --snapshot-update to delete unused snapshots. - ' ---- -# name: test_updated_snapshots - '2 snapshots passed. 5 snapshots updated.' ---- diff --git a/tests/test_extension_amber.py b/tests/test_extension_amber.py index d5a0573a..02a3c008 100644 --- a/tests/test_extension_amber.py +++ b/tests/test_extension_amber.py @@ -25,6 +25,9 @@ def test_newline_control_characters(snapshot): assert snapshot == "line 1\nline 2" assert snapshot == "line 1\r\nline 2" assert snapshot == "line 1\r\nline 2\r\n" + assert snapshot == "line 1\rline 2\r" + assert snapshot == "line 1\rline 2\n" + assert snapshot == "line 1\rline 2" def test_multiline_string_in_dict(snapshot): diff --git a/tests/test_extension_base.py b/tests/test_extension_base.py index b2a363cc..4c5e973a 100644 --- a/tests/test_extension_base.py +++ b/tests/test_extension_base.py @@ -22,6 +22,10 @@ class TestSnapshotReporter: "line 0\nline 1\nline 2\nline 3\t\nline 4\nline 5\nline 6\nline 7", "line 0\nline 1\nline 2\nline 3 \nline 4\nline 5\nline 6\nline 7", ), + ( + "line 0\nline 1\nline 2\nline 3\r\nline 4\nline 5\nline 6\nline 7", + "line 0\rline 1\nline 2\r\nline 3\nline 4\nline 5\nline 6\nline 7", + ), ], ids=lambda _: "", ) diff --git a/tests/test_integration_default.py b/tests/test_integration_default.py index 775cd9a7..941be0e1 100644 --- a/tests/test_integration_default.py +++ b/tests/test_integration_default.py @@ -3,15 +3,8 @@ import pytest -def get_result_snapshot_summary(result: object) -> str: - result_stdout = result.stdout.str() - start_idx = result_stdout.find("\n", result_stdout.find("---- snapshot report")) - end_idx = result_stdout.find("====", start_idx) - return result_stdout[start_idx:end_idx].strip() - - @pytest.fixture -def collection(testdir, snapshot): +def collection(testdir): tests = { "test_collected": ( """ @@ -31,12 +24,12 @@ def test_collected1(snapshot): } testdir.makepyfile(**tests) result = testdir.runpytest("-v", "--snapshot-update") - assert get_result_snapshot_summary(result) == snapshot - testdir.makefile(".ambr", **{"__snapshots__/other_snapfile": ""}) + 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, snapshot): +def test_unused_snapshots_ignored_if_not_targeted_using_dash_m(collection): updated_tests = { "test_collected": ( """ @@ -50,13 +43,18 @@ def test_collected(snapshot, actual): } collection.makepyfile(**updated_tests) result = collection.runpytest("-v", "--snapshot-update", "-m", "parametrize") - assert get_result_snapshot_summary(result) == snapshot + 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).joinpath("test_not_collected.ambr").exists() - assert Path(*snapshot_path).joinpath("other_snapfile.ambr").exists() + 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, snapshot): +def test_unused_snapshots_ignored_if_not_targeted_using_dash_k(collection): updated_tests = { "test_collected": ( """ @@ -70,15 +68,18 @@ def test_collected(snapshot, actual): } collection.makepyfile(**updated_tests) result = collection.runpytest("-v", "--snapshot-update", "-k", "test_collected[") - assert get_result_snapshot_summary(result) == snapshot + 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).joinpath("test_not_collected.ambr").exists() - assert Path(*snapshot_path).joinpath("other_snapfile.ambr").exists() + 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, snapshot -): +def test_unused_parameterized_ignored_if_not_targeted_using_dash_k(collection): updated_tests = { "test_collected": ( """ @@ -92,10 +93,15 @@ def test_collected(snapshot, actual): } collection.makepyfile(**updated_tests) result = collection.runpytest("-v", "--snapshot-update", "-k", "test_collected[") - assert get_result_snapshot_summary(result) == snapshot + 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).joinpath("test_not_collected.ambr").exists() - assert Path(*snapshot_path).joinpath("other_snapfile.ambr").exists() + 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): @@ -218,7 +224,7 @@ def test_updated_5(snapshot): 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 show up because it changes\\r\\n + 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 ''' """ @@ -263,7 +269,7 @@ def test_updated_5(snapshot): 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 show up because it changes\\n + 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 ''' @@ -273,10 +279,10 @@ def test_updated_5(snapshot): return {**testcases, **updated_testcases} -def test_missing_snapshots(testdir, testcases, snapshot): +def test_missing_snapshots(testdir, testcases): testdir.makepyfile(test_file=testcases["used"]) result = testdir.runpytest("-v") - assert get_result_snapshot_summary(result) == snapshot + result.stdout.re_match_lines((r"1 snapshot failed\.")) assert result.ret == 1 @@ -294,49 +300,110 @@ def test_injected_fixture(stubs): assert result.ret == 0 -def test_generated_snapshots(stubs, snapshot): +def test_generated_snapshots(stubs): result = stubs[0] - assert get_result_snapshot_summary(result) == snapshot + result.stdout.re_match_lines((r"7 snapshots generated\.")) assert result.ret == 0 -def test_failing_snapshots_diff(stubs, testcases_updated, snapshot): +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 = result.stdout.str() - start_index = result_stdout.find("\n", result_stdout.find("==== FAILURES")) - end_index = result_stdout.find("\n", result_stdout.find("---- snapshot report")) - assert result_stdout[start_index:end_index] == snapshot + 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, snapshot): +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") - assert get_result_snapshot_summary(result) == snapshot + result.stdout.re_match_lines((r"2 snapshots passed\. 5 snapshots updated\.")) assert result.ret == 0 -def test_unused_snapshots(stubs, snapshot): +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") - assert get_result_snapshot_summary(result) == snapshot + 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, snapshot): +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") - assert get_result_snapshot_summary(result) == snapshot + 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, snapshot): - testdir.makefile(".ambr", **{"__snapshots__/other_snapfile": ""}) +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=( @@ -354,23 +421,30 @@ def test_clever_girl(snapshot): result = testdir.runpytest( f"{testfile}::test_life_always_finds_a_way", "-v", "--snapshot-update" ) - assert get_result_snapshot_summary(result) == snapshot + result.stdout.re_match_lines((r"1 snapshot passed\.")) assert result.ret == 0 - assert Path("__snapshots__/other_snapfile.ambr").exists() + assert Path(f"{path_to_snap}.ambr").exists() -def test_unused_snapshots_ignored_if_not_targeted_by_module_testfiles(stubs, snapshot): +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", **{"__snapshots__/other_snapfile": ""}) + testdir.makefile(".ambr", **{str(path_to_snap): ""}) result = testdir.runpytest("-v", "--snapshot-update", "test_file.py") - assert get_result_snapshot_summary(result) == snapshot + 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("__snapshots__/other_snapfile.ambr").exists() + assert Path(f"{path_to_snap}.ambr").exists() -def test_unused_snapshots_cleaned_up_when_targeting_specific_testfiles(stubs, snapshot): +def test_unused_snapshots_cleaned_up_when_targeting_specific_testfiles(stubs): _, testdir, _, _ = stubs + path_to_snap = Path("__snapshots__", "other_snapfile") testdir.makepyfile( test_file=( """ @@ -379,49 +453,75 @@ def test_used(snapshot): """ ), ) - testdir.makefile(".ambr", **{"__snapshots__/other_snapfile": ""}) + testdir.makefile(".ambr", **{str(path_to_snap): ""}) result = testdir.runpytest("-v", "--snapshot-update", "test_file.py") - assert get_result_snapshot_summary(result) == snapshot + 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("__snapshots__/other_snapfile.ambr").exists() + assert Path(f"{path_to_snap}.ambr").exists() -def test_removed_snapshots(stubs, snapshot): +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") - assert get_result_snapshot_summary(result) == snapshot + 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, snapshot): +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") - assert get_result_snapshot_summary(result) == snapshot + 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, snapshot): +def test_removed_empty_snapshot_fossil_only(stubs): _, testdir, _, filepath = stubs - testdir.makefile(".ambr", **{"__snapshots__/test_empty": ""}) - empty_filepath = Path(testdir.tmpdir, "__snapshots__/test_empty.ambr") + 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") - assert get_result_snapshot_summary(result) == snapshot + 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, snapshot): +def test_removed_hanging_snapshot_fossil(stubs): _, testdir, _, filepath = stubs - testdir.makefile(".abc", **{"__snapshots__/test_hanging": ""}) - hanging_filepath = Path(testdir.tmpdir, "__snapshots__/test_hanging.abc") + 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() @@ -434,7 +534,7 @@ def test_removed_hanging_snapshot_fossil(stubs, snapshot): assert not Path(hanging_filepath).exists() -def test_snapshot_default_extension_option(testdir, snapshot): +def test_snapshot_default_extension_option(testdir): testdir.makepyfile( test_file=( """ @@ -449,12 +549,14 @@ def test_default(snapshot): "--snapshot-default-extension", "syrupy.extensions.single_file.SingleFileSnapshotExtension", ) - assert get_result_snapshot_summary(result) == snapshot - assert Path(testdir.tmpdir, "__snapshots__/test_file/test_default.raw").exists() + 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, snapshot): +def test_snapshot_default_extension_option_failure(testdir, testcases): testdir.makepyfile(test_file=testcases["used"]) result = testdir.runpytest( "-v", diff --git a/tests/test_integration_single_file.py b/tests/test_integration_single_file.py index 4c9cdf6f..b7bb0eb6 100644 --- a/tests/test_integration_single_file.py +++ b/tests/test_integration_single_file.py @@ -62,11 +62,11 @@ def test_passed_single(snapshot_single): return {**testcases, **updated_testcases} -def test_unsaved_snapshots(snapshot, testdir, testcases): +def test_unsaved_snapshots(testdir, testcases): testdir.makepyfile(test_file=testcases["passed"]) result = testdir.runpytest("-v") output = result.stdout.str() - assert "Snapshot does not exist" in output + assert "Snapshot 'test_passed_single' does not exist" in output assert "+ b'passed1'" in output assert result.ret == 1 diff --git a/tests/test_utils.py b/tests/test_utils.py index 77896264..b54ac00b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -40,9 +40,13 @@ def testfiles(testdir): def test_walk_dir_skips_non_snapshot_path(testfiles): _, testdir = testfiles + snap_folder = Path("__snapshots__") assert { str(Path(p).relative_to(Path.cwd())) for p in walk_snapshot_dir(testdir.tmpdir) - } == {"__snapshots__/snapfile1.ambr", "__snapshots__/snapfolder/snapfile2.svg"} + } == { + str(snap_folder.joinpath("snapfile1.ambr")), + str(snap_folder.joinpath("snapfolder", "snapfile2.svg")), + } def dummy_member():