reflex/tests/test_state.py
Nikhil Rao 29e37350e5
Add basic unit tests (#7)
* Unit tests for components, state, and utils
2022-11-20 14:34:25 -08:00

713 lines
20 KiB
Python

from typing import Dict, List
import pytest
from pynecone.base import Base
from pynecone.event import Event
from pynecone.state import State
from pynecone.var import BaseVar, ComputedVar
@pytest.fixture
def TestObject():
class TestObject(Base):
"""A test object fixture."""
prop1: int = 42
prop2: str = "hello"
return TestObject
@pytest.fixture
def TestState(TestObject):
class TestState(State):
"""A test state."""
num1: int
num2: float = 3.14
key: str
array: List[float] = [1, 2, 3.14]
mapping: Dict[str, List[int]] = {"a": [1, 2, 3], "b": [4, 5, 6]}
obj: TestObject = TestObject()
complex: Dict[int, TestObject] = {1: TestObject(), 2: TestObject()}
@ComputedVar
def sum(self) -> float:
"""Dynamically sum the numbers.
Returns:
The sum of the numbers.
"""
return self.num1 + self.num2
@ComputedVar
def upper(self) -> str:
"""Uppercase the key.
Returns:
The uppercased key.
"""
return self.key.upper()
def do_something(self):
"""Do something."""
pass
return TestState
@pytest.fixture
def ChildState(TestState):
class ChildState(TestState):
"""A child state fixture."""
value: str
count: int = 23
def change_both(self, value: str, count: int):
"""Change both the value and count.
Args:
value: The new value.
count: The new count.
"""
self.value = value.upper()
self.count = count * 2
return ChildState
@pytest.fixture
def ChildState2(TestState):
class ChildState2(TestState):
"""A child state fixture."""
value: str
return ChildState2
@pytest.fixture
def GrandchildState(ChildState):
class GrandchildState(ChildState):
"""A grandchild state fixture."""
value2: str
return GrandchildState
@pytest.fixture
def state(TestState) -> State:
"""A state.
Args:
TestState: The state class.
Returns:
A test state.
"""
return TestState() # type: ignore
def test_base_class_vars(state):
"""Test that the class vars are set correctly.
Args:
state: A state.
"""
fields = state.get_fields()
cls = type(state)
for field in fields:
if field in (
"parent_state",
"substates",
"dirty_vars",
"dirty_substates",
):
continue
prop = getattr(cls, field)
assert isinstance(prop, BaseVar)
assert prop.name == field
assert cls.num1.type_ == int
assert cls.num2.type_ == float
assert cls.key.type_ == str
def test_computed_class_var(state):
"""Test that the class computed vars are set correctly.
Args:
state: A state.
"""
cls = type(state)
vars = [(prop.name, prop.type_) for prop in cls.computed_vars.values()]
assert ("sum", float) in vars
assert ("upper", str) in vars
def test_class_vars(state):
"""Test that the class vars are set correctly.
Args:
state: A state.
"""
cls = type(state)
assert set(cls.vars.keys()) == {
"num1",
"num2",
"key",
"array",
"mapping",
"obj",
"complex",
"sum",
"upper",
}
def test_default_value(state):
"""Test that the default value of a var is correct.
Args:
state: A state.
"""
assert state.num1 == 0
assert state.num2 == 3.14
assert state.key == ""
assert state.sum == 3.14
assert state.upper == ""
def test_computed_vars(state):
"""Test that the computed var is computed correctly.
Args:
state: A state.
"""
state.num1 = 1
state.num2 = 4
assert state.sum == 5
state.key = "hello world"
assert state.upper == "HELLO WORLD"
def test_dict(state):
"""Test that the dict representation of a state is correct.
Args:
state: A state.
"""
assert set(state.dict().keys()) == set(state.vars.keys())
assert set(state.dict(include_computed=False).keys()) == set(state.base_vars)
def test_default_setters(TestState):
"""Test that we can set default values.
Args:
TestState: The state class.
"""
state = TestState()
for prop_name in state.base_vars:
# Each base var should have a default setter.
assert hasattr(state, f"set_{prop_name}")
def test_class_indexing_with_vars(TestState):
"""Test that we can index into a state var with another var.
Args:
TestState: The state class.
"""
prop = TestState.array[TestState.num1]
assert str(prop) == "{test_state.array[test_state.num1]}"
prop = TestState.mapping["a"][TestState.num1]
assert str(prop) == '{test_state.mapping["a"][test_state.num1]}'
def test_class_attributes(TestState):
"""Test that we can get class attributes.
Args:
TestState: The state class.
"""
prop = TestState.obj.prop1
assert str(prop) == "{test_state.obj.prop1}"
prop = TestState.complex[1].prop1
assert str(prop) == "{test_state.complex[1].prop1}"
def test_get_parent_state(TestState, ChildState, ChildState2, GrandchildState):
"""Test getting the parent state.
Args:
TestState: The state class.
ChildState: The child state class.
ChildState2: The child state class.
GrandchildState: The grandchild state class.
"""
assert TestState.get_parent_state() is None
assert ChildState.get_parent_state() == TestState
assert ChildState2.get_parent_state() == TestState
assert GrandchildState.get_parent_state() == ChildState
def test_get_substates(TestState, ChildState, ChildState2, GrandchildState):
"""Test getting the substates.
Args:
TestState: The state class.
ChildState: The child state class.
ChildState2: The child state class.
GrandchildState: The grandchild state class.
"""
assert TestState.get_substates() == {ChildState, ChildState2}
assert ChildState.get_substates() == {GrandchildState}
assert ChildState2.get_substates() == set()
assert GrandchildState.get_substates() == set()
def test_get_name(TestState, ChildState, ChildState2, GrandchildState):
"""Test getting the name of a state.
Args:
TestState: The state class.
ChildState: The child state class.
ChildState2: The child state class.
GrandchildState: The grandchild state class.
"""
assert TestState.get_name() == "test_state"
assert ChildState.get_name() == "child_state"
assert ChildState2.get_name() == "child_state2"
assert GrandchildState.get_name() == "grandchild_state"
def test_get_full_name(TestState, ChildState, ChildState2, GrandchildState):
"""Test getting the full name.
Args:
TestState: The state class.
ChildState: The child state class.
ChildState2: The child state class.
GrandchildState: The grandchild state class.
"""
assert TestState.get_full_name() == "test_state"
assert ChildState.get_full_name() == "test_state.child_state"
assert ChildState2.get_full_name() == "test_state.child_state2"
assert GrandchildState.get_full_name() == "test_state.child_state.grandchild_state"
def test_get_class_substate(TestState, ChildState, ChildState2, GrandchildState):
"""Test getting the substate of a class.
Args:
TestState: The state class.
ChildState: The child state class.
ChildState2: The child state class.
GrandchildState: The grandchild state class.
"""
assert TestState.get_class_substate(("child_state",)) == ChildState
assert TestState.get_class_substate(("child_state2",)) == ChildState2
assert ChildState.get_class_substate(("grandchild_state",)) == GrandchildState
assert (
TestState.get_class_substate(("child_state", "grandchild_state"))
== GrandchildState
)
with pytest.raises(ValueError):
TestState.get_class_substate(("invalid_child",))
with pytest.raises(ValueError):
TestState.get_class_substate(
(
"child_state",
"invalid_child",
)
)
def test_get_class_var(TestState, ChildState, ChildState2, GrandchildState):
"""Test getting the var of a class.
Args:
TestState: The state class.
ChildState: The child state class.
ChildState2: The child state class.
GrandchildState: The grandchild state class.
"""
assert TestState.get_class_var(("num1",)) == TestState.num1
assert TestState.get_class_var(("num2",)) == TestState.num2
assert ChildState.get_class_var(("value",)) == ChildState.value
assert GrandchildState.get_class_var(("value2",)) == GrandchildState.value2
assert TestState.get_class_var(("child_state", "value")) == ChildState.value
assert (
TestState.get_class_var(("child_state", "grandchild_state", "value2"))
== GrandchildState.value2
)
assert (
ChildState.get_class_var(("grandchild_state", "value2"))
== GrandchildState.value2
)
with pytest.raises(ValueError):
TestState.get_class_var(("invalid_var",))
with pytest.raises(ValueError):
TestState.get_class_var(
(
"child_state",
"invalid_var",
)
)
def test_set_class_var(TestState):
"""Test setting the var of a class.
Args:
TestState: The state class.
"""
with pytest.raises(AttributeError):
TestState.num3
TestState._set_var(BaseVar(name="num3", type_=int).set_state(TestState))
var = TestState.num3
assert var.name == "num3"
assert var.type_ == int
assert var.state == TestState.get_full_name()
def test_set_parent_and_substates(TestState, ChildState, ChildState2, GrandchildState):
"""Test setting the parent and substates.
Args:
TestState: The state class.
ChildState: The child state class.
ChildState2: The child state class.
GrandchildState: The grandchild state class.
"""
test_state = TestState()
assert len(test_state.substates) == 2
assert set(test_state.substates) == {"child_state", "child_state2"}
child_state = test_state.substates["child_state"]
assert child_state.parent_state == test_state
assert len(child_state.substates) == 1
assert set(child_state.substates) == {"grandchild_state"}
grandchild_state = child_state.substates["grandchild_state"]
assert grandchild_state.parent_state == child_state
assert len(grandchild_state.substates) == 0
def test_get_child_attribute(TestState, ChildState, ChildState2, GrandchildState):
"""Test getting the attribute of a state.
Args:
TestState: The state class.
ChildState: The child state class.
ChildState2: The child state class.
GrandchildState: The grandchild state class.
"""
test_state = TestState()
assert test_state.num1 == 0
assert test_state.child_state.value == ""
assert test_state.child_state2.value == ""
assert test_state.child_state.count == 23
assert test_state.child_state.grandchild_state.value2 == ""
with pytest.raises(AttributeError):
test_state.invalid
with pytest.raises(AttributeError):
test_state.child_state.invalid
with pytest.raises(AttributeError):
test_state.child_state.grandchild_state.invalid
def test_set_child_attribute(TestState, ChildState, ChildState2, GrandchildState):
"""Test setting the attribute of a state.
Args:
TestState: The state class.
ChildState: The child state class.
ChildState2: The child state class.
GrandchildState: The grandchild state class.
"""
test_state = TestState()
child_state = test_state.child_state
grandchild_state = child_state.grandchild_state
test_state.num1 = 10
assert test_state.num1 == 10
test_state.child_state.value = "test"
assert test_state.child_state.value == "test"
assert child_state.value == "test"
test_state.child_state.grandchild_state.value2 = "test2"
assert test_state.child_state.grandchild_state.value2 == "test2"
assert child_state.grandchild_state.value2 == "test2"
assert grandchild_state.value2 == "test2"
def test_get_parent_attribute(TestState, ChildState, ChildState2, GrandchildState):
"""Test setting the attribute of a state.
Args:
TestState: The state class.
ChildState: The child state class.
ChildState2: The child state class.
GrandchildState: The grandchild state class.
"""
test_state = TestState()
child_state = test_state.child_state
grandchild_state = child_state.grandchild_state
assert test_state.num1 == 0
assert child_state.num1 == 0
assert grandchild_state.num1 == 0
# Changing the parent var should change the child var.
test_state.num1 = 1
assert test_state.num1 == 1
assert child_state.num1 == 1
assert grandchild_state.num1 == 1
child_state.value = "test"
assert test_state.child_state.value == "test"
assert child_state.value == "test"
assert grandchild_state.value == "test"
def test_set_parent_attribute(TestState, ChildState, ChildState2, GrandchildState):
"""Test setting the attribute of a state.
Args:
TestState: The state class.
ChildState: The child state class.
ChildState2: The child state class.
GrandchildState: The grandchild state class.
"""
test_state = TestState()
child_state = test_state.child_state
grandchild_state = child_state.grandchild_state
# Changing the child var should not change the parent var.
child_state.num1 = 2
assert child_state.num1 == 2
assert test_state.num1 == 2
assert grandchild_state.num1 == 2
grandchild_state.num1 = 3
assert grandchild_state.num1 == 3
assert child_state.num1 == 3
assert test_state.num1 == 3
grandchild_state.value = "test2"
assert grandchild_state.value == "test2"
assert child_state.value == "test2"
def test_get_substate(TestState, ChildState, ChildState2, GrandchildState):
"""Test getting the substate of a state.
Args:
TestState: The state class.
ChildState: The child state class.
ChildState2: The child state class.
GrandchildState: The grandchild state class.
"""
test_state = TestState()
child_state = test_state.child_state
grandchild_state = child_state.grandchild_state
assert test_state.get_substate(("child_state",)) == child_state
assert test_state.get_substate(("child_state2",)) == test_state.child_state2
assert (
test_state.get_substate(("child_state", "grandchild_state")) == grandchild_state
)
assert child_state.get_substate(("grandchild_state",)) == grandchild_state
with pytest.raises(ValueError):
test_state.get_substate(("invalid",))
with pytest.raises(ValueError):
test_state.get_substate(("child_state", "invalid"))
with pytest.raises(ValueError):
test_state.get_substate(("child_state", "grandchild_state", "invalid"))
def test_set_dirty_var(TestState):
"""Test changing state vars marks the value as dirty.
Args:
TestState: The state class.
"""
test_state = TestState()
# Initially there should be no dirty vars.
assert test_state.dirty_vars == set()
# Setting a var should mark it as dirty.
test_state.num1 = 1
assert test_state.dirty_vars == {"num1"}
# Setting another var should mark it as dirty.
test_state.num2 = 2
assert test_state.dirty_vars == {"num1", "num2"}
# Cleaning the state should remove all dirty vars.
test_state.clean()
assert test_state.dirty_vars == set()
def test_set_dirty_substate(TestState, ChildState, ChildState2, GrandchildState):
"""Test changing substate vars marks the value as dirty.
Args:
TestState: The state class.
ChildState: The child state class.
ChildState2: The child state class.
GrandchildState: The grandchild state class.
"""
test_state = TestState()
child_state = test_state.child_state
child_state2 = test_state.child_state2
grandchild_state = child_state.grandchild_state
# Initially there should be no dirty vars.
assert test_state.dirty_vars == set()
assert child_state.dirty_vars == set()
assert child_state2.dirty_vars == set()
assert grandchild_state.dirty_vars == set()
# Setting a var should mark it as dirty.
child_state.value = "test"
assert child_state.dirty_vars == {"value"}
assert test_state.dirty_substates == {"child_state"}
assert child_state.dirty_substates == set()
# Cleaning the parent state should remove the dirty substate.
test_state.clean()
assert test_state.dirty_substates == set()
assert child_state.dirty_vars == set()
# Setting a var on the grandchild should bubble up.
grandchild_state.value2 = "test2"
assert child_state.dirty_substates == {"grandchild_state"}
assert test_state.dirty_substates == {"child_state"}
# Cleaning the middle state should keep the parent state dirty.
child_state.clean()
assert test_state.dirty_substates == {"child_state"}
assert child_state.dirty_substates == set()
assert grandchild_state.dirty_vars == set()
def test_reset(TestState, ChildState):
"""Test resetting the state.
Args:
TestState: The state class.
ChildState: The child state class.
"""
test_state = TestState()
child_state = test_state.child_state
# Set some values.
test_state.num1 = 1
test_state.num2 = 2
child_state.value = "test"
# Reset the state.
test_state.reset()
# The values should be reset.
assert test_state.num1 == 0
assert test_state.num2 == 3.14
assert child_state.value == ""
# The dirty vars should be reset.
assert test_state.dirty_vars == set()
assert child_state.dirty_vars == set()
# The dirty substates should be reset.
assert test_state.dirty_substates == set()
@pytest.mark.asyncio
async def test_process_event_simple(TestState):
"""Test processing an event.
Args:
TestState: The state class.
"""
test_state = TestState()
assert test_state.num1 == 0
event = Event(token="t", name="set_num1", payload={"value": 69})
update = await test_state.process(event)
# The event should update the value.
assert test_state.num1 == 69
# The delta should contain the changes, including computed vars.
assert update.delta == {"test_state": {"num1": 69, "sum": 72.14, "upper": ""}}
assert update.events == []
@pytest.mark.asyncio
async def test_process_event_substate(TestState, ChildState, GrandchildState):
"""Test processing an event on a substate.
Args:
TestState: The state class.
ChildState: The child state class.
GrandchildState: The grandchild state class.
"""
test_state = TestState()
child_state = test_state.child_state
grandchild_state = child_state.grandchild_state
# Events should bubble down to the substate.
assert child_state.value == ""
assert child_state.count == 23
event = Event(
token="t", name="child_state.change_both", payload={"value": "hi", "count": 12}
)
update = await test_state.process(event)
assert child_state.value == "HI"
assert child_state.count == 24
assert update.delta == {
"test_state.child_state": {"value": "HI", "count": 24},
"test_state": {"sum": 3.14, "upper": ""},
}
test_state.clean()
# Test with the granchild state.
assert grandchild_state.value2 == ""
event = Event(
token="t",
name="child_state.grandchild_state.set_value2",
payload={"value": "new"},
)
update = await test_state.process(event)
assert grandchild_state.value2 == "new"
assert update.delta == {
"test_state.child_state.grandchild_state": {"value2": "new"},
"test_state": {"sum": 3.14, "upper": ""},
}
@pytest.mark.asyncio
async def test_process_event_substate_set_parent_state(TestState, ChildState):
"""Test setting the parent state on a substate.
Args:
TestState: The state class.
ChildState: The child state class.
"""
test_state = TestState()
event = Event(token="t", name="child_state.set_num1", payload={"value": 69})
update = await test_state.process(event)
assert test_state.num1 == 69
assert update.delta == {"test_state": {"num1": 69, "sum": 72.14, "upper": ""}}