Skip to content

Commit 5ed431f

Browse files
committed
Add unit test cases for new registry/context/processor modules
Add unit test cases for reflex.istate.manager.token
1 parent 06270b3 commit 5ed431f

10 files changed

Lines changed: 1131 additions & 0 deletions

File tree

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
"""Tests for StateToken, BaseStateToken, and from_legacy_token."""
2+
3+
import io
4+
import pickle
5+
6+
import pytest
7+
8+
from reflex.istate.manager.token import BaseStateToken, StateToken
9+
10+
11+
def test_state_token_str():
12+
"""__str__ encodes ident and cls into 'ident/module.Class' format."""
13+
token = StateToken(ident="abc-123", cls=int)
14+
assert str(token) == "abc-123/builtins.int"
15+
16+
17+
def test_state_token_str_escapes_slashes():
18+
"""Slashes in ident or cls name are percent-encoded."""
19+
token = StateToken(ident="a/b", cls=int)
20+
result = str(token)
21+
assert "%2F" in result
22+
assert "/" in result
23+
24+
25+
def test_state_token_with_cls():
26+
"""with_cls returns a new token with updated cls, leaving the original unchanged."""
27+
token = StateToken(ident="tok", cls=int)
28+
new = token.with_cls(bool)
29+
assert new.cls is bool
30+
assert new.ident == "tok"
31+
assert token.cls is int
32+
33+
34+
def test_state_token_serialize_deserialize_roundtrip():
35+
"""serialize/deserialize with data= round-trips through pickle."""
36+
value = {"key": [1, 2, 3]}
37+
data = StateToken.serialize(value)
38+
assert isinstance(data, bytes)
39+
assert StateToken.deserialize(data=data) == value
40+
41+
42+
def test_state_token_deserialize_from_fp():
43+
"""Deserialize with fp= reads from a file-like object."""
44+
value = "hello"
45+
buf = io.BytesIO(pickle.dumps(value))
46+
assert StateToken.deserialize(fp=buf) == value
47+
48+
49+
def test_state_token_deserialize_neither_raises():
50+
"""Deserialize with neither data nor fp raises ValueError."""
51+
with pytest.raises(ValueError, match="Only one"):
52+
StateToken.deserialize()
53+
54+
55+
def test_state_token_get_and_reset_touched_state():
56+
"""Default implementation always returns True."""
57+
assert StateToken.get_and_reset_touched_state("anything") is True
58+
59+
60+
def test_base_state_token_str(clean_registration_context):
61+
"""__str__ uses 'ident_full_name' format.
62+
63+
Args:
64+
clean_registration_context: A fresh, empty registration context.
65+
"""
66+
from reflex.state import BaseState
67+
68+
class TokState(BaseState):
69+
pass
70+
71+
token = BaseStateToken(ident="client-abc", cls=TokState)
72+
result = str(token)
73+
assert result.startswith("client-abc_")
74+
assert TokState.get_full_name() in result
75+
76+
77+
def test_base_state_token_with_cls(clean_registration_context):
78+
"""with_cls returns a BaseStateToken (not a plain StateToken).
79+
80+
Args:
81+
clean_registration_context: A fresh, empty registration context.
82+
"""
83+
from reflex.state import BaseState
84+
85+
class A(BaseState):
86+
pass
87+
88+
class B(BaseState):
89+
pass
90+
91+
token = BaseStateToken(ident="tok", cls=A)
92+
new = token.with_cls(B)
93+
assert isinstance(new, BaseStateToken)
94+
assert new.cls is B
95+
96+
97+
def test_base_state_token_serialize_deserialize(clean_registration_context):
98+
"""BaseStateToken serialization uses BaseState._serialize/_deserialize.
99+
100+
Args:
101+
clean_registration_context: A fresh, empty registration context.
102+
"""
103+
from reflex.state import BaseState
104+
105+
class SerState(BaseState):
106+
x: int = 42
107+
108+
state = SerState()
109+
data = BaseStateToken.serialize(state)
110+
assert isinstance(data, bytes)
111+
restored = BaseStateToken.deserialize(data=data)
112+
assert isinstance(restored, SerState)
113+
assert restored.x == 42
114+
115+
116+
def test_base_state_token_get_and_reset_touched(clean_registration_context):
117+
"""get_and_reset_touched_state returns the touched flag and resets it.
118+
119+
Args:
120+
clean_registration_context: A fresh, empty registration context.
121+
"""
122+
from reflex.state import BaseState
123+
124+
class TouchState(BaseState):
125+
x: int = 0
126+
127+
state = TouchState()
128+
state._was_touched = True
129+
assert BaseStateToken.get_and_reset_touched_state(state) is True
130+
assert state._was_touched is False
131+
assert BaseStateToken.get_and_reset_touched_state(state) is False
132+
133+
134+
def test_from_legacy_token(clean_registration_context):
135+
"""from_legacy_token parses 'ident_state.path' into a BaseStateToken.
136+
137+
Args:
138+
clean_registration_context: A fresh, empty registration context.
139+
"""
140+
from reflex.state import BaseState
141+
142+
class LegacyRoot(BaseState):
143+
pass
144+
145+
full_name = LegacyRoot.get_full_name()
146+
legacy_str = f"my-client-token_{full_name}"
147+
148+
token = BaseStateToken.from_legacy_token(legacy_str, root_state=LegacyRoot)
149+
assert token.ident == "my-client-token"
150+
assert token.cls is LegacyRoot
151+
152+
153+
def test_from_legacy_token_substate(clean_registration_context):
154+
"""from_legacy_token resolves a substate path.
155+
156+
Args:
157+
clean_registration_context: A fresh, empty registration context.
158+
"""
159+
from reflex.state import BaseState
160+
161+
class LegRoot(BaseState):
162+
pass
163+
164+
class LegChild(LegRoot):
165+
pass
166+
167+
full_name = LegChild.get_full_name()
168+
legacy_str = f"tok_{full_name}"
169+
170+
token = BaseStateToken.from_legacy_token(legacy_str, root_state=LegRoot)
171+
assert token.ident == "tok"
172+
assert token.cls is LegChild
173+
174+
175+
def test_from_legacy_token_none_root_raises():
176+
"""from_legacy_token with root_state=None raises ValueError."""
177+
with pytest.raises(ValueError, match="Root state must be provided"):
178+
BaseStateToken.from_legacy_token("tok_some.state", root_state=None)

tests/units/reflex_core/__init__.py

Whitespace-only changes.

tests/units/reflex_core/_internal/__init__.py

Whitespace-only changes.

tests/units/reflex_core/_internal/context/__init__.py

Whitespace-only changes.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""Tests for BaseContext."""
2+
3+
import dataclasses
4+
5+
import pytest
6+
from reflex_core._internal.context.base import BaseContext
7+
8+
9+
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
10+
class _TestContext(BaseContext):
11+
"""Minimal BaseContext subclass for unit testing."""
12+
13+
label: str = "test"
14+
15+
16+
def test_get_without_set_raises():
17+
"""get() raises LookupError when no context is set."""
18+
with pytest.raises(LookupError):
19+
_TestContext.get()
20+
21+
22+
def test_set_and_get():
23+
"""set() makes the context retrievable via get()."""
24+
ctx = _TestContext(label="a")
25+
token = _TestContext.set(ctx)
26+
try:
27+
assert _TestContext.get() is ctx
28+
finally:
29+
_TestContext.reset(token)
30+
31+
32+
def test_reset_restores_previous():
33+
"""reset() restores the previously active context."""
34+
outer = _TestContext(label="outer")
35+
outer_tok = _TestContext.set(outer)
36+
try:
37+
inner = _TestContext(label="inner")
38+
inner_tok = _TestContext.set(inner)
39+
assert _TestContext.get() is inner
40+
_TestContext.reset(inner_tok)
41+
assert _TestContext.get() is outer
42+
finally:
43+
_TestContext.reset(outer_tok)
44+
45+
46+
def test_context_manager_enter_exit():
47+
"""__enter__ sets the context and __exit__ removes it."""
48+
ctx = _TestContext(label="cm")
49+
with ctx as entered:
50+
assert entered is ctx
51+
assert _TestContext.get() is ctx
52+
with pytest.raises(LookupError):
53+
_TestContext.get()
54+
55+
56+
def test_context_manager_nesting():
57+
"""Nested context managers restore the outer context on inner exit."""
58+
outer = _TestContext(label="outer")
59+
inner = _TestContext(label="inner")
60+
with outer:
61+
assert _TestContext.get().label == "outer"
62+
with inner:
63+
assert _TestContext.get().label == "inner"
64+
assert _TestContext.get().label == "outer"
65+
66+
67+
def test_double_enter_raises():
68+
"""Entering the same context instance twice raises RuntimeError."""
69+
ctx = _TestContext(label="double")
70+
with ctx, pytest.raises(RuntimeError, match="already attached"):
71+
ctx.__enter__()
72+
73+
74+
def test_ensure_context_attached():
75+
"""ensure_context_attached raises when not entered, succeeds when entered."""
76+
ctx = _TestContext(label="ensure")
77+
with pytest.raises(RuntimeError, match="must be entered"):
78+
ctx.ensure_context_attached()
79+
with ctx:
80+
ctx.ensure_context_attached()
81+
82+
83+
def test_subclasses_have_independent_context_vars():
84+
"""Two BaseContext subclasses do not share their ContextVar."""
85+
86+
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
87+
class _OtherContext(BaseContext):
88+
value: int = 0
89+
90+
ctx_a = _TestContext(label="a")
91+
ctx_b = _OtherContext(value=42)
92+
with ctx_a, ctx_b:
93+
assert _TestContext.get().label == "a"
94+
assert _OtherContext.get().value == 42

0 commit comments

Comments
 (0)