From 286dbba7a5bc0dd553835c43a0790585ffa61931 Mon Sep 17 00:00:00 2001 From: Sassan Haradji Date: Fri, 3 May 2024 05:30:55 +0400 Subject: [PATCH] test(middleware): add middleware tests --- CHANGELOG.md | 4 + pyproject.toml | 4 +- redux/main.py | 36 ++++---- tests/test_middleware.py | 178 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 199 insertions(+), 23 deletions(-) create mode 100644 tests/test_middleware.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f28bf9..a37d63f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Version 0.14.5 + +- test(middleware): add middleware tests + ## Version 0.14.4 - refactor(test): add the counter id of the failed snapshot to the error message diff --git a/pyproject.toml b/pyproject.toml index c377d4d..ed55617 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "python-redux" -version = "0.14.4" +version = "0.14.5" description = "Redux implementation for Python" authors = ["Sassan Haradji "] license = "Apache-2.0" @@ -50,7 +50,7 @@ inline-quotes = "single" multiline-quotes = "double" [tool.ruff.lint.per-file-ignores] -"tests/*" = ["S101", "PLR0915"] +"tests/*" = ["S101", "PLR0915", "PLR2004"] [tool.ruff.format] quote-style = 'single' diff --git a/redux/main.py b/redux/main.py index 022d4c4..1e37e9c 100644 --- a/redux/main.py +++ b/redux/main.py @@ -109,33 +109,27 @@ def _call_listeners(self: Store[State, Action, Event], state: State) -> None: self._create_task(result) def _run_actions(self: Store[State, Action, Event]) -> None: - while True: - if len(self._actions) == 0: - return + while len(self._actions) > 0: action = self._actions.pop(0) if action is not None: - break - result = self.reducer(self._state, action) - if is_complete_reducer_result(result): - self._state = result.state - self._call_listeners(self._state) - self.dispatch([*(result.actions or []), *(result.events or [])]) - elif is_state_reducer_result(result): - self._state = result - self._call_listeners(self._state) - - if isinstance(action, FinishAction): - self.dispatch(cast(Event, FinishEvent())) + result = self.reducer(self._state, action) + if is_complete_reducer_result(result): + self._state = result.state + self._call_listeners(self._state) + self.dispatch([*(result.actions or []), *(result.events or [])]) + elif is_state_reducer_result(result): + self._state = result + self._call_listeners(self._state) + + if isinstance(action, FinishAction): + self.dispatch(cast(Event, FinishEvent())) def _run_event_handlers(self: Store[State, Action, Event]) -> None: - while True: - if len(self._events) == 0: - return + while len(self._events) > 0: event = self._events.pop(0) if event is not None: - break - for event_handler in self._event_handlers[type(event)].copy(): - self._event_handlers_queue.put_nowait((event_handler, event)) + for event_handler in self._event_handlers[type(event)].copy(): + self._event_handlers_queue.put_nowait((event_handler, event)) def run(self: Store[State, Action, Event]) -> None: """Run the store.""" diff --git a/tests/test_middleware.py b/tests/test_middleware.py new file mode 100644 index 0000000..835208b --- /dev/null +++ b/tests/test_middleware.py @@ -0,0 +1,178 @@ +# ruff: noqa: D100, D101, D102, D103, D104, D107 + +from __future__ import annotations + +from dataclasses import replace + +import pytest +from immutable import Immutable + +from redux.basic_types import ( + BaseAction, + BaseEvent, + CompleteReducerResult, + CreateStoreOptions, + FinishAction, + FinishEvent, + InitAction, + InitializationActionError, +) +from redux.main import Store + + +class StateType(Immutable): + value: int + + +class IncrementAction(BaseAction): ... + + +class DecrementAction(BaseAction): ... + + +class SomeEvent(BaseEvent): ... + + +Action = IncrementAction | DecrementAction | InitAction | FinishAction + + +def reducer( + state: StateType | None, + action: Action, +) -> StateType | CompleteReducerResult[StateType, Action, SomeEvent | 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) + + if isinstance(action, DecrementAction): + return replace(state, value=state.value - 1) + + return state + + +class StoreType(Store[StateType, Action, FinishEvent | SomeEvent]): + @property + def state(self: StoreType) -> StateType | None: + return self._state + + +@pytest.fixture() +def store() -> StoreType: + return StoreType(reducer, options=CreateStoreOptions(auto_init=True)) + + +def test_identity_action_middleware(store: StoreType) -> None: + calls = [] + + def middleware(action: Action) -> Action: + calls.append(action) + if isinstance(action, IncrementAction): + return DecrementAction() + return action + + store.register_action_middleware(middleware) + + actions = [ + IncrementAction(), + IncrementAction(), + FinishAction(), + ] + + def check() -> None: + assert calls == actions + assert store.state + assert store.state.value == -2 + + store.subscribe_event(FinishEvent, check) + + for action in actions: + store.dispatch(action) + + +def test_cancelling_action_middleware(store: StoreType) -> None: + calls = [] + + def middleware(action: Action) -> Action | None: + calls.append(action) + if len(calls) == 1: + return None + return action + + store.register_action_middleware(middleware) + + actions = [ + IncrementAction(), + IncrementAction(), + FinishAction(), + ] + + def check() -> None: + assert store.state + assert store.state.value == 1 + + store.subscribe_event(FinishEvent, check) + + for action in actions: + store.dispatch(action) + + +def test_identity_event_middlewares(store: StoreType) -> None: + calls = [] + + def middleware(event: SomeEvent) -> SomeEvent | FinishEvent: + calls.append(event) + if len(calls) == 2: + return FinishEvent() + return event + + store.register_event_middleware(middleware) + + events = [ + SomeEvent(), + SomeEvent(), + SomeEvent(), + ] + + def check() -> None: + assert calls == events + + store.subscribe_event(FinishEvent, check) + + for event in events: + store.dispatch(event) + + +def test_cancelling_event_middlewares(store: StoreType) -> None: + calls = [] + + def middleware(event: SomeEvent | FinishEvent) -> SomeEvent | FinishEvent | None: + calls.append(event) + if len(calls) == 1 and isinstance(event, SomeEvent): + return None + return event + + side_effect_calls = [] + + def some_side_effect(event: SomeEvent) -> None: + side_effect_calls.append(event) + + store.register_event_middleware(middleware) + + events = [ + SomeEvent(), + SomeEvent(), + ] + + def check() -> None: + assert side_effect_calls == events[1:2] + + store.subscribe_event(SomeEvent, some_side_effect) + store.subscribe_event(FinishEvent, check) + + for event in events: + store.dispatch(event) + store.dispatch(FinishAction())