Skip to content

Commit

Permalink
Merge pull request #605 from tophat/next
Browse files Browse the repository at this point in the history
Graduate Syrupy v4 pre-release.
  • Loading branch information
noahnu committed Feb 2, 2023
2 parents 02abef5 + 6385979 commit 5eee3d8
Show file tree
Hide file tree
Showing 52 changed files with 1,058 additions and 558 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/cicd.yml
Expand Up @@ -31,7 +31,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11-dev']
python-version: ['3.8', '3.9', '3.10', '3.11-dev']
fail-fast: true
steps:
- uses: actions/checkout@v3.1.0
Expand Down
24 changes: 24 additions & 0 deletions CONTRIBUTING.md
Expand Up @@ -91,6 +91,30 @@ Fill in the relevant sections, clearly linking the issue the change is attemping

`debugpy` is installed in local development. A VSCode launch config is provided. Run `inv test -v -d` to enable the debugger (`-d` for debug). It'll then wait for you to attach your VSCode debugging client.

#### Debugging Performance Issues

You can run `inv benchmark` to run the full benchmark suite. Alternatively, write a test file, e.g.:

```py
# test_performance.py
import pytest
import os

SIZE = int(os.environ.get("SIZE", 1000))

@pytest.mark.parametrize("x", range(SIZE))
def test_performance(x, snapshot):
assert x == snapshot
```

and then run:

```sh
SIZE=1000 python -m cProfile -s cumtime -m pytest test_performance.py --snapshot-update -s > profile.log
```

See the cProfile docs for metric sorting options.

## Styleguides

### Commit Messages
Expand Down
11 changes: 11 additions & 0 deletions README.md
Expand Up @@ -37,6 +37,17 @@ pip uninstall snapshottest -y;
find . -type d ! -path '*/\.*' -name 'snapshots' | xargs rm -r
```

### Pytest and Python Compatibility

Syrupy will always be compatible with the latest version of Python and Pytest. If you're running an old version of Python or Pytest, you will need to use an older major version of Syrupy:

| Syrupy Version | Python Support | Pytest Support |
| -------------- | -------------- | -------------- |
| 4.x.x | >3.8.1 | >=7 |
| 3.x.x | >=3.7, <4 | >=5.1, <8 |
| 2.x.x | >=3.6, <4 | >=5.1, <8 |


## Usage

### Basic Usage
Expand Down
200 changes: 98 additions & 102 deletions poetry.lock

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions pyproject.toml
Expand Up @@ -12,7 +12,6 @@ classifiers = [
'Intended Audience :: Developers',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
Expand All @@ -28,29 +27,30 @@ include = ['src/syrupy/**/*']
syrupy = 'syrupy'

[tool.poetry.dependencies]
python = '>=3.7,<4'
python = '>=3.8.1,<4'
colored = '>=1.3.92,<2.0.0'
pytest = '>=5.1.0,<8.0.0'
pytest = '>=7.0.0,<8.0.0'

[tool.poetry.group.test.dependencies]
codecov = '^2.1.12'
invoke = '^1.7.3'
coverage = { version = '^6.5.0', extras = ['toml'] }
pytest-benchmark = '^4.0.0'
pytest-xdist = '^3.1.0'

[tool.poetry.group.dev.dependencies]
isort = '^5.10.1'
black = '^22.10.0'
mypy = '^0.960'
mypy = '^0.991'
py-githooks = '^1.1.1'
flake8 = '^3.9.2'
flake8-bugbear = '^21.11.29'
flake8 = '^6.0.0'
flake8-bugbear = '^22.10.27'
flake8-builtins = '^2.0.1'
flake8-comprehensions = '^3.10.1'
twine = '^4.0.1'
semver = '^2.13.0'
setuptools-scm = '^7.0.5'
debugpy = '^1.6.3'
debugpy = '^1.6.4'

[tool.black]
line-length = 88
Expand Down Expand Up @@ -93,7 +93,7 @@ dist,
'''

[tool.pytest.ini_options]
addopts = '-p syrupy --doctest-modules'
addopts = '-p syrupy -p pytester -p no:legacypath --doctest-modules'
testpaths = ['tests']

[tool.coverage.run]
Expand Down
6 changes: 3 additions & 3 deletions src/syrupy/__init__.py
Expand Up @@ -162,13 +162,13 @@ def pytest_runtest_logfinish(nodeid: str) -> None:
_syrupy.ran_item(nodeid)


@pytest.hookimpl(tryfirst=True)
def pytest_sessionfinish(session: Any, exitstatus: int) -> None:
@pytest.hookimpl(tryfirst=True) # type: ignore[misc]
def pytest_sessionfinish(session: "pytest.Session", exitstatus: int) -> None:
"""
Finish session run and set exit status.
https://docs.pytest.org/en/latest/reference.html#_pytest.hookspec.pytest_sessionfinish
"""
session.exitstatus |= exitstatus | session.config._syrupy.finish()
session.exitstatus |= exitstatus | session.config._syrupy.finish() # type: ignore[attr-defined] # noqa: E501


def pytest_terminal_summary(
Expand Down
102 changes: 70 additions & 32 deletions src/syrupy/assertion.py
Expand Up @@ -12,10 +12,14 @@
Dict,
List,
Optional,
Tuple,
Type,
)

from .exceptions import SnapshotDoesNotExist
from .exceptions import (
SnapshotDoesNotExist,
TaintedSnapshotError,
)
from .extensions.amber.serializer import Repr

if TYPE_CHECKING:
Expand Down Expand Up @@ -94,7 +98,7 @@ def __post_init__(self) -> None:
def __init_extension(
self, extension_class: Type["AbstractSyrupyExtension"]
) -> "AbstractSyrupyExtension":
return extension_class(test_location=self.test_location)
return extension_class()

@property
def extension(self) -> "AbstractSyrupyExtension":
Expand Down Expand Up @@ -125,13 +129,15 @@ def __repr(self) -> "SerializableData":
SnapshotAssertionRepr = namedtuple( # type: ignore
"SnapshotAssertion", ["name", "num_executions"]
)
assertion_result = self.executions.get(
(self._custom_index and self._execution_name_index.get(self._custom_index))
or self.num_executions - 1
)
execution_index = (
self._custom_index and self._execution_name_index.get(self._custom_index)
) or self.num_executions - 1
assertion_result = self.executions.get(execution_index)
return (
Repr(str(assertion_result.final_data))
if assertion_result
if execution_index in self.executions
and assertion_result
and assertion_result.final_data is not None
else SnapshotAssertionRepr(
name=self.name,
num_executions=self.num_executions,
Expand Down Expand Up @@ -179,15 +185,23 @@ def _serialize(self, data: "SerializableData") -> "SerializedData":
def get_assert_diff(self) -> List[str]:
assertion_result = self._execution_results[self.num_executions - 1]
if assertion_result.exception:
lines = [
line
for lines in traceback.format_exception(
assertion_result.exception.__class__,
assertion_result.exception,
assertion_result.exception.__traceback__,
)
for line in lines.splitlines()
]
if isinstance(assertion_result.exception, (TaintedSnapshotError,)):
lines = [
gettext(
"This snapshot needs to be regenerated. "
"This is typically due to a major Syrupy update."
)
]
else:
lines = [
line
for lines in traceback.format_exception(
assertion_result.exception.__class__,
assertion_result.exception,
assertion_result.exception.__traceback__,
)
for line in lines.splitlines()
]
# Rotate to place exception with message at first line
return lines[-1:] + lines[:-1]
snapshot_data = assertion_result.recalled_data
Expand Down Expand Up @@ -232,41 +246,54 @@ def __call__(
return self

def __repr__(self) -> str:
return str(self._serialize(self.__repr))
return str(self.__repr)

def __eq__(self, other: "SerializableData") -> bool:
return self._assert(other)

def _assert(self, data: "SerializableData") -> bool:
snapshot_location = self.extension.get_location(index=self.index)
snapshot_name = self.extension.get_snapshot_name(index=self.index)
snapshot_location = self.extension.get_location(
test_location=self.test_location, index=self.index
)
snapshot_name = self.extension.get_snapshot_name(
test_location=self.test_location, index=self.index
)
snapshot_data: Optional["SerializedData"] = None
serialized_data: Optional["SerializedData"] = None
matches = False
assertion_success = False
assertion_exception = None
try:
snapshot_data = self._recall_data(index=self.index)
snapshot_data, tainted = self._recall_data(index=self.index)
serialized_data = self._serialize(data)
snapshot_diff = getattr(self, "_snapshot_diff", None)
if snapshot_diff is not None:
snapshot_data_diff = self._recall_data(index=snapshot_diff)
snapshot_data_diff, _ = self._recall_data(index=snapshot_diff)
if snapshot_data_diff is None:
raise SnapshotDoesNotExist()
serialized_data = self.extension.diff_snapshots(
serialized_data=serialized_data,
snapshot_data=snapshot_data_diff,
)
matches = snapshot_data is not None and self.extension.matches(
serialized_data=serialized_data, snapshot_data=snapshot_data
matches = (
not tainted
and snapshot_data is not None
and self.extension.matches(
serialized_data=serialized_data, snapshot_data=snapshot_data
)
)
assertion_success = matches
if not matches and self.update_snapshots:
self.extension.write_snapshot(
data=serialized_data,
index=self.index,
)
assertion_success = True
if not matches:
if self.update_snapshots:
self.session.queue_snapshot_write(
extension=self.extension,
test_location=self.test_location,
data=serialized_data,
index=self.index,
)
assertion_success = True
elif tainted:
raise TaintedSnapshotError
return assertion_success
except Exception as e:
assertion_exception = e
Expand Down Expand Up @@ -295,8 +322,19 @@ def _post_assert(self) -> None:
while self._post_assert_actions:
self._post_assert_actions.pop()()

def _recall_data(self, index: "SnapshotIndex") -> Optional["SerializableData"]:
def _recall_data(
self, index: "SnapshotIndex"
) -> Tuple[Optional["SerializableData"], bool]:
try:
return self.extension.read_snapshot(index=index)
return (
self.extension.read_snapshot(
test_location=self.test_location,
index=index,
session_id=str(id(self.session)),
),
False,
)
except SnapshotDoesNotExist:
return None
return None, False
except TaintedSnapshotError as e:
return e.snapshot_data, True
4 changes: 2 additions & 2 deletions src/syrupy/constants.py
@@ -1,6 +1,6 @@
SNAPSHOT_DIRNAME = "__snapshots__"
SNAPSHOT_EMPTY_FOSSIL_KEY = "empty snapshot fossil"
SNAPSHOT_UNKNOWN_FOSSIL_KEY = "unknown snapshot fossil"
SNAPSHOT_EMPTY_FOSSIL_KEY = "empty snapshot collection"
SNAPSHOT_UNKNOWN_FOSSIL_KEY = "unknown snapshot collection"

EXIT_STATUS_FAIL_UNUSED = 1

Expand Down

0 comments on commit 5eee3d8

Please sign in to comment.