Files
metro-warden/tests/test_state.py
T

270 lines
8.3 KiB
Python

"""
Tests for core.state.StateStore
Covers:
- get / set / delete basic operations
- default value on missing key
- watcher callbacks on change
- watcher not called when value unchanged
- watcher wildcard patterns
- unwatch removes callback
- update() for bulk set
- snapshot() returns deep copy
- bus integration (state publishes to bus)
- keys() and __contains__
"""
from __future__ import annotations
from typing import Any, List
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from core.state import StateStore
# ── Fixtures ──────────────────────────────────────────────────────────────
@pytest.fixture
def store() -> StateStore:
return StateStore()
@pytest.fixture
def store_with_bus():
bus = MagicMock()
bus.publish_sync = MagicMock()
s = StateStore(bus=bus)
return s, bus
# ── Basic get / set ──────────────────────────────────────────────────────
def test_set_and_get(store: StateStore) -> None:
store.set("key.a", 42)
assert store.get("key.a") == 42
def test_get_missing_key_returns_default(store: StateStore) -> None:
assert store.get("does.not.exist") is None
assert store.get("does.not.exist", "fallback") == "fallback"
def test_set_overwrites_value(store: StateStore) -> None:
store.set("x", 1)
store.set("x", 2)
assert store.get("x") == 2
def test_get_returns_deep_copy(store: StateStore) -> None:
store.set("obj", {"a": [1, 2, 3]})
got = store.get("obj")
got["a"].append(99)
assert store.get("obj") == {"a": [1, 2, 3]} # original unchanged
# ── Delete ────────────────────────────────────────────────────────────────
def test_delete_existing_key(store: StateStore) -> None:
store.set("del.me", "value")
assert store.delete("del.me") is True
assert store.get("del.me") is None
def test_delete_missing_key_returns_false(store: StateStore) -> None:
assert store.delete("ghost.key") is False
# ── Update ────────────────────────────────────────────────────────────────
def test_update_sets_multiple_keys(store: StateStore) -> None:
store.update({"a": 1, "b": 2, "c": 3})
assert store.get("a") == 1
assert store.get("b") == 2
assert store.get("c") == 3
# ── Watchers ─────────────────────────────────────────────────────────────
def test_watcher_called_on_change(store: StateStore) -> None:
events: List[tuple] = []
def cb(key: str, old: Any, new: Any) -> None:
events.append((key, old, new))
store.watch("watched.key", cb)
store.set("watched.key", "hello")
assert len(events) == 1
assert events[0] == ("watched.key", None, "hello")
def test_watcher_called_with_old_and_new(store: StateStore) -> None:
events: List[tuple] = []
store.set("my.key", "initial")
def cb(key: str, old: Any, new: Any) -> None:
events.append((old, new))
store.watch("my.key", cb)
store.set("my.key", "updated")
assert events == [("initial", "updated")]
def test_watcher_not_called_when_value_unchanged(store: StateStore) -> None:
events: List[tuple] = []
def cb(key, old, new):
events.append((old, new))
store.set("stable", 100)
store.watch("stable", cb)
store.set("stable", 100) # same value
assert len(events) == 0
def test_multiple_watchers_on_same_key(store: StateStore) -> None:
results_a: list = []
results_b: list = []
store.watch("key", lambda k, o, n: results_a.append(n))
store.watch("key", lambda k, o, n: results_b.append(n))
store.set("key", "fire")
assert results_a == ["fire"]
assert results_b == ["fire"]
def test_watcher_on_delete(store: StateStore) -> None:
events: list = []
store.set("dying.key", "alive")
store.watch("dying.key", lambda k, o, n: events.append((o, n)))
store.delete("dying.key")
assert events == [("alive", None)]
# ── Wildcard watchers ─────────────────────────────────────────────────────
def test_wildcard_watcher_fires_for_matching_keys(store: StateStore) -> None:
events: list = []
store.watch("network.*", lambda k, o, n: events.append(k))
store.set("network.interfaces", {})
store.set("network.stats", {})
store.set("system.cpu", {}) # should NOT trigger
assert "network.interfaces" in events
assert "network.stats" in events
assert "system.cpu" not in events
def test_exact_and_wildcard_both_fired(store: StateStore) -> None:
exact: list = []
wild: list = []
store.watch("net.iface", lambda k, o, n: exact.append(n))
store.watch("net.*", lambda k, o, n: wild.append(n))
store.set("net.iface", "up")
assert exact == ["up"]
assert wild == ["up"]
# ── Unwatch ───────────────────────────────────────────────────────────────
def test_unwatch_stops_callback(store: StateStore) -> None:
events: list = []
def cb(k, o, n):
events.append(n)
wid = store.watch("remove.me", cb)
store.set("remove.me", 1)
assert len(events) == 1
store.unwatch(wid)
store.set("remove.me", 2)
assert len(events) == 1 # no new call
def test_unwatch_unknown_id_returns_false(store: StateStore) -> None:
assert store.unwatch("not-a-real-id") is False
# ── Snapshot ──────────────────────────────────────────────────────────────
def test_snapshot_returns_full_state(store: StateStore) -> None:
store.set("a", 1)
store.set("b", {"nested": True})
snap = store.snapshot()
assert snap["a"] == 1
assert snap["b"] == {"nested": True}
def test_snapshot_is_deep_copy(store: StateStore) -> None:
store.set("obj", [1, 2, 3])
snap = store.snapshot()
snap["obj"].append(99)
assert store.get("obj") == [1, 2, 3]
# ── Keys and contains ─────────────────────────────────────────────────────
def test_keys_returns_all_keys(store: StateStore) -> None:
store.set("x", 1)
store.set("y", 2)
assert set(store.keys()) == {"x", "y"}
def test_contains_operator(store: StateStore) -> None:
store.set("present", True)
assert "present" in store
assert "absent" not in store
# ── Bus integration ───────────────────────────────────────────────────────
def test_bus_publish_called_on_set(store_with_bus) -> None:
store, bus = store_with_bus
store.set("some.key", "value")
bus.publish_sync.assert_called_once_with(
"state.some.key", {"key": "some.key", "value": "value"}
)
def test_bus_not_called_when_value_unchanged(store_with_bus) -> None:
store, bus = store_with_bus
store.set("stable", 42)
bus.publish_sync.reset_mock()
store.set("stable", 42) # unchanged
bus.publish_sync.assert_not_called()
def test_bus_called_on_delete(store_with_bus) -> None:
store, bus = store_with_bus
store.set("temp", "here")
bus.publish_sync.reset_mock()
store.delete("temp")
bus.publish_sync.assert_called_once_with(
"state.temp", {"key": "temp", "value": None}
)
# ── Repr ──────────────────────────────────────────────────────────────────
def test_repr(store: StateStore) -> None:
store.set("k", "v")
store.watch("k", lambda a, b, c: None)
r = repr(store)
assert "StateStore" in r
assert "keys=1" in r