From 92d11b07672b3c71324bb0fb1a92f0e148040404 Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Fri, 1 Nov 2024 22:51:29 +0100 Subject: [PATCH 01/29] 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 00000000..a932d8dd --- /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 00000000..71568f1d --- /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 7132175b..f9c996f0 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 00000000..59538ada --- /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 00000000..66d3c721 --- /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}) From aee0fe0cda55f2f96e6c739ba5041ecd462a65d2 Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Mon, 11 Nov 2024 23:49:57 +0100 Subject: [PATCH 02/29] requested documentation changes Signed-off-by: Lorenzo Curcio --- .../python-sdk-actors/_index.md | 59 --------------- ...{python-mock-actors.md => python-actor.md} | 71 +++++++++++++++++-- 2 files changed, 64 insertions(+), 66 deletions(-) delete mode 100644 daprdocs/content/en/python-sdk-docs/python-sdk-actors/_index.md rename daprdocs/content/en/python-sdk-docs/python-sdk-actors/{python-mock-actors.md => python-actor.md} (70%) diff --git a/daprdocs/content/en/python-sdk-docs/python-sdk-actors/_index.md b/daprdocs/content/en/python-sdk-docs/python-sdk-actors/_index.md deleted file mode 100644 index 565435aa..00000000 --- a/daprdocs/content/en/python-sdk-docs/python-sdk-actors/_index.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -type: docs -title: "Getting started with the Dapr actor Python SDK" -linkTitle: "Actor" -weight: 20000 -description: How to get up and running with the Dapr Python SDK ---- - -The Dapr actor package allows you to interact with Dapr virtual actors from a Python application. - -## Pre-requisites - -- [Dapr CLI]({{< ref install-dapr-cli.md >}}) installed -- Initialized [Dapr environment]({{< ref install-dapr-selfhost.md >}}) -- [Python 3.8+](https://www.python.org/downloads/) installed -- [Dapr Python package]({{< ref "python#installation" >}}) installed - -## Actor interface - -The interface defines the actor contract that is shared between the actor implementation and the clients calling the actor. Because a client may depend on it, it typically makes sense to define it in an assembly that is separate from the actor implementation. - -```python -from dapr.actor import ActorInterface, actormethod - -class DemoActorInterface(ActorInterface): - @actormethod(name="GetMyData") - async def get_my_data(self) -> object: - ... -``` - -## Actor services - -An actor service hosts the virtual actor. It is implemented a class that derives from the base type `Actor` and implements the interfaces defined in the actor interface. - -Actors can be created using one of the Dapr actor extensions: - - [FastAPI actor extension]({{< ref python-fastapi.md >}}) - - [Flask actor extension]({{< ref python-flask.md >}}) - -## Actor client - -An actor client contains the implementation of the actor client which calls the actor methods defined in the actor interface. - -```python -import asyncio - -from dapr.actor import ActorProxy, ActorId -from demo_actor_interface import DemoActorInterface - -async def main(): - # Create proxy client - proxy = ActorProxy.create('DemoActor', ActorId('1'), DemoActorInterface) - - # Call method on client - resp = await proxy.GetMyData() -``` - -## Sample - -Visit [this page](https://github.com/dapr/python-sdk/tree/release-1.0/examples/demo_actor) for a runnable actor sample. \ No newline at end of file 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-actor.md similarity index 70% rename from daprdocs/content/en/python-sdk-docs/python-sdk-actors/python-mock-actors.md rename to daprdocs/content/en/python-sdk-docs/python-sdk-actors/python-actor.md index 59538ada..171a4265 100644 --- 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-actor.md @@ -1,14 +1,71 @@ --- 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 +title: "Getting started with the Dapr actor Python SDK" +linkTitle: "Actor" +weight: 20000 +description: How to get up and running with the Dapr Python SDK --- +The Dapr actor package allows you to interact with Dapr virtual actors from a Python application. + +## Pre-requisites + +- [Dapr CLI]({{< ref install-dapr-cli.md >}}) installed +- Initialized [Dapr environment]({{< ref install-dapr-selfhost.md >}}) +- [Python 3.8+](https://www.python.org/downloads/) installed +- [Dapr Python package]({{< ref "python#installation" >}}) installed + +## Actor interface + +The interface defines the actor contract that is shared between the actor implementation and the clients calling the actor. Because a client may depend on it, it typically makes sense to define it in an assembly that is separate from the actor implementation. + +```python +from dapr.actor import ActorInterface, actormethod + +class DemoActorInterface(ActorInterface): + @actormethod(name="GetMyData") + async def get_my_data(self) -> object: + ... +``` + +## Actor services + +An actor service hosts the virtual actor. It is implemented a class that derives from the base type `Actor` and implements the interfaces defined in the actor interface. + +Actors can be created using one of the Dapr actor extensions: + - [FastAPI actor extension]({{< ref python-fastapi.md >}}) + - [Flask actor extension]({{< ref python-flask.md >}}) + +## Actor client + +An actor client contains the implementation of the actor client which calls the actor methods defined in the actor interface. + +```python +import asyncio + +from dapr.actor import ActorProxy, ActorId +from demo_actor_interface import DemoActorInterface + +async def main(): + # Create proxy client + proxy = ActorProxy.create('DemoActor', ActorId('1'), DemoActorInterface) + + # Call method on client + resp = await proxy.GetMyData() +``` + +## Sample + +Visit [this page](https://github.com/dapr/python-sdk/tree/release-1.0/examples/demo_actor) for a runnable actor sample. + + +## Mock Actor Testing + 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 +### Sample Usage + + ``` from dapr.actor.runtime.mock_actor import create_mock_actor @@ -40,7 +97,7 @@ 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 +### 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.** @@ -57,7 +114,7 @@ The actor _runtime_ctx variable is set to None. Obviously all the normal actor m 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 +### 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). From faadd82b22d8a35240ee1403bd16fd43ca80ac0a Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 12 Nov 2024 00:06:21 +0100 Subject: [PATCH 03/29] forgot to move file back to starting point Signed-off-by: Lorenzo Curcio --- .../en/python-sdk-docs/{python-sdk-actors => }/python-actor.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename daprdocs/content/en/python-sdk-docs/{python-sdk-actors => }/python-actor.md (100%) diff --git a/daprdocs/content/en/python-sdk-docs/python-sdk-actors/python-actor.md b/daprdocs/content/en/python-sdk-docs/python-actor.md similarity index 100% rename from daprdocs/content/en/python-sdk-docs/python-sdk-actors/python-actor.md rename to daprdocs/content/en/python-sdk-docs/python-actor.md From 9d5e62654b6120262f875f7a6deccd578c676bbd Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Mon, 18 Nov 2024 20:48:49 +0100 Subject: [PATCH 04/29] result of ruff format Signed-off-by: Lorenzo Curcio --- tests/actor/test_mock_actor.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/tests/actor/test_mock_actor.py b/tests/actor/test_mock_actor.py index 66d3c721..f0bb39ce 100644 --- a/tests/actor/test_mock_actor.py +++ b/tests/actor/test_mock_actor.py @@ -9,31 +9,40 @@ class MockTestActorInterface(ActorInterface): @actormethod(name='GetData') - async def get_data(self) -> object: ... + async def get_data(self) -> object: + ... @actormethod(name='SetData') - async def set_data(self, data: object) -> None: ... + async def set_data(self, data: object) -> None: + ... @actormethod(name='ClearData') - async def clear_data(self) -> None: ... + async def clear_data(self) -> None: + ... @actormethod(name='TestData') - async def test_data(self) -> int: ... + async def test_data(self) -> int: + ... @actormethod(name='AddDataNoSave') - async def add_data_no_save(self, data: object) -> None: ... + async def add_data_no_save(self, data: object) -> None: + ... @actormethod(name='RemoveDataNoSave') - async def remove_data_no_save(self) -> None: ... + async def remove_data_no_save(self) -> None: + ... @actormethod(name='SaveState') - async def save_state(self) -> None: ... + async def save_state(self) -> None: + ... @actormethod(name='ToggleReminder') - async def toggle_reminder(self, name: str, enabled: bool) -> None: ... + async def toggle_reminder(self, name: str, enabled: bool) -> None: + ... @actormethod(name='ToggleTimer') - async def toggle_timer(self, name: str, enabled: bool) -> None: ... + async def toggle_timer(self, name: str, enabled: bool) -> None: + ... class MockTestActor(Actor, MockTestActorInterface, Remindable): From e17a85ba3b4f480376d30168583f2404b7952dd3 Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 19 Nov 2024 10:14:28 +0100 Subject: [PATCH 05/29] fixed minor formatting issues, fixed type issues Signed-off-by: Lorenzo Curcio --- dapr/actor/runtime/mock_actor.py | 6 ++++-- dapr/actor/runtime/mock_state_manager.py | 6 +++--- dapr/actor/runtime/state_manager.py | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/dapr/actor/runtime/mock_actor.py b/dapr/actor/runtime/mock_actor.py index a932d8dd..6c27c7fb 100644 --- a/dapr/actor/runtime/mock_actor.py +++ b/dapr/actor/runtime/mock_actor.py @@ -11,6 +11,8 @@ limitations under the License. """ +from __future__ import annotations + from datetime import timedelta from typing import Any, Optional, TypeVar @@ -45,7 +47,7 @@ async def set_state(self, data: dict) -> None: def __init__(self, actor_id: str, initstate: Optional[dict]): self.id = ActorId(actor_id) - self._runtime_ctx = None + self._runtime_ctx = None # type: ignore self._state_manager: ActorStateManager = MockStateManager(self, initstate) async def register_timer( @@ -115,7 +117,7 @@ async def unregister_reminder(self, name: str) -> None: def create_mock_actor(cls1: type[T], actor_id: str, initstate: Optional[dict] = None) -> T: - class MockSuperClass(MockActor, cls1): + class MockSuperClass(MockActor, cls1): # type: ignore 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 index 71568f1d..6dc66dbd 100644 --- a/dapr/actor/runtime/mock_state_manager.py +++ b/dapr/actor/runtime/mock_state_manager.py @@ -31,9 +31,9 @@ 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] = {} + self._mock_state: Dict[str, Any] = {} + self._mock_timers: Dict[str, ActorTimerData] = {} + self._mock_reminders: Dict[str, ActorReminderData] = {} if initstate: self._mock_state = initstate diff --git a/dapr/actor/runtime/state_manager.py b/dapr/actor/runtime/state_manager.py index f9c996f0..d55e23bf 100644 --- a/dapr/actor/runtime/state_manager.py +++ b/dapr/actor/runtime/state_manager.py @@ -70,9 +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] + 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): From c23cc33280f85ac942069c2feebf7d4411bd0850 Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 3 Dec 2024 10:30:37 +0100 Subject: [PATCH 06/29] minor test fix Signed-off-by: Lorenzo Curcio --- dapr/actor/runtime/mock_state_manager.py | 2 ++ tests/actor/test_mock_actor.py | 42 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/dapr/actor/runtime/mock_state_manager.py b/dapr/actor/runtime/mock_state_manager.py index 6dc66dbd..34d6e5a5 100644 --- a/dapr/actor/runtime/mock_state_manager.py +++ b/dapr/actor/runtime/mock_state_manager.py @@ -183,8 +183,10 @@ async def add_or_update_state( self._default_state_change_tracker[state_name] = StateMetadata( new_value, StateChangeKind.update ) + self._mock_state[state_name] = new_value return new_value self._default_state_change_tracker[state_name] = StateMetadata(value, StateChangeKind.add) + self._mock_state[state_name] = value return value async def get_state_names(self) -> List[str]: diff --git a/tests/actor/test_mock_actor.py b/tests/actor/test_mock_actor.py index f0bb39ce..81a284c3 100644 --- a/tests/actor/test_mock_actor.py +++ b/tests/actor/test_mock_actor.py @@ -24,6 +24,14 @@ async def clear_data(self) -> None: async def test_data(self) -> int: ... + @actormethod(name='AddState') + async def add_state(self, name: str, data: object) -> None: + ... + + @actormethod(name='UpdateState') + async def update_state(self, name: str, data: object) -> None: + ... + @actormethod(name='AddDataNoSave') async def add_data_no_save(self, data: object) -> None: ... @@ -77,6 +85,15 @@ async def test_data(self) -> int: return 3 return 4 + async def add_state(self, name: str, data: object) -> None: + await self._state_manager.add_state(name, data) + + async def update_state(self, name: str, data: object) -> None: + def double(_: str, x: int) -> int: + return 2 * x + + await self._state_manager.add_or_update_state(name, data, double) + async def add_data_no_save(self, data: object) -> None: await self._state_manager.set_state('state', data) @@ -227,6 +244,31 @@ async def test_test_data(self): result = await mockactor.test_data() self.assertEqual(result, 3) + async def test_add_state(self): + mockactor = create_mock_actor(MockTestActor, '1') + print(mockactor._state_manager._mock_state) + self.assertFalse(mockactor._state_manager._mock_state) + await mockactor.add_state('test', 5) + self.assertTrue('test' in mockactor._state_manager._mock_state) + self.assertEqual(mockactor._state_manager._mock_state['test'], 5) + await mockactor.add_state('test2', 10) + self.assertTrue('test2' in mockactor._state_manager._mock_state) + self.assertEqual(mockactor._state_manager._mock_state['test2'], 10) + self.assertEqual(len(mockactor._state_manager._mock_state), 2) + with self.assertRaises(ValueError): + await mockactor.add_state('test', 10) + + async def test_update_state(self): + mockactor = create_mock_actor(MockTestActor, '1') + self.assertFalse(mockactor._state_manager._mock_state) + await mockactor.update_state('test', 10) + self.assertTrue('test' in mockactor._state_manager._mock_state) + self.assertEqual(mockactor._state_manager._mock_state['test'], 10) + await mockactor.update_state('test', 10) + self.assertTrue('test' in mockactor._state_manager._mock_state) + self.assertEqual(mockactor._state_manager._mock_state['test'], 20) + self.assertEqual(len(mockactor._state_manager._mock_state), 1) + async def test_state_change_tracker(self): mockactor = create_mock_actor(MockTestActor, '1') self.assertEqual(len(mockactor._state_manager._default_state_change_tracker), 0) From e67659d535dc4f7f3ea378163e89480abaae51d1 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Tue, 3 Dec 2024 00:04:01 +0000 Subject: [PATCH 07/29] fixes try_add_state Signed-off-by: Elena Kolevska Signed-off-by: Lorenzo Curcio --- dapr/actor/runtime/mock_state_manager.py | 2 +- dapr/actor/runtime/state_manager.py | 2 +- tests/actor/test_mock_state_manager.py | 99 ++++++++++++++++++++++++ tests/actor/test_state_manager.py | 16 +++- 4 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 tests/actor/test_mock_state_manager.py diff --git a/dapr/actor/runtime/mock_state_manager.py b/dapr/actor/runtime/mock_state_manager.py index 34d6e5a5..ff6ac528 100644 --- a/dapr/actor/runtime/mock_state_manager.py +++ b/dapr/actor/runtime/mock_state_manager.py @@ -51,7 +51,7 @@ async def try_add_state(self, state_name: str, value: T) -> bool: return True return False existed = state_name in self._mock_state - if not existed: + if existed: return False self._default_state_change_tracker[state_name] = StateMetadata(value, StateChangeKind.add) self._mock_state[state_name] = value diff --git a/dapr/actor/runtime/state_manager.py b/dapr/actor/runtime/state_manager.py index d55e23bf..fb1df807 100644 --- a/dapr/actor/runtime/state_manager.py +++ b/dapr/actor/runtime/state_manager.py @@ -90,7 +90,7 @@ async def try_add_state(self, state_name: str, value: T) -> bool: existed = await self._actor.runtime_ctx.state_provider.contains_state( self._type_name, self._actor.id.id, state_name ) - if not existed: + if existed: return False state_change_tracker[state_name] = StateMetadata(value, StateChangeKind.add) diff --git a/tests/actor/test_mock_state_manager.py b/tests/actor/test_mock_state_manager.py new file mode 100644 index 00000000..faf71e2f --- /dev/null +++ b/tests/actor/test_mock_state_manager.py @@ -0,0 +1,99 @@ +import unittest +from dapr.actor.runtime.state_manager import StateChangeKind +from dapr.actor.runtime.mock_state_manager import MockStateManager +from dapr.actor.runtime.mock_actor import MockActor + + +class TestMockStateManager(unittest.IsolatedAsyncioTestCase): + def setUp(self): + """Set up a mock actor and state manager.""" + + class TestActor(MockActor): + pass + + self.mock_actor = TestActor(actor_id='test_actor', initstate=None) + self.state_manager = MockStateManager( + actor=self.mock_actor, initstate={'initial_key': 'initial_value'} + ) + + async def test_add_state(self): + """Test adding a new state.""" + await self.state_manager.add_state('new_key', 'new_value') + state = await self.state_manager.get_state('new_key') + self.assertEqual(state, 'new_value') + + # Ensure it is tracked as an added state + tracker = self.state_manager._default_state_change_tracker + self.assertEqual(tracker['new_key'].change_kind, StateChangeKind.add) + self.assertEqual(tracker['new_key'].value, 'new_value') + + async def test_get_existing_state(self): + """Test retrieving an existing state.""" + state = await self.state_manager.get_state('initial_key') + self.assertEqual(state, 'initial_value') + + async def test_get_nonexistent_state(self): + """Test retrieving a state that does not exist.""" + with self.assertRaises(KeyError): + await self.state_manager.get_state('nonexistent_key') + + async def test_update_state(self): + """Test updating an existing state.""" + await self.state_manager.set_state('initial_key', 'updated_value') + state = await self.state_manager.get_state('initial_key') + self.assertEqual(state, 'updated_value') + + # Ensure it is tracked as an updated state + tracker = self.state_manager._default_state_change_tracker + self.assertEqual(tracker['initial_key'].change_kind, StateChangeKind.update) + self.assertEqual(tracker['initial_key'].value, 'updated_value') + + async def test_remove_state(self): + """Test removing an existing state.""" + await self.state_manager.remove_state('initial_key') + with self.assertRaises(KeyError): + await self.state_manager.get_state('initial_key') + + # Ensure it is tracked as a removed state + tracker = self.state_manager._default_state_change_tracker + self.assertEqual(tracker['initial_key'].change_kind, StateChangeKind.remove) + + async def test_save_state(self): + """Test saving state changes.""" + await self.state_manager.add_state('key1', 'value1') + await self.state_manager.set_state('initial_key', 'value2') + await self.state_manager.remove_state('initial_key') + + await self.state_manager.save_state() + + # After saving, state tracker should be cleared + tracker = self.state_manager._default_state_change_tracker + self.assertEqual(len(tracker), 1) + + # State changes should be reflected in _mock_state + self.assertIn('key1', self.state_manager._mock_state) + self.assertEqual(self.state_manager._mock_state['key1'], 'value1') + self.assertNotIn('initial_key', self.state_manager._mock_state) + + async def test_contains_state(self): + """Test checking if a state exists.""" + self.assertTrue(await self.state_manager.contains_state('initial_key')) + self.assertFalse(await self.state_manager.contains_state('nonexistent_key')) + + async def test_clear_cache(self): + """Test clearing the cache.""" + await self.state_manager.add_state('key1', 'value1') + await self.state_manager.clear_cache() + + # Tracker should be empty + self.assertEqual(len(self.state_manager._default_state_change_tracker), 0) + + async def test_state_ttl(self): + """Test setting state with TTL.""" + await self.state_manager.set_state_ttl('key_with_ttl', 'value', ttl_in_seconds=10) + tracker = self.state_manager._default_state_change_tracker + self.assertEqual(tracker['key_with_ttl'].ttl_in_seconds, 10) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/actor/test_state_manager.py b/tests/actor/test_state_manager.py index 98db0228..c9406dbd 100644 --- a/tests/actor/test_state_manager.py +++ b/tests/actor/test_state_manager.py @@ -46,7 +46,7 @@ def setUp(self): @mock.patch( 'tests.actor.fake_client.FakeDaprActorClient.get_state', - new=_async_mock(return_value=base64.b64encode(b'"value1"')), + new=_async_mock(), ) @mock.patch( 'tests.actor.fake_client.FakeDaprActorClient.save_state_transactionally', new=_async_mock() @@ -67,6 +67,20 @@ def test_add_state(self): added = _run(state_manager.try_add_state('state1', 'value1')) self.assertFalse(added) + @mock.patch( + 'tests.actor.fake_client.FakeDaprActorClient.get_state', + new=_async_mock(return_value=base64.b64encode(b'"value1"')), + ) + @mock.patch( + 'tests.actor.fake_client.FakeDaprActorClient.save_state_transactionally', new=_async_mock() + ) + def test_add_state_with_existing_state(self): + state_manager = ActorStateManager(self._fake_actor) + + # Add first 'state1' + added = _run(state_manager.try_add_state('state1', 'value1')) + self.assertFalse(added) + @mock.patch('tests.actor.fake_client.FakeDaprActorClient.get_state', new=_async_mock()) def test_get_state_for_no_state(self): state_manager = ActorStateManager(self._fake_actor) From cc6ee785b06620f67fb2d8cf48a133583fbc2ddc Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Tue, 3 Dec 2024 00:31:28 +0000 Subject: [PATCH 08/29] Revert "fixes try_add_state" This reverts commit 254ad17bfb184310b2ceae37c1eb82c947466ce6. Signed-off-by: Lorenzo Curcio --- dapr/actor/runtime/mock_state_manager.py | 2 +- dapr/actor/runtime/state_manager.py | 2 +- tests/actor/test_mock_state_manager.py | 99 ------------------------ tests/actor/test_state_manager.py | 16 +--- 4 files changed, 3 insertions(+), 116 deletions(-) delete mode 100644 tests/actor/test_mock_state_manager.py diff --git a/dapr/actor/runtime/mock_state_manager.py b/dapr/actor/runtime/mock_state_manager.py index ff6ac528..34d6e5a5 100644 --- a/dapr/actor/runtime/mock_state_manager.py +++ b/dapr/actor/runtime/mock_state_manager.py @@ -51,7 +51,7 @@ async def try_add_state(self, state_name: str, value: T) -> bool: return True return False existed = state_name in self._mock_state - if existed: + if not existed: return False self._default_state_change_tracker[state_name] = StateMetadata(value, StateChangeKind.add) self._mock_state[state_name] = value diff --git a/dapr/actor/runtime/state_manager.py b/dapr/actor/runtime/state_manager.py index fb1df807..d55e23bf 100644 --- a/dapr/actor/runtime/state_manager.py +++ b/dapr/actor/runtime/state_manager.py @@ -90,7 +90,7 @@ async def try_add_state(self, state_name: str, value: T) -> bool: existed = await self._actor.runtime_ctx.state_provider.contains_state( self._type_name, self._actor.id.id, state_name ) - if existed: + if not existed: return False state_change_tracker[state_name] = StateMetadata(value, StateChangeKind.add) diff --git a/tests/actor/test_mock_state_manager.py b/tests/actor/test_mock_state_manager.py deleted file mode 100644 index faf71e2f..00000000 --- a/tests/actor/test_mock_state_manager.py +++ /dev/null @@ -1,99 +0,0 @@ -import unittest -from dapr.actor.runtime.state_manager import StateChangeKind -from dapr.actor.runtime.mock_state_manager import MockStateManager -from dapr.actor.runtime.mock_actor import MockActor - - -class TestMockStateManager(unittest.IsolatedAsyncioTestCase): - def setUp(self): - """Set up a mock actor and state manager.""" - - class TestActor(MockActor): - pass - - self.mock_actor = TestActor(actor_id='test_actor', initstate=None) - self.state_manager = MockStateManager( - actor=self.mock_actor, initstate={'initial_key': 'initial_value'} - ) - - async def test_add_state(self): - """Test adding a new state.""" - await self.state_manager.add_state('new_key', 'new_value') - state = await self.state_manager.get_state('new_key') - self.assertEqual(state, 'new_value') - - # Ensure it is tracked as an added state - tracker = self.state_manager._default_state_change_tracker - self.assertEqual(tracker['new_key'].change_kind, StateChangeKind.add) - self.assertEqual(tracker['new_key'].value, 'new_value') - - async def test_get_existing_state(self): - """Test retrieving an existing state.""" - state = await self.state_manager.get_state('initial_key') - self.assertEqual(state, 'initial_value') - - async def test_get_nonexistent_state(self): - """Test retrieving a state that does not exist.""" - with self.assertRaises(KeyError): - await self.state_manager.get_state('nonexistent_key') - - async def test_update_state(self): - """Test updating an existing state.""" - await self.state_manager.set_state('initial_key', 'updated_value') - state = await self.state_manager.get_state('initial_key') - self.assertEqual(state, 'updated_value') - - # Ensure it is tracked as an updated state - tracker = self.state_manager._default_state_change_tracker - self.assertEqual(tracker['initial_key'].change_kind, StateChangeKind.update) - self.assertEqual(tracker['initial_key'].value, 'updated_value') - - async def test_remove_state(self): - """Test removing an existing state.""" - await self.state_manager.remove_state('initial_key') - with self.assertRaises(KeyError): - await self.state_manager.get_state('initial_key') - - # Ensure it is tracked as a removed state - tracker = self.state_manager._default_state_change_tracker - self.assertEqual(tracker['initial_key'].change_kind, StateChangeKind.remove) - - async def test_save_state(self): - """Test saving state changes.""" - await self.state_manager.add_state('key1', 'value1') - await self.state_manager.set_state('initial_key', 'value2') - await self.state_manager.remove_state('initial_key') - - await self.state_manager.save_state() - - # After saving, state tracker should be cleared - tracker = self.state_manager._default_state_change_tracker - self.assertEqual(len(tracker), 1) - - # State changes should be reflected in _mock_state - self.assertIn('key1', self.state_manager._mock_state) - self.assertEqual(self.state_manager._mock_state['key1'], 'value1') - self.assertNotIn('initial_key', self.state_manager._mock_state) - - async def test_contains_state(self): - """Test checking if a state exists.""" - self.assertTrue(await self.state_manager.contains_state('initial_key')) - self.assertFalse(await self.state_manager.contains_state('nonexistent_key')) - - async def test_clear_cache(self): - """Test clearing the cache.""" - await self.state_manager.add_state('key1', 'value1') - await self.state_manager.clear_cache() - - # Tracker should be empty - self.assertEqual(len(self.state_manager._default_state_change_tracker), 0) - - async def test_state_ttl(self): - """Test setting state with TTL.""" - await self.state_manager.set_state_ttl('key_with_ttl', 'value', ttl_in_seconds=10) - tracker = self.state_manager._default_state_change_tracker - self.assertEqual(tracker['key_with_ttl'].ttl_in_seconds, 10) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/actor/test_state_manager.py b/tests/actor/test_state_manager.py index c9406dbd..98db0228 100644 --- a/tests/actor/test_state_manager.py +++ b/tests/actor/test_state_manager.py @@ -46,7 +46,7 @@ def setUp(self): @mock.patch( 'tests.actor.fake_client.FakeDaprActorClient.get_state', - new=_async_mock(), + new=_async_mock(return_value=base64.b64encode(b'"value1"')), ) @mock.patch( 'tests.actor.fake_client.FakeDaprActorClient.save_state_transactionally', new=_async_mock() @@ -67,20 +67,6 @@ def test_add_state(self): added = _run(state_manager.try_add_state('state1', 'value1')) self.assertFalse(added) - @mock.patch( - 'tests.actor.fake_client.FakeDaprActorClient.get_state', - new=_async_mock(return_value=base64.b64encode(b'"value1"')), - ) - @mock.patch( - 'tests.actor.fake_client.FakeDaprActorClient.save_state_transactionally', new=_async_mock() - ) - def test_add_state_with_existing_state(self): - state_manager = ActorStateManager(self._fake_actor) - - # Add first 'state1' - added = _run(state_manager.try_add_state('state1', 'value1')) - self.assertFalse(added) - @mock.patch('tests.actor.fake_client.FakeDaprActorClient.get_state', new=_async_mock()) def test_get_state_for_no_state(self): state_manager = ActorStateManager(self._fake_actor) From 6cebadd842d37a110bc5a43a87fba925498e2307 Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 3 Dec 2024 10:27:51 +0100 Subject: [PATCH 09/29] Update dapr/actor/runtime/mock_state_manager.py Fixing bug in try_add_state as mentioned in PR #756 Co-authored-by: Elena Kolevska Signed-off-by: Lorenzo Curcio Signed-off-by: Lorenzo Curcio --- dapr/actor/runtime/mock_state_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dapr/actor/runtime/mock_state_manager.py b/dapr/actor/runtime/mock_state_manager.py index 34d6e5a5..ff6ac528 100644 --- a/dapr/actor/runtime/mock_state_manager.py +++ b/dapr/actor/runtime/mock_state_manager.py @@ -51,7 +51,7 @@ async def try_add_state(self, state_name: str, value: T) -> bool: return True return False existed = state_name in self._mock_state - if not existed: + if existed: return False self._default_state_change_tracker[state_name] = StateMetadata(value, StateChangeKind.add) self._mock_state[state_name] = value From bebdcb1a6adb19f1b02174f3e4535286746738e9 Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 3 Dec 2024 10:37:01 +0100 Subject: [PATCH 10/29] Update dapr/actor/runtime/mock_actor.py Whoops missed this Co-authored-by: Elena Kolevska Signed-off-by: Lorenzo Curcio --- dapr/actor/runtime/mock_actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dapr/actor/runtime/mock_actor.py b/dapr/actor/runtime/mock_actor.py index 6c27c7fb..c0635578 100644 --- a/dapr/actor/runtime/mock_actor.py +++ b/dapr/actor/runtime/mock_actor.py @@ -39,7 +39,7 @@ 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) + mock_actor = create_mock_actor(SomeActor, "actor_1") assert mock_actor._state_manager._mock_state == {} await mock_actor.set_state({"test":10}) assert mock_actor._state_manager._mock_state == {"test":10} From 1f1569f1abe1e9cabb18f18761de12621f2a1720 Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 3 Dec 2024 10:37:09 +0100 Subject: [PATCH 11/29] Update daprdocs/content/en/python-sdk-docs/python-actor.md Co-authored-by: Elena Kolevska Signed-off-by: Lorenzo Curcio --- daprdocs/content/en/python-sdk-docs/python-actor.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daprdocs/content/en/python-sdk-docs/python-actor.md b/daprdocs/content/en/python-sdk-docs/python-actor.md index 171a4265..056baa22 100644 --- a/daprdocs/content/en/python-sdk-docs/python-actor.md +++ b/daprdocs/content/en/python-sdk-docs/python-actor.md @@ -56,7 +56,7 @@ async def main(): ## Sample -Visit [this page](https://github.com/dapr/python-sdk/tree/release-1.0/examples/demo_actor) for a runnable actor sample. +Visit [this page](https://github.com/dapr/python-sdk/tree/v1.14.0/examples/demo_actor) for a runnable actor sample. ## Mock Actor Testing From c3e6aca8772aa7e9b8c351d9730e8cf196952174 Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 3 Dec 2024 10:37:19 +0100 Subject: [PATCH 12/29] Update daprdocs/content/en/python-sdk-docs/python-actor.md Co-authored-by: Elena Kolevska Signed-off-by: Lorenzo Curcio --- daprdocs/content/en/python-sdk-docs/python-actor.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daprdocs/content/en/python-sdk-docs/python-actor.md b/daprdocs/content/en/python-sdk-docs/python-actor.md index 056baa22..62c5a905 100644 --- a/daprdocs/content/en/python-sdk-docs/python-actor.md +++ b/daprdocs/content/en/python-sdk-docs/python-actor.md @@ -61,7 +61,7 @@ Visit [this page](https://github.com/dapr/python-sdk/tree/v1.14.0/examples/demo_ ## Mock Actor Testing -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. +The Dapr Python SDK provides the ability to create mock actors to unit test your actor methods and see how they interact with the actor state. ### Sample Usage From ef74591a85c57216ab7e3a8878c3628bb1f89f38 Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 3 Dec 2024 10:37:44 +0100 Subject: [PATCH 13/29] Update daprdocs/content/en/python-sdk-docs/python-actor.md Co-authored-by: Elena Kolevska Signed-off-by: Lorenzo Curcio --- daprdocs/content/en/python-sdk-docs/python-actor.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daprdocs/content/en/python-sdk-docs/python-actor.md b/daprdocs/content/en/python-sdk-docs/python-actor.md index 62c5a905..518ee8a6 100644 --- a/daprdocs/content/en/python-sdk-docs/python-actor.md +++ b/daprdocs/content/en/python-sdk-docs/python-actor.md @@ -79,7 +79,7 @@ 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. +Mock actors are created by passing your actor class and an actor ID (a string) to the create_mock_actor function. This function returns an instance of the actor with many internal methods overridden. Instead of interacting with Dapr for tasks like saving state or managing timers, the mock actor uses in-memory state to simulate these behaviors. Those variables are: * **_state_manager._mock_state()** From b0650a6be54563dbafb2e934ab019b9f1f1352ce Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 3 Dec 2024 10:38:13 +0100 Subject: [PATCH 14/29] Update daprdocs/content/en/python-sdk-docs/python-actor.md Co-authored-by: Elena Kolevska Signed-off-by: Lorenzo Curcio --- daprdocs/content/en/python-sdk-docs/python-actor.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/daprdocs/content/en/python-sdk-docs/python-actor.md b/daprdocs/content/en/python-sdk-docs/python-actor.md index 518ee8a6..d75c411e 100644 --- a/daprdocs/content/en/python-sdk-docs/python-actor.md +++ b/daprdocs/content/en/python-sdk-docs/python-actor.md @@ -81,14 +81,14 @@ assert mockactor._state_manager._mock_state == 5 #True ``` Mock actors are created by passing your actor class and an actor ID (a string) to the create_mock_actor function. This function returns an instance of the actor with many internal methods overridden. Instead of interacting with Dapr for tasks like saving state or managing timers, the mock actor uses in-memory state to simulate these behaviors. -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. +This state can be accessed through the following variables: +- **_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 pair. 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_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()** +- **_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:** From 1733b42276c1824eca8a4766ea885794f0734de5 Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 3 Dec 2024 10:38:21 +0100 Subject: [PATCH 15/29] Update daprdocs/content/en/python-sdk-docs/python-actor.md Co-authored-by: Elena Kolevska Signed-off-by: Lorenzo Curcio --- daprdocs/content/en/python-sdk-docs/python-actor.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/daprdocs/content/en/python-sdk-docs/python-actor.md b/daprdocs/content/en/python-sdk-docs/python-actor.md index d75c411e..5f01beaa 100644 --- a/daprdocs/content/en/python-sdk-docs/python-actor.md +++ b/daprdocs/content/en/python-sdk-docs/python-actor.md @@ -99,11 +99,11 @@ result = await mock_actor.recieve_reminder(name, state, due_time, period, _ttl) ### 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 `_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. +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* +*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) From df85211436c098c38935191097b2a845ad7cd83e Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 3 Dec 2024 10:38:29 +0100 Subject: [PATCH 16/29] Update daprdocs/content/en/python-sdk-docs/python-actor.md Co-authored-by: Elena Kolevska Signed-off-by: Lorenzo Curcio --- daprdocs/content/en/python-sdk-docs/python-actor.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daprdocs/content/en/python-sdk-docs/python-actor.md b/daprdocs/content/en/python-sdk-docs/python-actor.md index 5f01beaa..2087aa4b 100644 --- a/daprdocs/content/en/python-sdk-docs/python-actor.md +++ b/daprdocs/content/en/python-sdk-docs/python-actor.md @@ -116,8 +116,8 @@ The actor _state_manager is overwritten with an instance of MockStateManager. Th ### 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). +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 unfortunately doesn't work 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. +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 From a7b86c7cab71fe6186d0dc93856d4abff073fc7e Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 3 Dec 2024 10:39:02 +0100 Subject: [PATCH 17/29] Update daprdocs/content/en/python-sdk-docs/python-actor.md Co-authored-by: Elena Kolevska Signed-off-by: Lorenzo Curcio --- daprdocs/content/en/python-sdk-docs/python-actor.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/daprdocs/content/en/python-sdk-docs/python-actor.md b/daprdocs/content/en/python-sdk-docs/python-actor.md index 2087aa4b..7c87948f 100644 --- a/daprdocs/content/en/python-sdk-docs/python-actor.md +++ b/daprdocs/content/en/python-sdk-docs/python-actor.md @@ -108,11 +108,11 @@ The `__init__`, `register_timer`, `unregister_timer`, `register_reminder`, `unre 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\_\_.* +*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 `_runtime_ctx` variable is set to None. 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. +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 From 1a8275428af6602f24a7d3c412dc337d8f40b40b Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 3 Dec 2024 10:41:23 +0100 Subject: [PATCH 18/29] minor error in docs Signed-off-by: Lorenzo Curcio --- daprdocs/content/en/python-sdk-docs/python-actor.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daprdocs/content/en/python-sdk-docs/python-actor.md b/daprdocs/content/en/python-sdk-docs/python-actor.md index 7c87948f..5a5ae0fc 100644 --- a/daprdocs/content/en/python-sdk-docs/python-actor.md +++ b/daprdocs/content/en/python-sdk-docs/python-actor.md @@ -71,13 +71,13 @@ 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.set_state('mystate', 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 +assert mockactor._state_manager._mock_state['mystate'] == 5 #True ``` Mock actors are created by passing your actor class and an actor ID (a string) to the create_mock_actor function. This function returns an instance of the actor with many internal methods overridden. Instead of interacting with Dapr for tasks like saving state or managing timers, the mock actor uses in-memory state to simulate these behaviors. From 2480d7e9e5cbe8dc5b3eb433b863290bb66f9f79 Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 3 Dec 2024 17:10:52 +0100 Subject: [PATCH 19/29] fixed and added more unit tests. Added example Signed-off-by: Lorenzo Curcio --- dapr/actor/runtime/mock_state_manager.py | 3 +- examples/demo_actor/README.md | 45 +++++++ .../demo_actor/demo_actor/test_demo_actor.py | 46 +++++++ tests/actor/test_mock_actor.py | 1 - tests/actor/test_mock_state_manager.py | 116 ++++++++++++++++++ 5 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 examples/demo_actor/demo_actor/test_demo_actor.py create mode 100644 tests/actor/test_mock_state_manager.py diff --git a/dapr/actor/runtime/mock_state_manager.py b/dapr/actor/runtime/mock_state_manager.py index ff6ac528..bcac6d95 100644 --- a/dapr/actor/runtime/mock_state_manager.py +++ b/dapr/actor/runtime/mock_state_manager.py @@ -151,6 +151,7 @@ async def get_or_add_state(self, state_name: str, value: T) -> Optional[T]: if self.is_state_marked_for_remove(state_name) else StateChangeKind.add ) + self._mock_state[state_name] = value self._default_state_change_tracker[state_name] = StateMetadata(value, change_kind) return value @@ -173,7 +174,7 @@ async def add_or_update_state( 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 + self._mock_state[state_name] = new_value return new_value has_value = state_name in self._mock_state diff --git a/examples/demo_actor/README.md b/examples/demo_actor/README.md index c5ece71e..db63e1e4 100644 --- a/examples/demo_actor/README.md +++ b/examples/demo_actor/README.md @@ -6,6 +6,7 @@ This document describes how to create an Actor(DemoActor) and invoke its methods - **The actor service(demo_actor_service.py).** This implements FastAPI service that is going to host the actor. It contains the implementation of the actor, `demo_actor.py`. An actor implementation is a class that derives from the base type `Actor` and implements the interfaces defined in `demo_actor_interface.py`. - **The actor service for flask(demo_actor_flask.py).** This implements Flask web service that is going to host the actor. - **The actor client(demo_actor_client.py)** This contains the implementation of the actor client which calls DemoActor's method defined in Actor Interfaces. +- **Actor tests(test_demo_actor.py)** This contains actor unit tests using mock actor testing functionality. ## Pre-requisites @@ -183,3 +184,47 @@ expected_stdout_lines: kubectl logs -l app="demoactor-client" -c demoactor-client ``` +## Run DemoActor mock actor tests + + + +1. Run Tests + + ```bash + cd demo_actor + unittest test_demo_actor.py + ``` + + Expected output (note that the unit test print outputs might not necessarily be in this order - what really matters is that all tests pass anyway): + + ``` + has_value: False + set_my_data: {'state': 5} + has_value: True + clear_my_data + has_value: False + ..has_value: False + .set reminder to True + set reminder is done + set reminder to False + set reminder is done + .has_value: False + set_my_data: {'state': 5} + has_value: True + . + ---------------------------------------------------------------------- + Ran 5 tests in 0.052s + + OK + ``` + + + diff --git a/examples/demo_actor/demo_actor/test_demo_actor.py b/examples/demo_actor/demo_actor/test_demo_actor.py new file mode 100644 index 00000000..63007a7f --- /dev/null +++ b/examples/demo_actor/demo_actor/test_demo_actor.py @@ -0,0 +1,46 @@ +import unittest + +from demo_actor import DemoActor + +from dapr.actor.runtime.mock_actor import create_mock_actor + + +class DemoActorTests(unittest.IsolatedAsyncioTestCase): + def test_create_actor(self): + mockactor = create_mock_actor(DemoActor, '1') + self.assertEqual(mockactor.id.id, '1') + + async def test_get_data(self): + mockactor = create_mock_actor(DemoActor, '1') + self.assertFalse(mockactor._state_manager._mock_state) + val = await mockactor.get_my_data() + self.assertIsNone(val) + + async def test_set_data(self): + mockactor = create_mock_actor(DemoActor, '1') + self.assertFalse(mockactor._state_manager._mock_state) + val = await mockactor.get_my_data() + self.assertIsNone(val) + await mockactor.set_my_data({'state': 5}) + val = await mockactor.get_my_data() + self.assertIs(val['state'], 5) # type: ignore + + async def test_clear_data(self): + mockactor = create_mock_actor(DemoActor, '1') + self.assertFalse(mockactor._state_manager._mock_state) + val = await mockactor.get_my_data() + self.assertIsNone(val) + await mockactor.set_my_data({'state': 5}) + val = await mockactor.get_my_data() + self.assertIs(val['state'], 5) # type: ignore + await mockactor.clear_my_data() + val = await mockactor.get_my_data() + self.assertIsNone(val) + + async def test_reminder(self): + mockactor = create_mock_actor(DemoActor, '1') + self.assertFalse(mockactor._state_manager._mock_reminders) + await mockactor.set_reminder(True) + self.assertTrue('demo_reminder' in mockactor._state_manager._mock_reminders) + await mockactor.set_reminder(False) + self.assertFalse(mockactor._state_manager._mock_reminders) diff --git a/tests/actor/test_mock_actor.py b/tests/actor/test_mock_actor.py index 81a284c3..abed6448 100644 --- a/tests/actor/test_mock_actor.py +++ b/tests/actor/test_mock_actor.py @@ -246,7 +246,6 @@ async def test_test_data(self): async def test_add_state(self): mockactor = create_mock_actor(MockTestActor, '1') - print(mockactor._state_manager._mock_state) self.assertFalse(mockactor._state_manager._mock_state) await mockactor.add_state('test', 5) self.assertTrue('test' in mockactor._state_manager._mock_state) diff --git a/tests/actor/test_mock_state_manager.py b/tests/actor/test_mock_state_manager.py new file mode 100644 index 00000000..89cc51a8 --- /dev/null +++ b/tests/actor/test_mock_state_manager.py @@ -0,0 +1,116 @@ +import unittest + +from dapr.actor import Actor, ActorInterface +from dapr.actor.runtime.mock_actor import create_mock_actor +from dapr.actor.runtime.mock_state_manager import MockStateManager + + +def double(_: str, x: int) -> int: + return 2 * x + + +class MockTestActorInterface(ActorInterface): + pass + + +class MockTestActor(Actor, MockTestActorInterface): + def __init__(self, ctx, actor_id): + super().__init__(ctx, actor_id) + + +class ActorMockActorTests(unittest.IsolatedAsyncioTestCase): + def test_init_state(self): + mock_actor = create_mock_actor(MockTestActor, 'test') + state_manager = mock_actor._state_manager + self.assertIsInstance(state_manager, MockStateManager) + self.assertFalse(state_manager._mock_state) + self.assertFalse(state_manager._mock_reminders) + self.assertFalse(state_manager._mock_timers) + + async def test_add_state(self): + mock_actor = create_mock_actor(MockTestActor, 'test') + state_manager = mock_actor._state_manager + await state_manager.add_state('state', 5) + self.assertIs(state_manager._mock_state['state'], 5) + await state_manager.add_state('state2', 5) + self.assertIs(state_manager._mock_state['state2'], 5) + with self.assertRaises(ValueError): + await state_manager.add_state('state', 5) + + async def test_get_state(self): + mock_actor = create_mock_actor(MockTestActor, 'test') + state_manager = mock_actor._state_manager + with self.assertRaises(KeyError): + await state_manager.get_state('state') + await state_manager.add_state('state', 5) + value = await state_manager.get_state('state') + self.assertIs(value, 5) + + async def test_set_state(self): + mock_actor = create_mock_actor(MockTestActor, 'test') + state_manager = mock_actor._state_manager + await state_manager.set_state('state', 5) + self.assertIs(state_manager._mock_state['state'], 5) + await state_manager.set_state('state', 10) + self.assertIs(state_manager._mock_state['state'], 10) + + async def test_remove_state(self): + mock_actor = create_mock_actor(MockTestActor, 'test') + state_manager = mock_actor._state_manager + await state_manager.set_state('state', 5) + self.assertIs(state_manager._mock_state['state'], 5) + await state_manager.remove_state('state') + self.assertFalse(state_manager._mock_state) + with self.assertRaises(KeyError): + await state_manager.remove_state('state') + + async def test_contains_state(self): + mock_actor = create_mock_actor(MockTestActor, 'test') + state_manager = mock_actor._state_manager + self.assertFalse(await state_manager.contains_state('state')) + await state_manager.set_state('state', 5) + self.assertTrue(await state_manager.contains_state('state')) + await state_manager.remove_state('state') + self.assertFalse(await state_manager.contains_state('state')) + + async def test_get_or_add_state(self): + mock_actor = create_mock_actor(MockTestActor, 'test') + state_manager = mock_actor._state_manager + out = await state_manager.get_or_add_state('state', 5) + self.assertIs(out, 5) + self.assertIs(state_manager._mock_state['state'], 5) + out = await state_manager.get_or_add_state('state', 10) + self.assertIs(out, 5) + self.assertIs(state_manager._mock_state['state'], 5) + + async def test_add_or_update_state(self): + mock_actor = create_mock_actor(MockTestActor, 'test') + state_manager = mock_actor._state_manager + await state_manager.add_or_update_state('state', 5, double) + self.assertIs(state_manager._mock_state['state'], 5) + await state_manager.add_or_update_state('state', 1000, double) + self.assertIs(state_manager._mock_state['state'], 10) + + async def test_get_state_names(self): + mock_actor = create_mock_actor(MockTestActor, 'test') + state_manager = mock_actor._state_manager + names = await state_manager.get_state_names() + self.assertFalse(names) + await state_manager.set_state('state1', 5) + names = await state_manager.get_state_names() + self.assertCountEqual(names, ['state1']) + await state_manager.set_state('state2', 5) + names = await state_manager.get_state_names() + self.assertCountEqual(names, ['state1', 'state2']) + await state_manager.save_state() + names = await state_manager.get_state_names() + self.assertFalse(names) + + async def test_clear_cache(self): + mock_actor = create_mock_actor(MockTestActor, 'test') + state_manager = mock_actor._state_manager + self.assertFalse(state_manager._default_state_change_tracker) + await state_manager.set_state('state1', 5) + self.assertTrue('state1', state_manager._default_state_change_tracker) + await state_manager.clear_cache() + self.assertFalse(state_manager._default_state_change_tracker) From 33a5d1d94fc0ee20c75e8ae34ffe3cd669224c29 Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 3 Dec 2024 19:03:42 +0100 Subject: [PATCH 20/29] unittest fix Signed-off-by: Lorenzo Curcio --- examples/demo_actor/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/demo_actor/README.md b/examples/demo_actor/README.md index db63e1e4..249c5e87 100644 --- a/examples/demo_actor/README.md +++ b/examples/demo_actor/README.md @@ -200,7 +200,7 @@ timeout_seconds: 60 ```bash cd demo_actor - unittest test_demo_actor.py + python -m unittest test_demo_actor.py ``` Expected output (note that the unit test print outputs might not necessarily be in this order - what really matters is that all tests pass anyway): From 4fa7fb8e3c92c4a4a1d0c70d1cbb76bdf19af406 Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 3 Dec 2024 19:47:24 +0100 Subject: [PATCH 21/29] Update examples/demo_actor/README.md Co-authored-by: Elena Kolevska Signed-off-by: Lorenzo Curcio --- examples/demo_actor/README.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/examples/demo_actor/README.md b/examples/demo_actor/README.md index 249c5e87..baa1602c 100644 --- a/examples/demo_actor/README.md +++ b/examples/demo_actor/README.md @@ -189,10 +189,25 @@ expected_stdout_lines: From 85b8d6c60a84b87dd72e6645012195418a0584b6 Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 3 Dec 2024 20:05:38 +0100 Subject: [PATCH 22/29] concentrated some tests Signed-off-by: Lorenzo Curcio --- .../demo_actor/demo_actor/test_demo_actor.py | 6 - tests/actor/test_mock_state_manager.py | 103 +++++------------- 2 files changed, 30 insertions(+), 79 deletions(-) diff --git a/examples/demo_actor/demo_actor/test_demo_actor.py b/examples/demo_actor/demo_actor/test_demo_actor.py index 63007a7f..d25185b0 100644 --- a/examples/demo_actor/demo_actor/test_demo_actor.py +++ b/examples/demo_actor/demo_actor/test_demo_actor.py @@ -18,18 +18,12 @@ async def test_get_data(self): async def test_set_data(self): mockactor = create_mock_actor(DemoActor, '1') - self.assertFalse(mockactor._state_manager._mock_state) - val = await mockactor.get_my_data() - self.assertIsNone(val) await mockactor.set_my_data({'state': 5}) val = await mockactor.get_my_data() self.assertIs(val['state'], 5) # type: ignore async def test_clear_data(self): mockactor = create_mock_actor(DemoActor, '1') - self.assertFalse(mockactor._state_manager._mock_state) - val = await mockactor.get_my_data() - self.assertIsNone(val) await mockactor.set_my_data({'state': 5}) val = await mockactor.get_my_data() self.assertIs(val['state'], 5) # type: ignore diff --git a/tests/actor/test_mock_state_manager.py b/tests/actor/test_mock_state_manager.py index 89cc51a8..ddd1ee04 100644 --- a/tests/actor/test_mock_state_manager.py +++ b/tests/actor/test_mock_state_manager.py @@ -27,90 +27,47 @@ def test_init_state(self): self.assertFalse(state_manager._mock_reminders) self.assertFalse(state_manager._mock_timers) - async def test_add_state(self): + async def test_state_methods(self): mock_actor = create_mock_actor(MockTestActor, 'test') state_manager = mock_actor._state_manager + self.assertFalse(await state_manager.contains_state('state')) + self.assertFalse(state_manager._default_state_change_tracker) + names = await state_manager.get_state_names() + self.assertFalse(names) + with self.assertRaises(KeyError): + await state_manager.get_state('state') await state_manager.add_state('state', 5) + names = await state_manager.get_state_names() + self.assertCountEqual(names, ['state']) self.assertIs(state_manager._mock_state['state'], 5) + value = await state_manager.get_state('state') + self.assertIs(value, 5) await state_manager.add_state('state2', 5) self.assertIs(state_manager._mock_state['state2'], 5) with self.assertRaises(ValueError): await state_manager.add_state('state', 5) - - async def test_get_state(self): - mock_actor = create_mock_actor(MockTestActor, 'test') - state_manager = mock_actor._state_manager - with self.assertRaises(KeyError): - await state_manager.get_state('state') - await state_manager.add_state('state', 5) - value = await state_manager.get_state('state') - self.assertIs(value, 5) - - async def test_set_state(self): - mock_actor = create_mock_actor(MockTestActor, 'test') - state_manager = mock_actor._state_manager - await state_manager.set_state('state', 5) - self.assertIs(state_manager._mock_state['state'], 5) - await state_manager.set_state('state', 10) - self.assertIs(state_manager._mock_state['state'], 10) - - async def test_remove_state(self): - mock_actor = create_mock_actor(MockTestActor, 'test') - state_manager = mock_actor._state_manager - await state_manager.set_state('state', 5) - self.assertIs(state_manager._mock_state['state'], 5) - await state_manager.remove_state('state') - self.assertFalse(state_manager._mock_state) + await state_manager.set_state('state3', 5) + self.assertIs(state_manager._mock_state['state3'], 5) + await state_manager.set_state('state3', 10) + self.assertIs(state_manager._mock_state['state3'], 10) + self.assertTrue(await state_manager.contains_state('state3')) + await state_manager.remove_state('state3') + self.assertFalse('state3' in state_manager._mock_state) with self.assertRaises(KeyError): - await state_manager.remove_state('state') - - async def test_contains_state(self): - mock_actor = create_mock_actor(MockTestActor, 'test') - state_manager = mock_actor._state_manager - self.assertFalse(await state_manager.contains_state('state')) - await state_manager.set_state('state', 5) - self.assertTrue(await state_manager.contains_state('state')) - await state_manager.remove_state('state') - self.assertFalse(await state_manager.contains_state('state')) - - async def test_get_or_add_state(self): - mock_actor = create_mock_actor(MockTestActor, 'test') - state_manager = mock_actor._state_manager - out = await state_manager.get_or_add_state('state', 5) + await state_manager.remove_state('state3') + self.assertFalse(await state_manager.contains_state('state3')) + await state_manager.add_or_update_state('state3', 5, double) + self.assertIs(state_manager._mock_state['state3'], 5) + await state_manager.add_or_update_state('state3', 1000, double) + self.assertIs(state_manager._mock_state['state3'], 10) + out = await state_manager.get_or_add_state('state4', 5) self.assertIs(out, 5) - self.assertIs(state_manager._mock_state['state'], 5) - out = await state_manager.get_or_add_state('state', 10) + self.assertIs(state_manager._mock_state['state4'], 5) + out = await state_manager.get_or_add_state('state4', 10) self.assertIs(out, 5) - self.assertIs(state_manager._mock_state['state'], 5) - - async def test_add_or_update_state(self): - mock_actor = create_mock_actor(MockTestActor, 'test') - state_manager = mock_actor._state_manager - await state_manager.add_or_update_state('state', 5, double) - self.assertIs(state_manager._mock_state['state'], 5) - await state_manager.add_or_update_state('state', 1000, double) - self.assertIs(state_manager._mock_state['state'], 10) - - async def test_get_state_names(self): - mock_actor = create_mock_actor(MockTestActor, 'test') - state_manager = mock_actor._state_manager - names = await state_manager.get_state_names() - self.assertFalse(names) - await state_manager.set_state('state1', 5) - names = await state_manager.get_state_names() - self.assertCountEqual(names, ['state1']) - await state_manager.set_state('state2', 5) + self.assertIs(state_manager._mock_state['state4'], 5) names = await state_manager.get_state_names() - self.assertCountEqual(names, ['state1', 'state2']) - await state_manager.save_state() - names = await state_manager.get_state_names() - self.assertFalse(names) - - async def test_clear_cache(self): - mock_actor = create_mock_actor(MockTestActor, 'test') - state_manager = mock_actor._state_manager - self.assertFalse(state_manager._default_state_change_tracker) - await state_manager.set_state('state1', 5) - self.assertTrue('state1', state_manager._default_state_change_tracker) + self.assertCountEqual(names, ['state', 'state2', 'state3', 'state4']) + self.assertTrue('state', state_manager._default_state_change_tracker) await state_manager.clear_cache() self.assertFalse(state_manager._default_state_change_tracker) From e78817495240d8e20b5a96ab5a4bb36e6a1c7d19 Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 3 Dec 2024 21:02:21 +0100 Subject: [PATCH 23/29] removed unnecessary type hint Signed-off-by: Lorenzo Curcio --- dapr/actor/runtime/mock_actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dapr/actor/runtime/mock_actor.py b/dapr/actor/runtime/mock_actor.py index c0635578..0da0aa8d 100644 --- a/dapr/actor/runtime/mock_actor.py +++ b/dapr/actor/runtime/mock_actor.py @@ -48,7 +48,7 @@ async def set_state(self, data: dict) -> None: def __init__(self, actor_id: str, initstate: Optional[dict]): self.id = ActorId(actor_id) self._runtime_ctx = None # type: ignore - self._state_manager: ActorStateManager = MockStateManager(self, initstate) + self._state_manager = MockStateManager(self, initstate) async def register_timer( self, From e391eda7db48fd75179783623388ca28d96ca94a Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Wed, 4 Dec 2024 10:06:42 +0100 Subject: [PATCH 24/29] Update daprdocs/content/en/python-sdk-docs/python-actor.md didnt see this earlier whoops Co-authored-by: Elena Kolevska Signed-off-by: Lorenzo Curcio --- daprdocs/content/en/python-sdk-docs/python-actor.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daprdocs/content/en/python-sdk-docs/python-actor.md b/daprdocs/content/en/python-sdk-docs/python-actor.md index 5a5ae0fc..1ea18898 100644 --- a/daprdocs/content/en/python-sdk-docs/python-actor.md +++ b/daprdocs/content/en/python-sdk-docs/python-actor.md @@ -94,7 +94,7 @@ A [str, ActorReminderData] dict which holds the active actor reminders. Any acto **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 +# Test the result directly or test for side effects (like changing state) by querying `_state_manager._mock_state` ``` ### Usage and Limitations From 97bc3006f40d37b5055571b1aed2de619bb7db6f Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 10 Dec 2024 20:50:05 +0100 Subject: [PATCH 25/29] Update examples/demo_actor/README.md Co-authored-by: Elena Kolevska Signed-off-by: Lorenzo Curcio --- examples/demo_actor/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/demo_actor/README.md b/examples/demo_actor/README.md index baa1602c..d45c15b9 100644 --- a/examples/demo_actor/README.md +++ b/examples/demo_actor/README.md @@ -190,7 +190,6 @@ expected_stdout_lines: name: Actor Tests background: true expected_stdout_lines: - - "has_value: False" - "set_my_data: {'state': 5}" - "has_value: True" - "clear_my_data" @@ -200,7 +199,6 @@ expected_stdout_lines: - "set reminder is done" - "set reminder to False" - "set reminder is done" - - "has_value: False" - "set_my_data: {'state': 5}" - "has_value: True" expected_stderr_lines: From 9314788bb160eb1264f0cc569501d40d97d4be45 Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Tue, 10 Dec 2024 21:03:37 +0100 Subject: [PATCH 26/29] documentation changes Signed-off-by: Lorenzo Curcio --- daprdocs/content/en/python-sdk-docs/python-actor.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/daprdocs/content/en/python-sdk-docs/python-actor.md b/daprdocs/content/en/python-sdk-docs/python-actor.md index 1ea18898..260725f2 100644 --- a/daprdocs/content/en/python-sdk-docs/python-actor.md +++ b/daprdocs/content/en/python-sdk-docs/python-actor.md @@ -99,7 +99,7 @@ result = await mock_actor.recieve_reminder(name, state, due_time, period, _ttl) ### 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.** +**To allow for more fine-grained control, 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. @@ -110,9 +110,11 @@ The `__init__`, `register_timer`, `unregister_timer`, `register_reminder`, `unre ``` *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. 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. +*If you have an actor which does override default Dapr actor methods, your best bet is likely to create a custom subclass of the MockActor class (from MockActor.py) which implements whatever custom logic you have along with interacting with `_mock_state`, `_mock_timers`, and `_mock_reminders` as normal, and then appyling that custom class as a mixin via a `create_mock_actor` function you define yourself.* -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. +The actor `_runtime_ctx` variable is set to None. All the normal actor methods have been overwritten such as to not call it, but if your code itself interacts directly with `_runtime_ctx`, tests may fail. + +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 tests will likely fail. ### Type Hinting From 55d91060a7ee6941d6900ffd246ab151fc6c9684 Mon Sep 17 00:00:00 2001 From: Lorenzo Curcio Date: Thu, 2 Jan 2025 11:17:37 +0100 Subject: [PATCH 27/29] now requires #type: ignore Signed-off-by: Lorenzo Curcio --- dapr/actor/runtime/mock_actor.py | 9 +-- dapr/actor/runtime/state_manager.py | 5 -- .../en/python-sdk-docs/python-actor.md | 3 + docs/clients/clients.grpc.rst | 31 +++++++ docs/proto/proto.runtime.rst | 18 +++++ docs/proto/proto.runtime.v1.rst | 37 +++++++++ .../demo_actor/demo_actor/test_demo_actor.py | 8 +- tests/actor/test_mock_actor.py | 80 +++++++++---------- tests/actor/test_mock_state_manager.py | 24 +++--- 9 files changed, 149 insertions(+), 66 deletions(-) create mode 100644 docs/clients/clients.grpc.rst create mode 100644 docs/proto/proto.runtime.rst create mode 100644 docs/proto/proto.runtime.v1.rst diff --git a/dapr/actor/runtime/mock_actor.py b/dapr/actor/runtime/mock_actor.py index 0da0aa8d..e35baac5 100644 --- a/dapr/actor/runtime/mock_actor.py +++ b/dapr/actor/runtime/mock_actor.py @@ -21,7 +21,6 @@ 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): @@ -72,7 +71,7 @@ async def register_timer( """ name = name or self.__get_new_timer_name() timer = ActorTimerData(name, callback, state, due_time, period, ttl) - self._state_manager._mock_timers[name] = timer + self._state_manager._mock_timers[name] = timer # type: ignore async def unregister_timer(self, name: str) -> None: """Unregisters actor timer from self._state_manager._mock_timers. @@ -80,7 +79,7 @@ async def unregister_timer(self, name: str) -> None: Args: name (str): the name of the timer to unregister. """ - self._state_manager._mock_timers.pop(name, None) + self._state_manager._mock_timers.pop(name, None) # type: ignore async def register_reminder( self, @@ -102,7 +101,7 @@ async def register_reminder( 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 + self._state_manager._mock_reminders[name] = reminder # type: ignore async def unregister_reminder(self, name: str) -> None: """Unregisters actor reminder from self._state_manager._mock_reminders.. @@ -110,7 +109,7 @@ async def unregister_reminder(self, name: str) -> None: Args: name (str): the name of the reminder to unregister. """ - self._state_manager._mock_reminders.pop(name, None) + self._state_manager._mock_reminders.pop(name, None) # type: ignore T = TypeVar('T', bound=Actor) diff --git a/dapr/actor/runtime/state_manager.py b/dapr/actor/runtime/state_manager.py index d55e23bf..35cc33fb 100644 --- a/dapr/actor/runtime/state_manager.py +++ b/dapr/actor/runtime/state_manager.py @@ -21,8 +21,6 @@ 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') @@ -70,9 +68,6 @@ 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-actor.md index 260725f2..a71c7db5 100644 --- a/daprdocs/content/en/python-sdk-docs/python-actor.md +++ b/daprdocs/content/en/python-sdk-docs/python-actor.md @@ -82,6 +82,9 @@ assert mockactor._state_manager._mock_state['mystate'] == 5 #True Mock actors are created by passing your actor class and an actor ID (a string) to the create_mock_actor function. This function returns an instance of the actor with many internal methods overridden. Instead of interacting with Dapr for tasks like saving state or managing timers, the mock actor uses in-memory state to simulate these behaviors. This state can be accessed through the following variables: + +**IMPORTANT NOTE: Due to type hinting issues as discussed further down, these variables will not be visible to type hinters/linters/etc, who will think they are invalid variables. You will need to use them with #type: ignore in order to satisfy any such systems.** + - **_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 pair. Any value loaded via `try_get_state` or any other statemanager method is taken from this dict. diff --git a/docs/clients/clients.grpc.rst b/docs/clients/clients.grpc.rst new file mode 100644 index 00000000..472cbce6 --- /dev/null +++ b/docs/clients/clients.grpc.rst @@ -0,0 +1,31 @@ +clients.grpc package +==================== + +Submodules +---------- + + +.. automodule:: clients.grpc.client + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: clients.grpc.interceptors + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: clients.grpc.subscription + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: clients.grpc + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/proto/proto.runtime.rst b/docs/proto/proto.runtime.rst new file mode 100644 index 00000000..ccad378d --- /dev/null +++ b/docs/proto/proto.runtime.rst @@ -0,0 +1,18 @@ +proto.runtime package +===================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + proto.runtime.v1 + +Module contents +--------------- + +.. automodule:: proto.runtime + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/proto/proto.runtime.v1.rst b/docs/proto/proto.runtime.v1.rst new file mode 100644 index 00000000..3d4fb150 --- /dev/null +++ b/docs/proto/proto.runtime.v1.rst @@ -0,0 +1,37 @@ +proto.runtime.v1 package +======================== + +Submodules +---------- + + +.. automodule:: proto.runtime.v1.appcallback_pb2 + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: proto.runtime.v1.appcallback_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: proto.runtime.v1.dapr_pb2 + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: proto.runtime.v1.dapr_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: proto.runtime.v1 + :members: + :undoc-members: + :show-inheritance: diff --git a/examples/demo_actor/demo_actor/test_demo_actor.py b/examples/demo_actor/demo_actor/test_demo_actor.py index d25185b0..e4289dda 100644 --- a/examples/demo_actor/demo_actor/test_demo_actor.py +++ b/examples/demo_actor/demo_actor/test_demo_actor.py @@ -12,7 +12,7 @@ def test_create_actor(self): async def test_get_data(self): mockactor = create_mock_actor(DemoActor, '1') - self.assertFalse(mockactor._state_manager._mock_state) + self.assertFalse(mockactor._state_manager._mock_state) # type: ignore val = await mockactor.get_my_data() self.assertIsNone(val) @@ -33,8 +33,8 @@ async def test_clear_data(self): async def test_reminder(self): mockactor = create_mock_actor(DemoActor, '1') - self.assertFalse(mockactor._state_manager._mock_reminders) + self.assertFalse(mockactor._state_manager._mock_reminders) # type: ignore await mockactor.set_reminder(True) - self.assertTrue('demo_reminder' in mockactor._state_manager._mock_reminders) + self.assertTrue('demo_reminder' in mockactor._state_manager._mock_reminders) # type: ignore await mockactor.set_reminder(False) - self.assertFalse(mockactor._state_manager._mock_reminders) + self.assertFalse(mockactor._state_manager._mock_reminders) # type: ignore diff --git a/tests/actor/test_mock_actor.py b/tests/actor/test_mock_actor.py index abed6448..c37cdf4f 100644 --- a/tests/actor/test_mock_actor.py +++ b/tests/actor/test_mock_actor.py @@ -150,14 +150,14 @@ def test_create_actor(self): 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) + self.assertTrue('state' in mockactor._state_manager._mock_state) # type: ignore + self.assertEqual(mockactor._state_manager._mock_state['state'], 5) # type: ignore 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}) + self.assertTrue('state' in mockactor._state_manager._mock_state) # type: ignore + self.assertEqual(mockactor._state_manager._mock_state['state'], {'test': 5}) # type: ignore async def test_get_data(self): mockactor = create_mock_actor(MockTestActor, '1') @@ -173,48 +173,48 @@ async def test_get_data_initstate(self): 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}) + self.assertTrue('state' in mockactor._state_manager._mock_state) # type: ignore + self.assertEqual(mockactor._state_manager._mock_state['state'], {'test': 5}) # type: ignore 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}) + self.assertTrue('state' in mockactor._state_manager._mock_state) # type: ignore + self.assertEqual(mockactor._state_manager._mock_state['state'], {'test': 10}) # type: ignore 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}) + self.assertTrue('state' in mockactor._state_manager._mock_state) # type: ignore + self.assertEqual(mockactor._state_manager._mock_state['state'], {'test': 5}) # type: ignore await mockactor.clear_data() - self.assertFalse('state' in mockactor._state_manager._mock_state) - self.assertIsNone(mockactor._state_manager._mock_state.get('state')) + self.assertFalse('state' in mockactor._state_manager._mock_state) # type: ignore + self.assertIsNone(mockactor._state_manager._mock_state.get('state')) # type: ignore 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) + self.assertEqual(len(mockactor._state_manager._mock_reminders), 0) # type: ignore 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.assertEqual(len(mockactor._state_manager._mock_reminders), 1) # type: ignore + self.assertTrue('test' in mockactor._state_manager._mock_reminders) # type: ignore + reminderstate = mockactor._state_manager._mock_reminders['test'] # type: ignore self.assertTrue(reminderstate.reminder_name, 'test') await mockactor.toggle_reminder('test', False) - self.assertEqual(len(mockactor._state_manager._mock_reminders), 0) + self.assertEqual(len(mockactor._state_manager._mock_reminders), 0) # type: ignore 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) + self.assertEqual(len(mockactor._state_manager._mock_timers), 0) # type: ignore 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.assertEqual(len(mockactor._state_manager._mock_timers), 1) # type: ignore + self.assertTrue('test' in mockactor._state_manager._mock_timers) # type: ignore + timerstate = mockactor._state_manager._mock_timers['test'] # type: ignore self.assertTrue(timerstate.timer_name, 'test') await mockactor.toggle_timer('test', False) - self.assertEqual(len(mockactor._state_manager._mock_timers), 0) + self.assertEqual(len(mockactor._state_manager._mock_timers), 0) # type: ignore async def test_activate_reminder(self): mockactor = create_mock_actor(MockTestActor, '1') @@ -225,7 +225,7 @@ async def test_activate_reminder(self): datetime.timedelta(days=1), datetime.timedelta(days=1), ) - self.assertEqual(mockactor._state_manager._mock_state['test'], True) + self.assertEqual(mockactor._state_manager._mock_state['test'], True) # type: ignore async def test_test_data(self): mockactor = create_mock_actor(MockTestActor, '1') @@ -246,27 +246,27 @@ async def test_test_data(self): async def test_add_state(self): mockactor = create_mock_actor(MockTestActor, '1') - self.assertFalse(mockactor._state_manager._mock_state) + self.assertFalse(mockactor._state_manager._mock_state) # type: ignore await mockactor.add_state('test', 5) - self.assertTrue('test' in mockactor._state_manager._mock_state) - self.assertEqual(mockactor._state_manager._mock_state['test'], 5) + self.assertTrue('test' in mockactor._state_manager._mock_state) # type: ignore + self.assertEqual(mockactor._state_manager._mock_state['test'], 5) # type: ignore await mockactor.add_state('test2', 10) - self.assertTrue('test2' in mockactor._state_manager._mock_state) - self.assertEqual(mockactor._state_manager._mock_state['test2'], 10) - self.assertEqual(len(mockactor._state_manager._mock_state), 2) + self.assertTrue('test2' in mockactor._state_manager._mock_state) # type: ignore + self.assertEqual(mockactor._state_manager._mock_state['test2'], 10) # type: ignore + self.assertEqual(len(mockactor._state_manager._mock_state), 2) # type: ignore with self.assertRaises(ValueError): await mockactor.add_state('test', 10) async def test_update_state(self): mockactor = create_mock_actor(MockTestActor, '1') - self.assertFalse(mockactor._state_manager._mock_state) + self.assertFalse(mockactor._state_manager._mock_state) # type: ignore await mockactor.update_state('test', 10) - self.assertTrue('test' in mockactor._state_manager._mock_state) - self.assertEqual(mockactor._state_manager._mock_state['test'], 10) + self.assertTrue('test' in mockactor._state_manager._mock_state) # type: ignore + self.assertEqual(mockactor._state_manager._mock_state['test'], 10) # type: ignore await mockactor.update_state('test', 10) - self.assertTrue('test' in mockactor._state_manager._mock_state) - self.assertEqual(mockactor._state_manager._mock_state['test'], 20) - self.assertEqual(len(mockactor._state_manager._mock_state), 1) + self.assertTrue('test' in mockactor._state_manager._mock_state) # type: ignore + self.assertEqual(mockactor._state_manager._mock_state['test'], 20) # type: ignore + self.assertEqual(len(mockactor._state_manager._mock_state), 1) # type: ignore async def test_state_change_tracker(self): mockactor = create_mock_actor(MockTestActor, '1') @@ -281,7 +281,7 @@ async def test_state_change_tracker(self): self.assertEqual( mockactor._state_manager._default_state_change_tracker['state'].value, {'test': 5} ) - self.assertEqual(mockactor._state_manager._mock_state['state'], {'test': 5}) + self.assertEqual(mockactor._state_manager._mock_state['state'], {'test': 5}) # type: ignore await mockactor.remove_data_no_save() self.assertEqual( mockactor._state_manager._default_state_change_tracker['state'].change_kind, @@ -290,10 +290,10 @@ async def test_state_change_tracker(self): self.assertEqual( mockactor._state_manager._default_state_change_tracker['state'].value, {'test': 5} ) - self.assertTrue('state' not in mockactor._state_manager._mock_state) + self.assertTrue('state' not in mockactor._state_manager._mock_state) # type: ignore 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) + self.assertTrue('state' not in mockactor._state_manager._mock_state) # type: ignore await mockactor.add_data_no_save({'test': 6}) self.assertEqual( mockactor._state_manager._default_state_change_tracker['state'].change_kind, @@ -302,7 +302,7 @@ async def test_state_change_tracker(self): self.assertEqual( mockactor._state_manager._default_state_change_tracker['state'].value, {'test': 6} ) - self.assertEqual(mockactor._state_manager._mock_state['state'], {'test': 6}) + self.assertEqual(mockactor._state_manager._mock_state['state'], {'test': 6}) # type: ignore await mockactor.save_state() self.assertEqual( mockactor._state_manager._default_state_change_tracker['state'].change_kind, @@ -311,4 +311,4 @@ async def test_state_change_tracker(self): self.assertEqual( mockactor._state_manager._default_state_change_tracker['state'].value, {'test': 6} ) - self.assertEqual(mockactor._state_manager._mock_state['state'], {'test': 6}) + self.assertEqual(mockactor._state_manager._mock_state['state'], {'test': 6}) # type: ignore diff --git a/tests/actor/test_mock_state_manager.py b/tests/actor/test_mock_state_manager.py index ddd1ee04..81af9307 100644 --- a/tests/actor/test_mock_state_manager.py +++ b/tests/actor/test_mock_state_manager.py @@ -23,9 +23,9 @@ def test_init_state(self): mock_actor = create_mock_actor(MockTestActor, 'test') state_manager = mock_actor._state_manager self.assertIsInstance(state_manager, MockStateManager) - self.assertFalse(state_manager._mock_state) - self.assertFalse(state_manager._mock_reminders) - self.assertFalse(state_manager._mock_timers) + self.assertFalse(state_manager._mock_state) # type: ignore + self.assertFalse(state_manager._mock_reminders) # type: ignore + self.assertFalse(state_manager._mock_timers) # type: ignore async def test_state_methods(self): mock_actor = create_mock_actor(MockTestActor, 'test') @@ -39,33 +39,33 @@ async def test_state_methods(self): await state_manager.add_state('state', 5) names = await state_manager.get_state_names() self.assertCountEqual(names, ['state']) - self.assertIs(state_manager._mock_state['state'], 5) + self.assertIs(state_manager._mock_state['state'], 5) # type: ignore value = await state_manager.get_state('state') self.assertIs(value, 5) await state_manager.add_state('state2', 5) - self.assertIs(state_manager._mock_state['state2'], 5) + self.assertIs(state_manager._mock_state['state2'], 5) # type: ignore with self.assertRaises(ValueError): await state_manager.add_state('state', 5) await state_manager.set_state('state3', 5) - self.assertIs(state_manager._mock_state['state3'], 5) + self.assertIs(state_manager._mock_state['state3'], 5) # type: ignore await state_manager.set_state('state3', 10) - self.assertIs(state_manager._mock_state['state3'], 10) + self.assertIs(state_manager._mock_state['state3'], 10) # type: ignore self.assertTrue(await state_manager.contains_state('state3')) await state_manager.remove_state('state3') - self.assertFalse('state3' in state_manager._mock_state) + self.assertFalse('state3' in state_manager._mock_state) # type: ignore with self.assertRaises(KeyError): await state_manager.remove_state('state3') self.assertFalse(await state_manager.contains_state('state3')) await state_manager.add_or_update_state('state3', 5, double) - self.assertIs(state_manager._mock_state['state3'], 5) + self.assertIs(state_manager._mock_state['state3'], 5) # type: ignore await state_manager.add_or_update_state('state3', 1000, double) - self.assertIs(state_manager._mock_state['state3'], 10) + self.assertIs(state_manager._mock_state['state3'], 10) # type: ignore out = await state_manager.get_or_add_state('state4', 5) self.assertIs(out, 5) - self.assertIs(state_manager._mock_state['state4'], 5) + self.assertIs(state_manager._mock_state['state4'], 5) # type: ignore out = await state_manager.get_or_add_state('state4', 10) self.assertIs(out, 5) - self.assertIs(state_manager._mock_state['state4'], 5) + self.assertIs(state_manager._mock_state['state4'], 5) # type: ignore names = await state_manager.get_state_names() self.assertCountEqual(names, ['state', 'state2', 'state3', 'state4']) self.assertTrue('state', state_manager._default_state_change_tracker) From d72be8e0f3cff7d4970bf787d7c2e941a3bd8d14 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Thu, 2 Jan 2025 11:46:44 +0000 Subject: [PATCH 28/29] small docs change Signed-off-by: Elena Kolevska --- daprdocs/content/en/python-sdk-docs/python-actor.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daprdocs/content/en/python-sdk-docs/python-actor.md b/daprdocs/content/en/python-sdk-docs/python-actor.md index a71c7db5..bd85e996 100644 --- a/daprdocs/content/en/python-sdk-docs/python-actor.md +++ b/daprdocs/content/en/python-sdk-docs/python-actor.md @@ -113,7 +113,7 @@ The `__init__`, `register_timer`, `unregister_timer`, `register_reminder`, `unre ``` *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__`.* -*If you have an actor which does override default Dapr actor methods, your best bet is likely to create a custom subclass of the MockActor class (from MockActor.py) which implements whatever custom logic you have along with interacting with `_mock_state`, `_mock_timers`, and `_mock_reminders` as normal, and then appyling that custom class as a mixin via a `create_mock_actor` function you define yourself.* +*If you have an actor which does override default Dapr actor methods, you can create a custom subclass of the MockActor class (from MockActor.py) which implements whatever custom logic you have along with interacting with `_mock_state`, `_mock_timers`, and `_mock_reminders` as normal, and then applying that custom class as a mixin via a `create_mock_actor` function you define yourself.* The actor `_runtime_ctx` variable is set to None. All the normal actor methods have been overwritten such as to not call it, but if your code itself interacts directly with `_runtime_ctx`, tests may fail. From 6b3b3ef32fef849a955e4487f480f5b83a66ad37 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Thu, 2 Jan 2025 11:46:44 +0000 Subject: [PATCH 29/29] examples test fix Signed-off-by: Elena Kolevska --- .../en/python-sdk-docs/python-actor.md | 2 +- examples/demo_actor/README.md | 35 +++++++++---------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/daprdocs/content/en/python-sdk-docs/python-actor.md b/daprdocs/content/en/python-sdk-docs/python-actor.md index a71c7db5..bd85e996 100644 --- a/daprdocs/content/en/python-sdk-docs/python-actor.md +++ b/daprdocs/content/en/python-sdk-docs/python-actor.md @@ -113,7 +113,7 @@ The `__init__`, `register_timer`, `unregister_timer`, `register_reminder`, `unre ``` *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__`.* -*If you have an actor which does override default Dapr actor methods, your best bet is likely to create a custom subclass of the MockActor class (from MockActor.py) which implements whatever custom logic you have along with interacting with `_mock_state`, `_mock_timers`, and `_mock_reminders` as normal, and then appyling that custom class as a mixin via a `create_mock_actor` function you define yourself.* +*If you have an actor which does override default Dapr actor methods, you can create a custom subclass of the MockActor class (from MockActor.py) which implements whatever custom logic you have along with interacting with `_mock_state`, `_mock_timers`, and `_mock_reminders` as normal, and then applying that custom class as a mixin via a `create_mock_actor` function you define yourself.* The actor `_runtime_ctx` variable is set to None. All the normal actor methods have been overwritten such as to not call it, but if your code itself interacts directly with `_runtime_ctx`, tests may fail. diff --git a/examples/demo_actor/README.md b/examples/demo_actor/README.md index d45c15b9..64ca7ca5 100644 --- a/examples/demo_actor/README.md +++ b/examples/demo_actor/README.md @@ -218,26 +218,23 @@ timeout_seconds: 60 Expected output (note that the unit test print outputs might not necessarily be in this order - what really matters is that all tests pass anyway): - ``` - has_value: False - set_my_data: {'state': 5} - has_value: True - clear_my_data - has_value: False - ..has_value: False - .set reminder to True - set reminder is done - set reminder to False - set reminder is done - .has_value: False - set_my_data: {'state': 5} - has_value: True - . - ---------------------------------------------------------------------- - Ran 5 tests in 0.052s + ``` + set_my_data: {'state': 5} + has_value: True + clear_my_data + has_value: False + has_value: False + set reminder to True + set reminder is done + set reminder to False + set reminder is done + set_my_data: {'state': 5} + has_value: True + ---------------------------------------------------------------------- + Ran 5 tests in 0.052s - OK - ``` + OK + ```