diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c617c2..bb41c62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/redux_pytest/fixtures/__init__.py b/redux_pytest/fixtures/__init__.py index 92f2033..87e3348 100644 --- a/redux_pytest/fixtures/__init__.py +++ b/redux_pytest/fixtures/__init__.py @@ -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 @@ -24,6 +24,7 @@ 'WaitFor', 'event_loop', 'needs_finish', + 'snapshot_prefix', 'store', 'store_monitor', 'store_snapshot', diff --git a/redux_pytest/fixtures/snapshot.py b/redux_pytest/fixtures/snapshot.py index 0952715..251f86f 100644 --- a/redux_pytest/fixtures/snapshot.py +++ b/redux_pytest/fixtures/snapshot.py @@ -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 @@ -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 @@ -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) @@ -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], @@ -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}' @@ -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) @@ -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, ), ) @@ -151,4 +174,5 @@ def store_snapshot(request: SubRequest, store: Store) -> StoreSnapshot: path=request.node.path, override=override, store=store, + prefix=snapshot_prefix, ) diff --git a/tests/conftest.py b/tests/conftest.py index b8b2492..951ff2d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,7 @@ from redux_pytest.fixtures import ( event_loop, needs_finish, + snapshot_prefix, store, store_monitor, store_snapshot, @@ -21,6 +22,7 @@ __all__ = [ 'event_loop', 'needs_finish', + 'snapshot_prefix', 'store', 'store_monitor', 'store_snapshot', diff --git a/tests/results/test_snapshot_fixture/monitor/store-000.jsonc b/tests/results/test_snapshot_fixture/monitor/store-000.jsonc new file mode 100644 index 0000000..800071e --- /dev/null +++ b/tests/results/test_snapshot_fixture/monitor/store-000.jsonc @@ -0,0 +1,2 @@ +// store-000 +1 diff --git a/tests/results/test_snapshot_fixture/monitor/store-001.jsonc b/tests/results/test_snapshot_fixture/monitor/store-001.jsonc new file mode 100644 index 0000000..b49beb9 --- /dev/null +++ b/tests/results/test_snapshot_fixture/monitor/store-001.jsonc @@ -0,0 +1,2 @@ +// store-001 +3 diff --git a/tests/results/test_snapshot_fixture/prefix/store-custom_prefix-000.jsonc b/tests/results/test_snapshot_fixture/prefix/store-custom_prefix-000.jsonc new file mode 100644 index 0000000..211f6e6 --- /dev/null +++ b/tests/results/test_snapshot_fixture/prefix/store-custom_prefix-000.jsonc @@ -0,0 +1,2 @@ +// store-custom_prefix-000 +0 diff --git a/tests/results/test_snapshot_fixture/prefix/store-custom_prefix-001.jsonc b/tests/results/test_snapshot_fixture/prefix/store-custom_prefix-001.jsonc new file mode 100644 index 0000000..8d813ab --- /dev/null +++ b/tests/results/test_snapshot_fixture/prefix/store-custom_prefix-001.jsonc @@ -0,0 +1,2 @@ +// store-custom_prefix-001 +1 diff --git a/tests/test_monitor_fixtures.py b/tests/test_monitor_fixtures.py index 23689a9..263c0da 100644 --- a/tests/test_monitor_fixtures.py +++ b/tests/test_monitor_fixtures.py @@ -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, diff --git a/tests/test_performance.py b/tests/test_performance.py index 5464a6f..995bc0f 100644 --- a/tests/test_performance.py +++ b/tests/test_performance.py @@ -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()) @@ -80,7 +80,7 @@ def callback(_: StateType | None) -> None: store.subscribe(callback) - count = 500 + count = 400 for _ in range(count): store.dispatch(IncrementAction()) diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py new file mode 100644 index 0000000..08cc732 --- /dev/null +++ b/tests/test_snapshot.py @@ -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()) diff --git a/tests/test_snapshot_fixture.py b/tests/test_snapshot_fixture.py new file mode 100644 index 0000000..5a6f001 --- /dev/null +++ b/tests/test_snapshot_fixture.py @@ -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())