Skip to content

Commit

Permalink
test(middleware): add middleware tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sassanh committed May 3, 2024
1 parent ff25e91 commit 286dbba
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 23 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>"]
license = "Apache-2.0"
Expand Down Expand Up @@ -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'
Expand Down
36 changes: 15 additions & 21 deletions redux/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
178 changes: 178 additions & 0 deletions tests/test_middleware.py
Original file line number Diff line number Diff line change
@@ -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())

0 comments on commit 286dbba

Please sign in to comment.