diff --git a/README.md b/README.md index 5177b1fb..b7c6be91 100644 --- a/README.md +++ b/README.md @@ -132,12 +132,13 @@ Both options will generate equivalent snapshots but the latter is only viable wh These are the cli options exposed to `pytest` by the plugin. | Option | Description | Default | -| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | +| ------------------------------ |--------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------| | `--snapshot-update` | Snapshots will be updated to match assertions and unused snapshots will be deleted. | `False` | | `--snapshot-details` | Includes details of unused snapshots (test name and snapshot location) in the final report. | `False` | | `--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` | ### Assertion Options @@ -470,6 +471,21 @@ 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. + +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. + + ## Uninstalling ```python diff --git a/src/syrupy/__init__.py b/src/syrupy/__init__.py index 3c42f7fe..1899fb2d 100644 --- a/src/syrupy/__init__.py +++ b/src/syrupy/__init__.py @@ -1,10 +1,16 @@ import argparse import sys -from functools import lru_cache +import warnings +from functools import ( + lru_cache, + wraps, +) from gettext import gettext +from inspect import signature from typing import ( Any, ContextManager, + Iterable, List, Optional, ) @@ -85,6 +91,13 @@ def pytest_addoption(parser: Any) -> None: dest="no_colors", help="Disable test results output highlighting", ) + group.addoption( + "--snapshot-patch-pycharm-diff", + action="store_true", + default=False, + dest="patch_pycharm_diff", + help="Patch Pycharm diff", + ) def __terminal_color(config: Any) -> "ContextManager[None]": @@ -192,3 +205,66 @@ def snapshot(request: Any) -> "SnapshotAssertion": test_location=PyTestLocation(request.node), session=request.session.config._syrupy, ) + + +@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, + ) + 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