Skip to content

Commit

Permalink
chore: add test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
noahnu committed Aug 23, 2024
1 parent a4dfcf5 commit 7d4c750
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 78 deletions.
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ These are the cli options exposed to `pytest` by the plugin.
| `--snapshot-warn-unused` | Prints a warning on unused snapshots rather than fail the test suite. | `False` |
| `--snapshot-default-extension` | Use to change the default snapshot extension class. | [AmberSnapshotExtension](https://github.com/syrupy-project/syrupy/blob/main/src/syrupy/extensions/amber/__init__.py) |
| `--snapshot-no-colors` | Disable test results output highlighting. Equivalent to setting the environment variables `ANSI_COLORS_DISABLED` or `NO_COLOR` | Disabled by default if not in terminal. |
| `--snapshot-patch-pycharm-diff`| Override Pycharm's default diffs viewer when looking at snapshot diffs. More information in [Using Syrupy from Pycharm] | `False` |
| `--snapshot-patch-pycharm-diff`| Override PyCharm's default diffs viewer when looking at snapshot diffs. See [IDE Integrations](#ide-integrations) | `False` |

### Assertion Options

Expand Down Expand Up @@ -471,20 +471,20 @@ The generated snapshot:
- [JPEG image extension](https://github.com/syrupy-project/syrupy/tree/main/tests/examples/test_custom_image_extension.py)
- [Built-in image extensions](https://github.com/syrupy-project/syrupy/blob/main/tests/syrupy/extensions/image/test_image_svg.py)

### Viewing Snapshot Diffs in Pycharm IDEs
Pycharm IDEs come with a built-in tool that helps you to more easily identify differences between the expected result and the actual result in a test.
However, this tool does not play nicely with syrupy snapshots by default.
## IDE Integrations

### PyCharm

The [PyCharm](https://www.jetbrains.com/pycharm/) IDE comes with a built-in tool for visualizing differences between expected and actual results in a test. To properly render Syrupy snapshots in the PyCharm diff viewer, we need to apply a patch to the diff viewer library. To do this, use the `--snapshot-patch-pycharm-diff` flag, e.g.:

In your `pytest.ini`:

Fortunately, Syrupy comes with a runtime flag that will extend Pycharm's default behavior to work nicely with snapshots.
Pass the `--snapshot-patch-pycharm-diff` flag in your pytest run configuration or create a `pytest.ini` in your project with the following content:
```ini
[pytest]
addopts = --snapshot-patch-pycharm-diff

```

Now you will be able to see snapshot diffs more easily.

See [#675](https://github.com/syrupy-project/syrupy/issues/675) for the original issue.

## Uninstalling

Expand Down
80 changes: 11 additions & 69 deletions src/syrupy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import argparse
import sys
import warnings
from functools import (
lru_cache,
wraps,
)
from functools import lru_cache
from gettext import gettext
from inspect import signature
from typing import (
Any,
ContextManager,
Iterable,
Iterator,
List,
Optional,
)
Expand All @@ -22,6 +17,7 @@
from .exceptions import FailedToLoadModuleMember
from .extensions import DEFAULT_EXTENSION
from .location import PyTestLocation
from .patches.pycharm_diff import patch_pycharm_diff
from .session import SnapshotSession
from .terminal import (
received_style,
Expand Down Expand Up @@ -96,7 +92,7 @@ def pytest_addoption(parser: Any) -> None:
action="store_true",
default=False,
dest="patch_pycharm_diff",
help="Patch Pycharm diff",
help="Patch PyCharm diff",
)


Expand Down Expand Up @@ -198,73 +194,19 @@ def pytest_terminal_summary(


@pytest.fixture
def snapshot(request: Any) -> "SnapshotAssertion":
def snapshot(request: "pytest.FixtureRequest") -> "SnapshotAssertion":
return SnapshotAssertion(
update_snapshots=request.config.option.update_snapshots,
extension_class=__import_extension(request.config.option.default_extension),
test_location=PyTestLocation(request.node),
session=request.session.config._syrupy,
session=request.session.config._syrupy, # type: ignore
)


@pytest.fixture(scope="session", autouse=True)
def _patch_pycharm_diff_viewer_for_snapshots(request: Any) -> Iterable[None]:
if not request.config.option.patch_pycharm_diff:
yield
return

try:
from teamcity.diff_tools import EqualsAssertionError # type: ignore
except ImportError:
warnings.warn(
"Pycharm's diff tools have failed to be imported. "
"Snapshot diffs will not be patched.",
stacklevel=2,
)
def _syrupy_apply_ide_patches(request: "pytest.FixtureRequest") -> Iterator[None]:
if request.config.option.patch_pycharm_diff:
with patch_pycharm_diff():
yield
else:
yield
return

old_init = EqualsAssertionError.__init__
old_init_signature = signature(old_init)

@wraps(old_init)
def new_init(self: EqualsAssertionError, *args: Any, **kwargs: Any) -> None:

# Extract the __init__ arguments as originally passed in order to
# process them later
parameters = old_init_signature.bind(self, *args, **kwargs)
parameters.apply_defaults()
expected = parameters.arguments["expected"]
actual = parameters.arguments["actual"]
real_exception = parameters.arguments["real_exception"]

if isinstance(expected, SnapshotAssertion):
snapshot = expected
elif isinstance(actual, SnapshotAssertion):
snapshot = actual
else:
snapshot = None

old_init(self, *args, **kwargs)

# No snapshot was involved in the assertion. Let the old logic do its
# thing.
if snapshot is None:
return

# Although a snapshot was involved in the assertion, it seems the error
# was a result of a non-assertion exception (Ex. `assert 1/0`).
# Therefore, We will not do anything here either.
if real_exception is not None:
return

assertion_result = snapshot.executions[snapshot.num_executions - 1]
if assertion_result.exception is not None:
return

self.expected = str(assertion_result.recalled_data)
self.actual = str(assertion_result.asserted_data)

EqualsAssertionError.__init__ = new_init
yield
EqualsAssertionError.__init__ = old_init
Empty file added src/syrupy/patches/__init__.py
Empty file.
76 changes: 76 additions & 0 deletions src/syrupy/patches/pycharm_diff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import warnings
from contextlib import contextmanager
from functools import wraps
from inspect import signature
from typing import (
Any,
Iterator,
)

from syrupy.assertion import SnapshotAssertion


@contextmanager
def patch_pycharm_diff() -> Iterator[None]:
"""
Applies PyCharm diff patch to add Syrupy snapshot support.
See: https://github.com/syrupy-project/syrupy/issues/675
"""

try:
from teamcity.diff_tools import EqualsAssertionError # type: ignore
except ImportError:
warnings.warn(
"Failed to patch PyCharm's diff tools. Skipping patch.",
stacklevel=2,
)
yield
return

old_init = EqualsAssertionError.__init__
old_init_signature = signature(old_init)

@wraps(old_init)
def new_init(self: "EqualsAssertionError", *args: Any, **kwargs: Any) -> None:

# Extract the __init__ arguments as originally passed in order to
# process them later
parameters = old_init_signature.bind(self, *args, **kwargs)
parameters.apply_defaults()

expected = parameters.arguments["expected"]
actual = parameters.arguments["actual"]
real_exception = parameters.arguments["real_exception"]

if isinstance(expected, SnapshotAssertion):
snapshot = expected
elif isinstance(actual, SnapshotAssertion):
snapshot = actual

Check warning on line 48 in src/syrupy/patches/pycharm_diff.py

View check run for this annotation

Codecov / codecov/patch

src/syrupy/patches/pycharm_diff.py#L47-L48

Added lines #L47 - L48 were not covered by tests
else:
snapshot = None

Check warning on line 50 in src/syrupy/patches/pycharm_diff.py

View check run for this annotation

Codecov / codecov/patch

src/syrupy/patches/pycharm_diff.py#L50

Added line #L50 was not covered by tests

old_init(self, *args, **kwargs)

# No snapshot was involved in the assertion. Let the old logic do its
# thing.
if snapshot is None:
return

Check warning on line 57 in src/syrupy/patches/pycharm_diff.py

View check run for this annotation

Codecov / codecov/patch

src/syrupy/patches/pycharm_diff.py#L57

Added line #L57 was not covered by tests

# Although a snapshot was involved in the assertion, it seems the error
# was a result of a non-assertion exception (Ex. `assert 1/0`).
# Therefore, We will not do anything here either.
if real_exception is not None:
return

Check warning on line 63 in src/syrupy/patches/pycharm_diff.py

View check run for this annotation

Codecov / codecov/patch

src/syrupy/patches/pycharm_diff.py#L63

Added line #L63 was not covered by tests

assertion_result = snapshot.executions[snapshot.num_executions - 1]
if assertion_result.exception is not None:
return

Check warning on line 67 in src/syrupy/patches/pycharm_diff.py

View check run for this annotation

Codecov / codecov/patch

src/syrupy/patches/pycharm_diff.py#L67

Added line #L67 was not covered by tests

self.expected = str(assertion_result.recalled_data)
self.actual = str(assertion_result.asserted_data)

try:
EqualsAssertionError.__init__ = new_init
yield
finally:
EqualsAssertionError.__init__ = old_init
126 changes: 126 additions & 0 deletions tests/integration/test_pycharm_patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from pathlib import Path

import pytest


# EqualsAssertionError comes from:
# https://github.com/JetBrains/intellij-community/blob/cd9bfbd98a7dca730fbc469156ce1ed30364afba/python/helpers/pycharm/teamcity/diff_tools.py#L53
@pytest.fixture
def mock_teamcity_diff_tools(testdir: "pytest.Testdir"):
teamcity_pkg = testdir.mkpydir("teamcity")
diff_tools_file = teamcity_pkg / Path("diff_tools.py")
diff_tools_file.write_text(
"""
class EqualsAssertionError:
def __init__(self, expected, actual, msg=None, preformated=False, real_exception=None): # noqa: E501
self.real_exception = real_exception
self.expected = expected
self.actual = actual
self.msg = str(msg)
""",
"utf-8",
)


@pytest.mark.filterwarnings("default")
def test_logs_a_warning_if_unable_to_apply_patch(testdir):
testdir.makepyfile(
test_file="""
def test_case(snapshot):
assert snapshot == [1, 2]
"""
)
testdir.runpytest("-v", "--snapshot-update")
testdir.makepyfile(
test_file="""
def test_case(snapshot):
assert snapshot == [1, 2, 3]
"""
)

result = testdir.runpytest("-v", "--snapshot-patch-pycharm-diff")
result.assert_outcomes(failed=1, passed=0, warnings=1)


@pytest.mark.filterwarnings("default")
def test_patches_pycharm_diff_tools_when_flag_set(testdir, mock_teamcity_diff_tools):
# Generate initial snapshot
testdir.makepyfile(
test_file="""
def test_case(snapshot):
assert snapshot == [1, 2]
"""
)
testdir.runpytest("-v", "--snapshot-update")

# Generate diff and mimic EqualsAssertionError being thrown
testdir.makepyfile(
test_file="""
def test_case(snapshot):
try:
assert snapshot == [1, 2, 3]
except:
from teamcity.diff_tools import EqualsAssertionError
err = EqualsAssertionError(expected=snapshot, actual=[1,2,3])
print("Expected:", repr(err.expected))
print("Actual:", repr(err.actual))
raise
"""
)

result = testdir.runpytest("-v", "--snapshot-patch-pycharm-diff")
# No warnings because patch should have been successful
result.assert_outcomes(failed=1, passed=0, warnings=0)

result.stdout.re_match_lines(
[
r"Expected: 'list([\n 1,\n 2,\n])'",
# Actual is the amber-style list representation
r"Actual: 'list([\n 1,\n 2,\n 3,\n])'",
]
)


@pytest.mark.filterwarnings("default")
def test_it_does_not_patch_pycharm_diff_tools_by_default(
testdir, mock_teamcity_diff_tools
):
# Generate initial snapshot
testdir.makepyfile(
test_file="""
def test_case(snapshot):
assert snapshot == [1, 2]
"""
)
testdir.runpytest("-v", "--snapshot-update")

# Generate diff and mimic EqualsAssertionError being thrown
testdir.makepyfile(
test_file="""
def test_case(snapshot):
try:
assert snapshot == [1, 2, 3]
except:
from teamcity.diff_tools import EqualsAssertionError
err = EqualsAssertionError(expected=snapshot, actual=[1,2,3])
print("Expected:", repr(str(err.expected)))
print("Actual:", repr(str(err.actual)))
raise
"""
)

result = testdir.runpytest("-v")
# No warnings because patch should have been successful
result.assert_outcomes(failed=1, passed=0, warnings=0)

result.stdout.re_match_lines(
[
r"Expected: 'list([\n 1,\n 2,\n])'",
# Actual is the original list's repr. No newlines or amber-style list prefix
r"Actual: '[1, 2, 3]'",
]
)

0 comments on commit 7d4c750

Please sign in to comment.