Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: pretty diff of failed snapshot assertions #86

Merged
merged 13 commits into from
Jan 5, 2020
26 changes: 16 additions & 10 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,33 @@
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --allow-unsafe --no-emit-find-links --no-index requirements.in
# pip-compile --allow-unsafe --no-emit-find-links --no-index --output-file=- -
#
appdirs==1.4.3 # via black
attrs==19.3.0 # via black, flake8-bugbear, pytest
black==19.10b0
bleach==3.1.0 # via readme-renderer
certifi==2019.11.28 # via requests
cffi==1.13.2 # via cryptography
chardet==3.0.4 # via requests
click==7.0 # via black, pip-tools
codecov==2.0.15
colored==1.4.2
configparser==4.0.2 # via py-githooks
coverage[toml]==5.0.1
coverage[toml]==5.0.1 # via codecov
cryptography==2.8 # via secretstorage
docutils==0.15.2 # via readme-renderer
entrypoints==0.3 # via flake8
flake8-bugbear==19.8.0
flake8-bugbear==20.1.0
flake8-builtins==1.4.2
flake8-comprehensions==3.1.4
flake8-i18n==0.1.0
flake8==3.7.9
flake8==3.7.9 # via flake8-bugbear, flake8-builtins, flake8-comprehensions, flake8-i18n
idna==2.8 # via requests
importlib-metadata==1.3.0 # via flake8-comprehensions, keyring, pluggy, pytest, twine
invoke==1.3.0
invoke==1.4.0
isort==4.3.21
jeepney==0.4.2 # via secretstorage
keyring==21.0.0 # via twine
mccabe==0.6.1 # via flake8
more-itertools==8.0.2 # via pytest, zipp
Expand All @@ -38,6 +42,7 @@ pluggy==0.13.1 # via pytest
py-githooks==1.1.0
py==1.8.1 # via pytest
pycodestyle==2.5.0 # via flake8
pycparser==2.19 # via cffi
pyflakes==2.1.1 # via flake8
pygments==2.5.2 # via readme-renderer
pyparsing==2.4.6 # via packaging
Expand All @@ -46,18 +51,19 @@ readme-renderer==24.0 # via twine
regex==2019.12.20 # via black
requests-toolbelt==0.9.1 # via twine
requests==2.22.0 # via codecov, requests-toolbelt, twine
secretstorage==3.1.1 # via keyring
semver==2.9.0
six==1.13.0 # via bleach, packaging, pip-tools, readme-renderer
six==1.13.0 # via bleach, cryptography, packaging, pip-tools, readme-renderer
toml==0.10.0 # via black, coverage
tqdm==4.41.0 # via twine
tqdm==4.41.1 # via twine
twine==3.1.1
typed-ast==1.4.0 # via black, mypy
typing-extensions==3.7.4.1
typing-extensions==3.7.4.1 # via mypy
urllib3==1.25.7 # via requests
wcwidth==0.1.7 # via pytest
wcwidth==0.1.8 # via pytest
webencodings==0.5.1 # via bleach
wheel==0.33.6
zipp==0.6.0 # via importlib-metadata

# The following packages are considered to be unsafe in a requirements file:
setuptools==42.0.2 # via twine
setuptools==44.0.0 # via twine
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

python_requires = "~=3.6"
setup_requires = ["setuptools_scm"]
install_requires = ["typing_extensions>=3.6"]
install_requires = ["colored", "typing_extensions>=3.6"]
dev_requires = [
"black",
"codecov",
Expand Down
9 changes: 7 additions & 2 deletions src/syrupy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
from .location import TestLocation
from .serializers import DEFAULT_SERIALIZER
from .session import SnapshotSession
from .terminal import (
green,
red,
reset,
)


def pytest_addoption(parser: Any) -> None:
Expand Down Expand Up @@ -40,10 +45,10 @@ def pytest_assertrepr_compare(op: str, left: Any, right: Any) -> Optional[List[s
https://docs.pytest.org/en/latest/reference.html#_pytest.hookspec.pytest_assertrepr_compare
"""
if isinstance(left, SnapshotAssertion):
assert_msg = f"{left.name} {op} {right}"
assert_msg = reset(f"{green(left.name)} {op} {red('received')}")
return [assert_msg] + left.get_assert_diff(right)
elif isinstance(right, SnapshotAssertion):
assert_msg = f"{left} {op} {right.name}"
assert_msg = reset(f"{red('received')} {op} {green(right.name)}")
return [assert_msg] + right.get_assert_diff(left)
return None

Expand Down
29 changes: 2 additions & 27 deletions src/syrupy/assertion.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from gettext import gettext
from itertools import zip_longest
from typing import (
TYPE_CHECKING,
Dict,
Expand All @@ -16,12 +15,6 @@
SnapshotFiles,
)
from .exceptions import SnapshotDoesNotExist
from .terminal import (
error_style,
green,
red,
success_style,
)
from .utils import walk_snapshot_dir


Expand Down Expand Up @@ -111,27 +104,9 @@ def get_assert_diff(self, data: "SerializableData") -> List[str]:
if snapshot_data is None:
return [gettext("Snapshot does not exist!")]

diff = []
diff: List[str] = []
if not assertion_result.success:
received = serialized_data.splitlines()
stored = snapshot_data.splitlines()

marker_stored = success_style("-")
marker_received = error_style("+")

for received_line, stored_line in zip_longest(received, stored):
if received_line is None:
diff.append(f"{marker_stored} {green(stored_line)}")
elif stored_line is None:
diff.append(f"{marker_received} {red(received_line)}")
elif received_line != stored_line:
diff.extend(
[
f"{marker_stored} {green(stored_line)}",
f"{marker_received} {red(received_line)}",
]
)

diff.extend(self.serializer.diff_lines(serialized_data, snapshot_data))
return diff

def __repr__(self) -> str:
Expand Down
2 changes: 1 addition & 1 deletion src/syrupy/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def lines(self) -> Generator[str, None, None]:
snapshots = (snapshot.name for snapshot in snapshot_file)
path_to_file = os.path.relpath(filepath, self.base_dir)
deleted_snapshots = ", ".join(map(bold, sorted(snapshots)))
yield gettext("Deleted {} ({})").format(
yield warning_style(gettext("Deleted {} ({})")).format(
deleted_snapshots, path_to_file
)
else:
Expand Down
44 changes: 44 additions & 0 deletions src/syrupy/serializers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
ABC,
abstractmethod,
)
from difflib import ndiff
iamogbz marked this conversation as resolved.
Show resolved Hide resolved
from itertools import zip_longest
from typing import (
TYPE_CHECKING,
Callable,
Generator,
Optional,
Set,
Union,
)

from typing_extensions import final
Expand All @@ -18,6 +23,13 @@
SnapshotFile,
)
from syrupy.exceptions import SnapshotDoesNotExist
from syrupy.terminal import (
emphasize,
green,
mute,
red,
reset,
)


if TYPE_CHECKING:
Expand Down Expand Up @@ -182,3 +194,35 @@ def _write_snapshot_to_file(self, snapshot_file: "SnapshotFile") -> None:
Adds the snapshot data to the snapshots read from the file
"""
raise NotImplementedError

def diff_lines(
self, serialized_data: "SerializedData", snapshot_data: "SerializedData"
) -> Generator[str, None, None]:
for line in self.__diff_lines(str(snapshot_data), str(serialized_data)):
yield reset(line)

def __diff_lines(self, a: str, b: str) -> Generator[str, None, None]:
line_styler = {"-": green, "+": red}
staged_line, skip = "", False
for line in ndiff(a.splitlines(), b.splitlines()):
if staged_line and line[:1] != "?":
yield line_styler[staged_line[:1]](staged_line)
staged_line, skip = "", False
if line[:1] in "-+":
staged_line = line
elif line[:1] == "?":
yield self.__diff_line(line, staged_line, line_styler[staged_line[:1]])
staged_line, skip = "", False
elif not skip:
yield mute(" ...")
skip = True
if staged_line:
yield line_styler[staged_line[:1]](staged_line)

def __diff_line(
self, marker_line: str, line: str, line_style: Callable[[Union[str, int]], str]
) -> str:
return "".join(
emphasize(line_style(char)) if str(marker) in "-+^" else line_style(char)
for marker, char in zip_longest(marker_line.strip(), line)
)
22 changes: 18 additions & 4 deletions src/syrupy/terminal.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
from typing import Union

import colored


def reset(text: Union[str, int]) -> str:
return colored.stylize(text, colored.attr("reset"))


def red(text: Union[str, int]) -> str:
return f"\033[31m{text}\033[0m"
return colored.stylize(text, colored.fg("red"))


def yellow(text: Union[str, int]) -> str:
return f"\033[33m{text}\033[0m"
return colored.stylize(text, colored.fg("yellow"))


def green(text: Union[str, int]) -> str:
return f"\033[32m{text}\033[0m"
return colored.stylize(text, colored.fg("green"))


def bold(text: Union[str, int]) -> str:
return f"\033[1m{text}\033[0m"
return colored.stylize(text, colored.attr("bold"))


def mute(text: Union[str, int]) -> str:
return colored.stylize(text, colored.attr("dim"))


def emphasize(text: Union[str, int]) -> str:
return colored.stylize(bold(text), colored.attr("underlined"))


def error_style(text: Union[str, int]) -> str:
Expand Down
6 changes: 6 additions & 0 deletions stubs/colored.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from typing import Union

def attr(color: Union[str, int]) -> str: ...
def bg(color: Union[str, int]) -> str: ...
def fg(color: Union[str, int]) -> str: ...
def stylize(text: Union[str, int], style: str, reset: bool = True) -> str: ...
83 changes: 83 additions & 0 deletions tests/__snapshots__/test_integration.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# name: test_failing_snapshots_diff
'

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 - 'be',
E - 'updated',
E + 'not',
E + 'match',
E ...

test_file.py:17: AssertionError

snapshot = SnapshotAssertion(name='snapshot', num_executions=1)

def test_updated_2(snapshot):
> assert ['this', 'will', 'fail'] == snapshot
E AssertionError: assert received == snapshot
E ...
E + 'fail',
E - 'be',
E - 'updated',
E ...

test_file.py:22: AssertionError

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 - 'updated',
E + 'too',
E + 'much',
E ...

test_file.py:27: AssertionError

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

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 color
and this line does not exist in the first one
'''
E assert snapshot == received
E ...
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 ...
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 ...

test_file.py:37: AssertionError
'
---
Loading