From 92d11b07672b3c71324bb0fb1a92f0e148040404 Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Fri, 1 Nov 2024 22:51:29 +0100 Subject: [PATCH] Moved files to new branch to avoid weird git bug Signed-off-by: Lorenzo Curcio --- dapr/actor/runtime/mock_actor.py | 121 ++++++++ dapr/actor/runtime/mock_state_manager.py | 235 ++++++++++++++++ dapr/actor/runtime/state_manager.py | 10 +- .../_index.md} | 0 .../python-sdk-actors/python-mock-actors.md | 66 +++++ tests/actor/test_mock_actor.py | 264 ++++++++++++++++++ 6 files changed, 693 insertions(+), 3 deletions(-) create mode 100644 dapr/actor/runtime/mock_actor.py create mode 100644 dapr/actor/runtime/mock_state_manager.py rename daprdocs/content/en/python-sdk-docs/{python-actor.md => python-sdk-actors/_index.md} (100%) create mode 100644 daprdocs/content/en/python-sdk-docs/python-sdk-actors/python-mock-actors.md create mode 100644 tests/actor/test_mock_actor.py diff --git a/dapr/actor/runtime/mock_actor.py b/dapr/actor/runtime/mock_actor.py new file mode 100644 index 000000000..a932d8dde --- /dev/null +++ b/dapr/actor/runtime/mock_actor.py @@ -0,0 +1,121 @@ +""" +Copyright 2023 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from datetime import timedelta +from typing import Any, Optional, TypeVar + +from dapr.actor.id import ActorId +from dapr.actor.runtime._reminder_data import ActorReminderData +from dapr.actor.runtime._timer_data import TIMER_CALLBACK, ActorTimerData +from dapr.actor.runtime.actor import Actor +from dapr.actor.runtime.mock_state_manager import MockStateManager +from dapr.actor.runtime.state_manager import ActorStateManager + + +class MockActor(Actor): + """A mock actor class to be used to override certain Actor methods for unit testing. + To be used only via the create_mock_actor function, which takes in a class and returns a + mock actor object for that class. + + Examples: + class SomeActorInterface(ActorInterface): + @actor_method(name="method") + async def set_state(self, data: dict) -> None: + + class SomeActor(Actor, SomeActorInterface): + async def set_state(self, data: dict) -> None: + await self._state_manager.set_state('state', data) + await self._state_manager.save_state() + + mock_actor = create_mock_actor(SomeActor) + assert mock_actor._state_manager._mock_state == {} + await mock_actor.set_state({"test":10}) + assert mock_actor._state_manager._mock_state == {"test":10} + """ + + def __init__(self, actor_id: str, initstate: Optional[dict]): + self.id = ActorId(actor_id) + self._runtime_ctx = None + self._state_manager: ActorStateManager = MockStateManager(self, initstate) + + async def register_timer( + self, + name: Optional[str], + callback: TIMER_CALLBACK, + state: Any, + due_time: timedelta, + period: timedelta, + ttl: Optional[timedelta] = None, + ) -> None: + """Adds actor timer to self._state_manager._mock_timers. + Args: + name (str): the name of the timer to register. + callback (Callable): An awaitable callable which will be called when the timer fires. + state (Any): An object which will pass to the callback method, or None. + due_time (datetime.timedelta): the amount of time to delay before the awaitable + callback is first invoked. + period (datetime.timedelta): the time interval between invocations + of the awaitable callback. + ttl (Optional[datetime.timedelta]): the time interval before the timer stops firing + """ + name = name or self.__get_new_timer_name() + timer = ActorTimerData(name, callback, state, due_time, period, ttl) + self._state_manager._mock_timers[name] = timer + + async def unregister_timer(self, name: str) -> None: + """Unregisters actor timer from self._state_manager._mock_timers. + + Args: + name (str): the name of the timer to unregister. + """ + self._state_manager._mock_timers.pop(name, None) + + async def register_reminder( + self, + name: str, + state: bytes, + due_time: timedelta, + period: timedelta, + ttl: Optional[timedelta] = None, + ) -> None: + """Adds actor reminder to self._state_manager._mock_reminders. + + Args: + name (str): the name of the reminder to register. the name must be unique per actor. + state (bytes): the user state passed to the reminder invocation. + due_time (datetime.timedelta): the amount of time to delay before invoking the reminder + for the first time. + period (datetime.timedelta): the time interval between reminder invocations after + the first invocation. + ttl (datetime.timedelta): the time interval before the reminder stops firing + """ + reminder = ActorReminderData(name, state, due_time, period, ttl) + self._state_manager._mock_reminders[name] = reminder + + async def unregister_reminder(self, name: str) -> None: + """Unregisters actor reminder from self._state_manager._mock_reminders.. + + Args: + name (str): the name of the reminder to unregister. + """ + self._state_manager._mock_reminders.pop(name, None) + + +T = TypeVar('T', bound=Actor) + + +def create_mock_actor(cls1: type[T], actor_id: str, initstate: Optional[dict] = None) -> T: + class MockSuperClass(MockActor, cls1): + pass + + return MockSuperClass(actor_id, initstate) # type: ignore diff --git a/dapr/actor/runtime/mock_state_manager.py b/dapr/actor/runtime/mock_state_manager.py new file mode 100644 index 000000000..71568f1db --- /dev/null +++ b/dapr/actor/runtime/mock_state_manager.py @@ -0,0 +1,235 @@ +""" +Copyright 2023 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import asyncio +from contextvars import ContextVar +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, TypeVar + +from dapr.actor.runtime._reminder_data import ActorReminderData +from dapr.actor.runtime._timer_data import ActorTimerData +from dapr.actor.runtime.state_change import ActorStateChange, StateChangeKind +from dapr.actor.runtime.state_manager import ActorStateManager, StateMetadata + +if TYPE_CHECKING: + from dapr.actor.runtime.mock_actor import MockActor + +T = TypeVar('T') +CONTEXT: ContextVar[Optional[Dict[str, Any]]] = ContextVar('state_tracker_context') + + +class MockStateManager(ActorStateManager): + def __init__(self, actor: 'MockActor', initstate: Optional[dict]): + self._actor = actor + self._default_state_change_tracker: Dict[str, StateMetadata] = {} + self._mock_state: dict[str, Any] = {} + self._mock_timers: dict[str, ActorTimerData] = {} + self._mock_reminders: dict[str, ActorReminderData] = {} + if initstate: + self._mock_state = initstate + + async def add_state(self, state_name: str, value: T) -> None: + if not await self.try_add_state(state_name, value): + raise ValueError(f'The actor state name {state_name} already exist.') + + async def try_add_state(self, state_name: str, value: T) -> bool: + if state_name in self._default_state_change_tracker: + state_metadata = self._default_state_change_tracker[state_name] + if state_metadata.change_kind == StateChangeKind.remove: + self._default_state_change_tracker[state_name] = StateMetadata( + value, StateChangeKind.update + ) + return True + return False + existed = state_name in self._mock_state + if not existed: + return False + self._default_state_change_tracker[state_name] = StateMetadata(value, StateChangeKind.add) + self._mock_state[state_name] = value + return True + + async def get_state(self, state_name: str) -> Optional[T]: + has_value, val = await self.try_get_state(state_name) + if has_value: + return val + else: + raise KeyError(f'Actor State with name {state_name} was not found.') + + async def try_get_state(self, state_name: str) -> Tuple[bool, Optional[T]]: + if state_name in self._default_state_change_tracker: + state_metadata = self._default_state_change_tracker[state_name] + if state_metadata.change_kind == StateChangeKind.remove: + return False, None + return True, state_metadata.value + has_value = state_name in self._mock_state + val = self._mock_state.get(state_name) + if has_value: + self._default_state_change_tracker[state_name] = StateMetadata( + val, StateChangeKind.none + ) + return has_value, val + + async def set_state(self, state_name: str, value: T) -> None: + await self.set_state_ttl(state_name, value, None) + + async def set_state_ttl(self, state_name: str, value: T, ttl_in_seconds: Optional[int]) -> None: + if ttl_in_seconds is not None and ttl_in_seconds < 0: + return + + if state_name in self._default_state_change_tracker: + state_metadata = self._default_state_change_tracker[state_name] + state_metadata.value = value + state_metadata.ttl_in_seconds = ttl_in_seconds + + if ( + state_metadata.change_kind == StateChangeKind.none + or state_metadata.change_kind == StateChangeKind.remove + ): + state_metadata.change_kind = StateChangeKind.update + self._default_state_change_tracker[state_name] = state_metadata + self._mock_state[state_name] = value + return + + existed = state_name in self._mock_state + if existed: + self._default_state_change_tracker[state_name] = StateMetadata( + value, StateChangeKind.update, ttl_in_seconds + ) + else: + self._default_state_change_tracker[state_name] = StateMetadata( + value, StateChangeKind.add, ttl_in_seconds + ) + self._mock_state[state_name] = value + + async def remove_state(self, state_name: str) -> None: + if not await self.try_remove_state(state_name): + raise KeyError(f'Actor State with name {state_name} was not found.') + + async def try_remove_state(self, state_name: str) -> bool: + if state_name in self._default_state_change_tracker: + state_metadata = self._default_state_change_tracker[state_name] + if state_metadata.change_kind == StateChangeKind.remove: + return False + elif state_metadata.change_kind == StateChangeKind.add: + self._default_state_change_tracker.pop(state_name, None) + self._mock_state.pop(state_name, None) + return True + self._mock_state.pop(state_name, None) + state_metadata.change_kind = StateChangeKind.remove + return True + + existed = state_name in self._mock_state + if existed: + self._default_state_change_tracker[state_name] = StateMetadata( + None, StateChangeKind.remove + ) + self._mock_state.pop(state_name, None) + return True + return False + + async def contains_state(self, state_name: str) -> bool: + if state_name in self._default_state_change_tracker: + state_metadata = self._default_state_change_tracker[state_name] + return state_metadata.change_kind != StateChangeKind.remove + return state_name in self._mock_state + + async def get_or_add_state(self, state_name: str, value: T) -> Optional[T]: + has_value, val = await self.try_get_state(state_name) + if has_value: + return val + change_kind = ( + StateChangeKind.update + if self.is_state_marked_for_remove(state_name) + else StateChangeKind.add + ) + self._default_state_change_tracker[state_name] = StateMetadata(value, change_kind) + return value + + async def add_or_update_state( + self, state_name: str, value: T, update_value_factory: Callable[[str, T], T] + ) -> T: + if not callable(update_value_factory): + raise AttributeError('update_value_factory is not callable') + + if state_name in self._default_state_change_tracker: + state_metadata = self._default_state_change_tracker[state_name] + if state_metadata.change_kind == StateChangeKind.remove: + self._default_state_change_tracker[state_name] = StateMetadata( + value, StateChangeKind.update + ) + self._mock_state[state_name] = value + return value + new_value = update_value_factory(state_name, state_metadata.value) + state_metadata.value = new_value + if state_metadata.change_kind == StateChangeKind.none: + state_metadata.change_kind = StateChangeKind.update + self._default_state_change_tracker[state_name] = state_metadata + self._mock_state[state_name] = value + return new_value + + has_value = state_name in self._mock_state + val: Any = self._mock_state.get(state_name) + if has_value: + new_value = update_value_factory(state_name, val) + self._default_state_change_tracker[state_name] = StateMetadata( + new_value, StateChangeKind.update + ) + return new_value + self._default_state_change_tracker[state_name] = StateMetadata(value, StateChangeKind.add) + return value + + async def get_state_names(self) -> List[str]: + # TODO: Get all state names from Dapr once implemented. + def append_names_sync(): + state_names = [] + for key, value in self._default_state_change_tracker.items(): + if value.change_kind == StateChangeKind.add: + state_names.append(key) + elif value.change_kind == StateChangeKind.remove: + state_names.append(key) + return state_names + + default_loop = asyncio.get_running_loop() + return await default_loop.run_in_executor(None, append_names_sync) + + async def clear_cache(self) -> None: + self._default_state_change_tracker.clear() + + async def save_state(self) -> None: + if len(self._default_state_change_tracker) == 0: + return + + state_changes = [] + states_to_remove = [] + for state_name, state_metadata in self._default_state_change_tracker.items(): + if state_metadata.change_kind == StateChangeKind.none: + continue + state_changes.append( + ActorStateChange( + state_name, + state_metadata.value, + state_metadata.change_kind, + state_metadata.ttl_in_seconds, + ) + ) + if state_metadata.change_kind == StateChangeKind.remove: + states_to_remove.append(state_name) + # Mark the states as unmodified so that tracking for next invocation is done correctly. + state_metadata.change_kind = StateChangeKind.none + for state_name in states_to_remove: + self._default_state_change_tracker.pop(state_name, None) + + def is_state_marked_for_remove(self, state_name: str) -> bool: + return ( + state_name in self._default_state_change_tracker + and self._default_state_change_tracker[state_name].change_kind == StateChangeKind.remove + ) diff --git a/dapr/actor/runtime/state_manager.py b/dapr/actor/runtime/state_manager.py index 7132175b3..f9c996f0a 100644 --- a/dapr/actor/runtime/state_manager.py +++ b/dapr/actor/runtime/state_manager.py @@ -15,13 +15,14 @@ import asyncio from contextvars import ContextVar +from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, List, Optional, Tuple, TypeVar -from dapr.actor.runtime.state_change import StateChangeKind, ActorStateChange from dapr.actor.runtime.reentrancy_context import reentrancy_ctx - -from typing import Any, Callable, Dict, Generic, List, Tuple, TypeVar, Optional, TYPE_CHECKING +from dapr.actor.runtime.state_change import ActorStateChange, StateChangeKind if TYPE_CHECKING: + from dapr.actor.runtime._reminder_data import ActorReminderData + from dapr.actor.runtime._timer_data import ActorTimerData from dapr.actor.runtime.actor import Actor T = TypeVar('T') @@ -69,6 +70,9 @@ def __init__(self, actor: 'Actor'): self._type_name = actor.runtime_ctx.actor_type_info.type_name self._default_state_change_tracker: Dict[str, StateMetadata] = {} + self._mock_state: dict[str, Any] + self._mock_timers: dict[str, ActorTimerData] + self._mock_reminders: dict[str, ActorReminderData] async def add_state(self, state_name: str, value: T) -> None: if not await self.try_add_state(state_name, value): diff --git a/daprdocs/content/en/python-sdk-docs/python-actor.md b/daprdocs/content/en/python-sdk-docs/python-sdk-actors/_index.md similarity index 100% rename from daprdocs/content/en/python-sdk-docs/python-actor.md rename to daprdocs/content/en/python-sdk-docs/python-sdk-actors/_index.md diff --git a/daprdocs/content/en/python-sdk-docs/python-sdk-actors/python-mock-actors.md b/daprdocs/content/en/python-sdk-docs/python-sdk-actors/python-mock-actors.md new file mode 100644 index 000000000..59538ada2 --- /dev/null +++ b/daprdocs/content/en/python-sdk-docs/python-sdk-actors/python-mock-actors.md @@ -0,0 +1,66 @@ +--- +type: docs +title: "Dapr Python SDK mock actor unit tests" +linkTitle: "Mock Actors" +weight: 200000 +description: How to unit test actor methods using mock actors +--- + +The Dapr Python SDK provides the ability to create mock actors to unit test your actor methods and how they interact with the actor state. + +## Basic Usage +``` +from dapr.actor.runtime.mock_actor import create_mock_actor + +class MyActor(Actor, MyActorInterface): + async def save_state(self, data) -> None: + await self._state_manager.set_state('state', data) + await self._state_manager.save_state() + +mock_actor = create_mock_actor(MyActor, "id") + +await mock_actor.save_state(5) +assert mockactor._state_manager._mock_state == 5 #True +``` +Mock actors work by passing your actor class as well as the actor id (str) into the function create_mock_actor, which returns an instance of the actor with a bunch of the internal actor methods overwritten, such that instead of attempting to interact with Dapr to save state, manage timers, etc it instead only uses local variables. + +Those variables are: +* **_state_manager._mock_state()** +A [str, object] dict where all the actor state is stored. Any variable saved via _state_manager.save_state(key, value), or any other statemanager method is stored in the dict as that key, value combo. Any value loaded via try_get_state or any other statemanager method is taken from this dict. + +* **_state_manager._mock_timers()** +A [str, ActorTimerData] dict which holds the active actor timers. Any actor method which would add or remove a timer adds or pops the appropriate ActorTimerData object from this dict. + +* **_state_manager._mock_reminders()** +A [str, ActorReminderData] dict which holds the active actor reminders. Any actor method which would add or remove a timer adds or pops the appropriate ActorReminderData object from this dict. + +**Note: The timers and reminders will never actually trigger. The dictionaries exist only so methods that should add or remove timers/reminders can be tested. If you need to test the callbacks they should activate, you should call them directly with the appropriate values:** +``` +result = await mock_actor.recieve_reminder(name, state, due_time, period, _ttl) +# Test the result directly or test for side effects (like changing state) by querying _state_manager._mock_state +``` + +## Usage and Limitations + +**The \_on\_activate method will not be called automatically the way it is when Dapr initializes a new Actor instance. You should call it manually as needed as part of your tests.** + +The \_\_init\_\_, register_timer, unregister_timer, register_reminder, unregister_reminder methods are all overwritten by the MockActor class that gets applied as a mixin via create_mock_actor. If your actor itself overwrites these methods, those modifications will themselves be overwritten and the actor will likely not behave as you expect. + +*note: \_\_init\_\_ is a special case where you are expected to define it as* +``` + def __init__(self, ctx, actor_id): + super().__init__(ctx, actor_id) +``` +*Mock actors work fine with this, but if you have added any extra logic into \_\_init\_\_, it will be overwritten. It is worth noting that the correct way to apply logic on initialization is via \_on\_activate (which can also be safely used with mock actors) instead of \_\_init\_\_.* + +The actor _runtime_ctx variable is set to None. Obviously all the normal actor methods have been overwritten such as to not call it, but if your code itself interacts directly with _runtime_ctx, it will likely break. + +The actor _state_manager is overwritten with an instance of MockStateManager. This has all the same methods and functionality of the base ActorStateManager, except for using the various _mock variables for storing data instead of the _runtime_ctx. If your code implements its own custom state manager it will be overwritten and your code will likely break. + +## Type Hinting + +Because of Python's lack of a unified method for type hinting type intersections (see: [python/typing #213](https://github.com/python/typing/issues/213)), type hinting is unfortunately mostly broken with Mock Actors. The return type is type hinted as "instance of Actor subclass T" when it should really be type hinted as "instance of MockActor subclass T" or "instance of type intersection [Actor subclass T, MockActor]" (where, it is worth noting, MockActor is itself a subclass of Actor). + +This means that, for example, if you hover over ```mockactor._state_manager``` in a code editor, it will come up as an instance of ActorStateManager (instead of MockStateManager), and various IDE helper functions (like VSCode's ```Go to Definition```, which will bring you to the definition of ActorStateManager instead of MockStateManager) won't work properly. + +For now, this issue is unfixable, so it's merely something to be noted because of the confusion it might cause. If in the future it becomes possible to accurately type hint cases like this feel free to open an issue about implementing it. \ No newline at end of file diff --git a/tests/actor/test_mock_actor.py b/tests/actor/test_mock_actor.py new file mode 100644 index 000000000..66d3c7215 --- /dev/null +++ b/tests/actor/test_mock_actor.py @@ -0,0 +1,264 @@ +import datetime +import unittest +from typing import Optional + +from dapr.actor import Actor, ActorInterface, Remindable, actormethod +from dapr.actor.runtime.mock_actor import create_mock_actor +from dapr.actor.runtime.state_change import StateChangeKind + + +class MockTestActorInterface(ActorInterface): + @actormethod(name='GetData') + async def get_data(self) -> object: ... + + @actormethod(name='SetData') + async def set_data(self, data: object) -> None: ... + + @actormethod(name='ClearData') + async def clear_data(self) -> None: ... + + @actormethod(name='TestData') + async def test_data(self) -> int: ... + + @actormethod(name='AddDataNoSave') + async def add_data_no_save(self, data: object) -> None: ... + + @actormethod(name='RemoveDataNoSave') + async def remove_data_no_save(self) -> None: ... + + @actormethod(name='SaveState') + async def save_state(self) -> None: ... + + @actormethod(name='ToggleReminder') + async def toggle_reminder(self, name: str, enabled: bool) -> None: ... + + @actormethod(name='ToggleTimer') + async def toggle_timer(self, name: str, enabled: bool) -> None: ... + + +class MockTestActor(Actor, MockTestActorInterface, Remindable): + def __init__(self, ctx, actor_id): + super().__init__(ctx, actor_id) + + async def _on_activate(self) -> None: + await self._state_manager.set_state('state', {'test': 5}) + await self._state_manager.save_state() + + async def get_data(self) -> object: + _, val = await self._state_manager.try_get_state('state') + return val + + async def set_data(self, data) -> None: + await self._state_manager.set_state('state', data) + await self._state_manager.save_state() + + async def clear_data(self) -> None: + await self._state_manager.remove_state('state') + await self._state_manager.save_state() + + async def test_data(self) -> int: + _, val = await self._state_manager.try_get_state('state') + if val is None: + return 0 + if 'test' not in val: + return 1 + if val['test'] % 2 == 1: + return 2 + elif val['test'] % 2 == 0: + return 3 + return 4 + + async def add_data_no_save(self, data: object) -> None: + await self._state_manager.set_state('state', data) + + async def remove_data_no_save(self) -> None: + await self._state_manager.remove_state('state') + + async def save_state(self) -> None: + await self._state_manager.save_state() + + async def toggle_reminder(self, name: str, enabled: bool) -> None: + if enabled: + await self.register_reminder( + name, + b'reminder_state', + datetime.timedelta(seconds=5), + datetime.timedelta(seconds=10), + datetime.timedelta(seconds=15), + ) + else: + await self.unregister_reminder(name) + + async def toggle_timer(self, name: str, enabled: bool) -> None: + if enabled: + await self.register_timer( + name, + self.timer_callback, + 'timer_state', + datetime.timedelta(seconds=5), + datetime.timedelta(seconds=10), + datetime.timedelta(seconds=15), + ) + else: + await self.unregister_timer(name) + + async def receive_reminder( + self, + name: str, + state: bytes, + due_time: datetime.timedelta, + period: datetime.timedelta, + ttl: Optional[datetime.timedelta] = None, + ) -> None: + await self._state_manager.set_state(name, True) + await self._state_manager.save_state() + + async def timer_callback(self, state) -> None: + print('Timer triggered') + + +class ActorMockActorTests(unittest.IsolatedAsyncioTestCase): + def test_create_actor(self): + mockactor = create_mock_actor(MockTestActor, '1') + self.assertEqual(mockactor.id.id, '1') + + async def test_inistate(self): + mockactor = create_mock_actor(MockTestActor, '1', initstate={'state': 5}) + self.assertTrue('state' in mockactor._state_manager._mock_state) + self.assertEqual(mockactor._state_manager._mock_state['state'], 5) + + async def test_on_activate(self): + mockactor = create_mock_actor(MockTestActor, '1') + await mockactor._on_activate() + self.assertTrue('state' in mockactor._state_manager._mock_state) + self.assertEqual(mockactor._state_manager._mock_state['state'], {'test': 5}) + + async def test_get_data(self): + mockactor = create_mock_actor(MockTestActor, '1') + await mockactor._on_activate() + out1 = await mockactor.get_data() + self.assertEqual(out1, {'test': 5}) + + async def test_get_data_initstate(self): + mockactor = create_mock_actor(MockTestActor, '1', initstate={'state': {'test': 6}}) + out1 = await mockactor.get_data() + self.assertEqual(out1, {'test': 6}) + + async def test_set_data(self): + mockactor = create_mock_actor(MockTestActor, '1') + await mockactor._on_activate() + self.assertTrue('state' in mockactor._state_manager._mock_state) + self.assertEqual(mockactor._state_manager._mock_state['state'], {'test': 5}) + await mockactor.set_data({'test': 10}) + self.assertTrue('state' in mockactor._state_manager._mock_state) + self.assertEqual(mockactor._state_manager._mock_state['state'], {'test': 10}) + out1 = await mockactor.get_data() + self.assertEqual(out1, {'test': 10}) + + async def test_clear_data(self): + mockactor = create_mock_actor(MockTestActor, '1') + await mockactor._on_activate() + self.assertTrue('state' in mockactor._state_manager._mock_state) + self.assertEqual(mockactor._state_manager._mock_state['state'], {'test': 5}) + await mockactor.clear_data() + self.assertFalse('state' in mockactor._state_manager._mock_state) + self.assertIsNone(mockactor._state_manager._mock_state.get('state')) + out1 = await mockactor.get_data() + self.assertIsNone(out1) + + async def test_toggle_reminder(self): + mockactor = create_mock_actor(MockTestActor, '1') + await mockactor._on_activate() + self.assertEqual(len(mockactor._state_manager._mock_reminders), 0) + await mockactor.toggle_reminder('test', True) + self.assertEqual(len(mockactor._state_manager._mock_reminders), 1) + self.assertTrue('test' in mockactor._state_manager._mock_reminders) + reminderstate = mockactor._state_manager._mock_reminders['test'] + self.assertTrue(reminderstate.reminder_name, 'test') + await mockactor.toggle_reminder('test', False) + self.assertEqual(len(mockactor._state_manager._mock_reminders), 0) + + async def test_toggle_timer(self): + mockactor = create_mock_actor(MockTestActor, '1') + await mockactor._on_activate() + self.assertEqual(len(mockactor._state_manager._mock_timers), 0) + await mockactor.toggle_timer('test', True) + self.assertEqual(len(mockactor._state_manager._mock_timers), 1) + self.assertTrue('test' in mockactor._state_manager._mock_timers) + timerstate = mockactor._state_manager._mock_timers['test'] + self.assertTrue(timerstate.timer_name, 'test') + await mockactor.toggle_timer('test', False) + self.assertEqual(len(mockactor._state_manager._mock_timers), 0) + + async def test_activate_reminder(self): + mockactor = create_mock_actor(MockTestActor, '1') + await mockactor.receive_reminder( + 'test', + b'test1', + datetime.timedelta(days=1), + datetime.timedelta(days=1), + datetime.timedelta(days=1), + ) + self.assertEqual(mockactor._state_manager._mock_state['test'], True) + + async def test_test_data(self): + mockactor = create_mock_actor(MockTestActor, '1') + result = await mockactor.test_data() + self.assertEqual(result, 0) + await mockactor.set_data('lol') + result = await mockactor.test_data() + self.assertEqual(result, 1) + await mockactor.set_data({'test': 'lol'}) + with self.assertRaises(TypeError): + await mockactor.test_data() + await mockactor.set_data({'test': 1}) + result = await mockactor.test_data() + self.assertEqual(result, 2) + await mockactor.set_data({'test': 2}) + result = await mockactor.test_data() + self.assertEqual(result, 3) + + async def test_state_change_tracker(self): + mockactor = create_mock_actor(MockTestActor, '1') + self.assertEqual(len(mockactor._state_manager._default_state_change_tracker), 0) + await mockactor._on_activate() + self.assertEqual(len(mockactor._state_manager._default_state_change_tracker), 1) + self.assertTrue('state' in mockactor._state_manager._default_state_change_tracker) + self.assertEqual( + mockactor._state_manager._default_state_change_tracker['state'].change_kind, + StateChangeKind.none, + ) + self.assertEqual( + mockactor._state_manager._default_state_change_tracker['state'].value, {'test': 5} + ) + self.assertEqual(mockactor._state_manager._mock_state['state'], {'test': 5}) + await mockactor.remove_data_no_save() + self.assertEqual( + mockactor._state_manager._default_state_change_tracker['state'].change_kind, + StateChangeKind.remove, + ) + self.assertEqual( + mockactor._state_manager._default_state_change_tracker['state'].value, {'test': 5} + ) + self.assertTrue('state' not in mockactor._state_manager._mock_state) + await mockactor.save_state() + self.assertEqual(len(mockactor._state_manager._default_state_change_tracker), 0) + self.assertTrue('state' not in mockactor._state_manager._mock_state) + await mockactor.add_data_no_save({'test': 6}) + self.assertEqual( + mockactor._state_manager._default_state_change_tracker['state'].change_kind, + StateChangeKind.add, + ) + self.assertEqual( + mockactor._state_manager._default_state_change_tracker['state'].value, {'test': 6} + ) + self.assertEqual(mockactor._state_manager._mock_state['state'], {'test': 6}) + await mockactor.save_state() + self.assertEqual( + mockactor._state_manager._default_state_change_tracker['state'].change_kind, + StateChangeKind.none, + ) + self.assertEqual( + mockactor._state_manager._default_state_change_tracker['state'].value, {'test': 6} + ) + self.assertEqual(mockactor._state_manager._mock_state['state'], {'test': 6})