pydase/tests/data_service/test_state_manager.py

353 lines
9.9 KiB
Python

import json
from pathlib import Path
from typing import Any
import pydase
import pydase.components
import pydase.units as u
from pydase.data_service.data_service_observer import DataServiceObserver
from pydase.data_service.state_manager import (
StateManager,
has_load_state_decorator,
load_state,
)
from pytest import LogCaptureFixture
class SubService(pydase.DataService):
name = "SubService"
class State(pydase.components.ColouredEnum):
RUNNING = "#0000FF80"
COMPLETED = "hsl(120, 100%, 50%)"
FAILED = "hsla(0, 100%, 50%, 0.7)"
class MySlider(pydase.components.NumberSlider):
@property
def min(self) -> float:
return self._min
@min.setter
@load_state
def min(self, value: float) -> None:
self._min = value
@property
def max(self) -> float:
return self._max
@max.setter
@load_state
def max(self, value: float) -> None:
self._max = value
@property
def step_size(self) -> float:
return self._step_size
@step_size.setter
@load_state
def step_size(self, value: float) -> None:
self._step_size = value
@property
def value(self) -> float:
return self._value
@value.setter
@load_state
def value(self, value: float) -> None:
if value < self._min or value > self._max:
raise ValueError("Value is either below allowed min or above max value.")
self._value = value
class Service(pydase.DataService):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.subservice = SubService()
self.some_unit: u.Quantity = 1.2 * u.units.A
self.some_float = 1.0
self.list_attr = [1.0, 2.0]
self._property_attr = 1337.0
self._name = "Service"
self.state = State.RUNNING
self.my_slider = MySlider()
@property
def name(self) -> str:
return self._name
@property
def property_attr(self) -> float:
return self._property_attr
@property_attr.setter
def property_attr(self, value: float) -> None:
self._property_attr = value
CURRENT_STATE = Service().serialize()["value"]
LOAD_STATE = {
"list_attr": {
"type": "list",
"value": [
{"type": "float", "value": 1.4, "readonly": False, "doc": None},
{"type": "float", "value": 2.0, "readonly": False, "doc": None},
],
"readonly": False,
"doc": None,
},
"my_slider": {
"type": "NumberSlider",
"value": {
"max": {
"type": "float",
"value": 101.0,
"readonly": False,
"doc": "The min property.",
},
"min": {
"type": "float",
"value": 1.0,
"readonly": False,
"doc": "The min property.",
},
"step_size": {
"type": "float",
"value": 2.0,
"readonly": False,
"doc": "The min property.",
},
"value": {
"type": "float",
"value": 1.0,
"readonly": False,
"doc": "The value property.",
},
},
"readonly": False,
"doc": None,
},
"name": {
"type": "str",
"value": "Another name",
"readonly": True,
"doc": None,
},
"some_float": {
"type": "int",
"value": 10,
"readonly": False,
"doc": None,
},
"property_attr": {
"type": "float",
"value": 1337.1,
"readonly": False,
"doc": None,
},
"some_unit": {
"type": "Quantity",
"value": {"magnitude": 12.0, "unit": "A"},
"readonly": False,
"doc": None,
},
"state": {
"type": "ColouredEnum",
"value": "FAILED",
"readonly": True,
"doc": None,
"enum": {
"RUNNING": "#0000FF80",
"COMPLETED": "hsl(120, 100%, 50%)",
"FAILED": "hsla(0, 100%, 50%, 0.7)",
},
},
"subservice": {
"type": "DataService",
"value": {
"name": {
"type": "str",
"value": "SubService",
"readonly": False,
"doc": None,
}
},
"readonly": False,
"doc": None,
},
"removed_attr": {
"type": "str",
"value": "removed",
"readonly": False,
"doc": None,
},
}
def test_save_state(tmp_path: Path) -> None:
# Create a StateManager instance with a temporary file
file = tmp_path / "test_state.json"
manager = StateManager(service=Service(), filename=str(file))
# Trigger the saving action
manager.save_state()
# Now check that the file was written correctly
assert file.read_text() == json.dumps(CURRENT_STATE, indent=4)
def test_load_state(tmp_path: Path, caplog: LogCaptureFixture) -> None:
# Create a StateManager instance with a temporary file
file = tmp_path / "test_state.json"
# Write a temporary JSON file to read back
with open(file, "w") as f:
json.dump(LOAD_STATE, f, indent=4)
service = Service()
state_manager = StateManager(service=service, filename=str(file))
DataServiceObserver(state_manager)
state_manager.load_state()
assert service.some_unit == u.Quantity(12, "A") # has changed
assert service.list_attr[0] == 1.4 # has changed
assert service.list_attr[1] == 2.0 # has not changed
assert (
service.property_attr == 1337
) # has not changed as property has not @load_state decorator
assert service.state == State.FAILED # has changed
assert service.name == "Service" # has not changed as readonly
assert service.some_float == 1.0 # has not changed due to different type
assert service.subservice.name == "SubService" # didn't change
assert service.my_slider.value == 1.0 # changed
assert service.my_slider.min == 1.0 # changed
assert service.my_slider.max == 101.0 # changed
assert service.my_slider.step_size == 2.0 # changed
assert "'some_unit' changed to '12.0 A'" in caplog.text
assert (
"Property 'name' has no '@load_state' decorator. "
"Ignoring value from JSON file..." in caplog.text
)
assert (
"Attribute type of 'some_float' changed from 'int' to 'float'. "
"Ignoring value from JSON file..."
) in caplog.text
assert (
"Path 'removed_attr' could not be loaded. It does not correspond to an "
"attribute of the class. Ignoring value from JSON file..." in caplog.text
)
assert "Value of attribute 'subservice.name' has not changed..." in caplog.text
assert "'my_slider.value' changed to '1.0'" in caplog.text
assert "'my_slider.min' changed to '1.0'" in caplog.text
assert "'my_slider.max' changed to '101.0'" in caplog.text
assert "'my_slider.step_size' changed to '2.0'" in caplog.text
def test_filename_error(caplog: LogCaptureFixture) -> None:
service = Service()
manager = StateManager(service=service)
manager.save_state()
assert (
"State manager was not initialised with a filename. Skipping 'save_state'..."
in caplog.text
)
def test_readonly_attribute(tmp_path: Path, caplog: LogCaptureFixture) -> None:
# Create a StateManager instance with a temporary file
file = tmp_path / "test_state.json"
# Write a temporary JSON file to read back
with open(file, "w") as f:
json.dump(LOAD_STATE, f, indent=4)
service = Service()
manager = StateManager(service=service, filename=str(file))
manager.load_state()
assert service.name == "Service"
assert (
"Property 'name' has no '@load_state' decorator. "
"Ignoring value from JSON file..." in caplog.text
)
def test_changed_type(tmp_path: Path, caplog: LogCaptureFixture) -> None:
# Create a StateManager instance with a temporary file
file = tmp_path / "test_state.json"
# Write a temporary JSON file to read back
with open(file, "w") as f:
json.dump(LOAD_STATE, f, indent=4)
service = Service()
manager = StateManager(service=service, filename=str(file))
manager.load_state()
assert (
"Attribute type of 'some_float' changed from 'int' to "
"'float'. Ignoring value from JSON file..."
) in caplog.text
def test_property_load_state(tmp_path: Path) -> None:
# Create a StateManager instance with a temporary file
file = tmp_path / "test_state.json"
LOAD_STATE = {
"name": {
"type": "str",
"value": "Some other name",
"readonly": False,
"doc": None,
},
"not_loadable_attr": {
"type": "str",
"value": "But I AM loadable!?",
"readonly": False,
"doc": None,
},
}
# Write a temporary JSON file to read back
with open(file, "w") as f:
json.dump(LOAD_STATE, f, indent=4)
class Service(pydase.DataService):
_name = "Service"
_not_loadable_attr = "Not loadable"
@property
def name(self) -> str:
return self._name
@name.setter
@load_state
def name(self, value: str) -> None:
self._name = value
@property
def not_loadable_attr(self) -> str:
return self._not_loadable_attr
@not_loadable_attr.setter
def not_loadable_attr(self, value: str) -> None:
self._not_loadable_attr = value
@property
def property_without_setter(self) -> None:
return
service_instance = Service()
StateManager(service_instance, filename=file).load_state()
assert service_instance.name == "Some other name"
assert service_instance.not_loadable_attr == "Not loadable"
assert not has_load_state_decorator(type(service_instance).property_without_setter)