Skip to content

Commit

Permalink
feat(test-snapshot): add prefix to snapshot fixture
Browse files Browse the repository at this point in the history
  • Loading branch information
sassanh committed Jun 25, 2024
1 parent c0f0a91 commit 0f76ec0
Show file tree
Hide file tree
Showing 12 changed files with 176 additions and 14 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- refactor(core): use `str_to_bool` of `python-strtobool` instead of `strtobool`
of `distutils`
- feat(test-snapshot): add prefix to snapshot fixture

## Version 0.15.8

Expand Down
3 changes: 2 additions & 1 deletion redux_pytest/fixtures/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from .event_loop import LoopThread, event_loop # noqa: E402
from .monitor import StoreMonitor, store_monitor # noqa: E402
from .snapshot import StoreSnapshot, store_snapshot # noqa: E402
from .snapshot import StoreSnapshot, snapshot_prefix, store_snapshot # noqa: E402
from .store import needs_finish, store # noqa: E402
from .wait_for import Waiter, WaitFor, wait_for # noqa: E402

Expand All @@ -24,6 +24,7 @@
'WaitFor',
'event_loop',
'needs_finish',
'snapshot_prefix',
'store',
'store_monitor',
'store_snapshot',
Expand Down
46 changes: 35 additions & 11 deletions redux_pytest/fixtures/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
import json
import os
from collections import defaultdict
from distutils.util import strtobool
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Generic, cast

import pytest
from str_to_bool import str_to_bool

from redux.basic_types import FinishEvent, State

Expand All @@ -23,15 +23,17 @@
class StoreSnapshot(Generic[State]):
"""Context object for tests taking snapshots of the store."""

def __init__(
def __init__( # noqa: PLR0913
self: StoreSnapshot,
*,
test_id: str,
path: Path,
override: bool,
store: Store,
prefix: str | None,
) -> None:
"""Create a new store snapshot context."""
self.prefix = prefix
self._is_failed = False
self._is_closed = False
self.override = override
Expand All @@ -41,8 +43,13 @@ def __init__(
path.parent / 'results' / file / test_id.split('::')[-1][5:],
)
if self.results_dir.exists():
prefix_element = ''
if self.prefix:
prefix_element = self.prefix + '-'
for file in self.results_dir.glob(
'store-*.jsonc' if override else 'store-*.mismatch.jsonc',
f'store-{prefix_element}*.jsonc'
if override
else f'store-{prefix_element}*.mismatch.jsonc',
):
file.unlink() # pragma: no cover
self.results_dir.mkdir(parents=True, exist_ok=True)
Expand All @@ -69,9 +76,15 @@ def json_snapshot(

def get_filename(self: StoreSnapshot[State], title: str | None) -> str:
"""Get the filename for the snapshot."""
title_element = ''
if title:
return f"""store-{title}-{self.test_counter[title]:03d}"""
return f"""store-{self.test_counter[title]:03d}"""
title_element = title + '-'
prefix_element = ''
if self.prefix:
prefix_element = self.prefix + '-'
return (
f"""store-{prefix_element}{title_element}{self.test_counter[title]:03d}"""
)

def take(
self: StoreSnapshot[State],
Expand Down Expand Up @@ -101,10 +114,10 @@ def take(
if json_path.exists():
old_snapshot = json_path.read_text().split('\n', 1)[1][:-1]
else:
old_snapshot = None
if old_snapshot != new_snapshot:
old_snapshot = None # pragma: no cover
if old_snapshot != new_snapshot: # pragma: no cover
self._is_failed = True
mismatch_path.write_text( # pragma: no cover
mismatch_path.write_text(
f'// MISMATCH: {filename}\n{new_snapshot}\n',
)
assert new_snapshot == old_snapshot, f'Store snapshot mismatch - {filename}'
Expand All @@ -123,7 +136,7 @@ def _(state: object | None) -> None:
def close(self: StoreSnapshot[State]) -> None:
"""Close the snapshot context."""
self._is_closed = True
if self._is_failed:
if self._is_failed: # pragma: no cover
return
for title in self.test_counter:
filename = self.get_filename(title)
Expand All @@ -133,14 +146,24 @@ def close(self: StoreSnapshot[State]) -> None:


@pytest.fixture()
def store_snapshot(request: SubRequest, store: Store) -> StoreSnapshot:
def snapshot_prefix() -> str | None:
"""Return the prefix for the snapshots."""
return None


@pytest.fixture()
def store_snapshot(
request: SubRequest,
store: Store,
snapshot_prefix: str | None,
) -> StoreSnapshot:
"""Take a snapshot of the current state of the store."""
override = (
request.config.getoption(
'--override-store-snapshots',
default=cast(
Any,
strtobool(os.environ.get('REDUX_TEST_OVERRIDE_SNAPSHOTS', 'false'))
str_to_bool(os.environ.get('REDUX_TEST_OVERRIDE_SNAPSHOTS', 'false'))
== 1,
),
)
Expand All @@ -151,4 +174,5 @@ def store_snapshot(request: SubRequest, store: Store) -> StoreSnapshot:
path=request.node.path,
override=override,
store=store,
prefix=snapshot_prefix,
)
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from redux_pytest.fixtures import (
event_loop,
needs_finish,
snapshot_prefix,
store,
store_monitor,
store_snapshot,
Expand All @@ -21,6 +22,7 @@
__all__ = [
'event_loop',
'needs_finish',
'snapshot_prefix',
'store',
'store_monitor',
'store_snapshot',
Expand Down
2 changes: 2 additions & 0 deletions tests/results/test_snapshot_fixture/monitor/store-000.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// store-000
1
2 changes: 2 additions & 0 deletions tests/results/test_snapshot_fixture/monitor/store-001.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// store-001
3
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// store-custom_prefix-000
0
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// store-custom_prefix-001
1
5 changes: 5 additions & 0 deletions tests/test_monitor_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ class DummyEvent(BaseEvent): ...
Action = IncrementAction | InitAction | FinishAction


@pytest.fixture()
def snapshot_prefix() -> str:
return 'prefix'


def reducer(
state: StateType | None,
action: Action,
Expand Down
4 changes: 2 additions & 2 deletions tests/test_performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def store() -> Generator[StoreType, None, None]:


def test_simple_dispatch(store: StoreType) -> None:
count = 100000
count = 50000
for _ in range(count):
store.dispatch(IncrementAction())

Expand All @@ -80,7 +80,7 @@ def callback(_: StateType | None) -> None:

store.subscribe(callback)

count = 500
count = 400
for _ in range(count):
store.dispatch(IncrementAction())

Expand Down
32 changes: 32 additions & 0 deletions tests/test_snapshot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# ruff: noqa: D100, D101, D102, D103, D104, D107
from __future__ import annotations

from immutable import Immutable

from redux.basic_types import (
BaseAction,
CreateStoreOptions,
FinishAction,
FinishEvent,
)
from redux.main import Store


class StateType(Immutable):
value: int


StoreType = Store[StateType, BaseAction, FinishEvent]


def test_snapshot() -> None:
initial_state = StateType(value=0)

store = Store(
lambda state, __: state or initial_state,
options=CreateStoreOptions(auto_init=True),
)

assert store.snapshot == {'value': 0}

store.dispatch(FinishAction())
89 changes: 89 additions & 0 deletions tests/test_snapshot_fixture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# ruff: noqa: D100, D101, D102, D103, D104, D107
from __future__ import annotations

from dataclasses import replace
from typing import TYPE_CHECKING

import pytest
from immutable import Immutable

from redux.basic_types import (
BaseAction,
BaseEvent,
CompleteReducerResult,
CreateStoreOptions,
FinishAction,
FinishEvent,
InitAction,
InitializationActionError,
)
from redux.main import Store

if TYPE_CHECKING:
from redux_pytest.fixtures.snapshot import StoreSnapshot


class StateType(Immutable):
value: int


class IncrementAction(BaseAction): ...


class DummyEvent(BaseEvent): ...


Action = IncrementAction | InitAction | FinishAction


def reducer(
state: StateType | None,
action: Action,
) -> StateType | CompleteReducerResult[StateType, Action, DummyEvent | FinishEvent]:
if state is None:
if isinstance(action, InitAction):
return StateType(value=0)
raise InitializationActionError(action)

if isinstance(action, IncrementAction):
return replace(state, value=state.value + 1)
return state


@pytest.fixture()
def store() -> Store:
return Store(
reducer,
options=CreateStoreOptions(
auto_init=True,
),
)


def test_monitor(
store: Store,
store_snapshot: StoreSnapshot[StateType],
needs_finish: None,
) -> None:
_ = needs_finish
store_snapshot.monitor(lambda state: state.value if state.value % 2 != 0 else None)
store.dispatch(IncrementAction())
store.dispatch(IncrementAction())
store.dispatch(IncrementAction())
store.dispatch(IncrementAction())


class TestSnapshotPrefix:
@pytest.fixture(scope='class')
def snapshot_prefix(self: TestSnapshotPrefix) -> str:
return 'custom_prefix'

def test_prefix(
self: TestSnapshotPrefix,
store: Store,
store_snapshot: StoreSnapshot[StateType],
needs_finish: None,
) -> None:
_ = needs_finish
store_snapshot.monitor(lambda state: state.value)
store.dispatch(IncrementAction())

0 comments on commit 0f76ec0

Please sign in to comment.