Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Actor Mocks #750

Merged
merged 35 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
92d11b0
Moved files to new branch to avoid weird git bug
lor1113 Nov 1, 2024
aee0fe0
requested documentation changes
lor1113 Nov 11, 2024
385456a
Merge branch 'dapr:main' into mock-actors-3
lor1113 Nov 11, 2024
faadd82
forgot to move file back to starting point
lor1113 Nov 11, 2024
45978e2
Merge branch 'mock-actors-3' of github.com:lor1113/python-sdk into mo…
lor1113 Nov 11, 2024
9d5e626
result of ruff format
lor1113 Nov 18, 2024
e17a85b
fixed minor formatting issues, fixed type issues
lor1113 Nov 19, 2024
278ba8d
Merge branch 'main' into mock-actors-3
elena-kolevska Nov 29, 2024
c23cc33
minor test fix
Dec 3, 2024
e67659d
fixes try_add_state
elena-kolevska Dec 3, 2024
cc6ee78
Revert "fixes try_add_state"
elena-kolevska Dec 3, 2024
6cebadd
Update dapr/actor/runtime/mock_state_manager.py
lor1113 Dec 3, 2024
bebdcb1
Update dapr/actor/runtime/mock_actor.py
lor1113 Dec 3, 2024
1f1569f
Update daprdocs/content/en/python-sdk-docs/python-actor.md
lor1113 Dec 3, 2024
c3e6aca
Update daprdocs/content/en/python-sdk-docs/python-actor.md
lor1113 Dec 3, 2024
ef74591
Update daprdocs/content/en/python-sdk-docs/python-actor.md
lor1113 Dec 3, 2024
b0650a6
Update daprdocs/content/en/python-sdk-docs/python-actor.md
lor1113 Dec 3, 2024
1733b42
Update daprdocs/content/en/python-sdk-docs/python-actor.md
lor1113 Dec 3, 2024
df85211
Update daprdocs/content/en/python-sdk-docs/python-actor.md
lor1113 Dec 3, 2024
a7b86c7
Update daprdocs/content/en/python-sdk-docs/python-actor.md
lor1113 Dec 3, 2024
1a82754
minor error in docs
Dec 3, 2024
2480d7e
fixed and added more unit tests. Added example
Dec 3, 2024
33a5d1d
unittest fix
Dec 3, 2024
4fa7fb8
Update examples/demo_actor/README.md
lor1113 Dec 3, 2024
85b8d6c
concentrated some tests
Dec 3, 2024
e788174
removed unnecessary type hint
Dec 3, 2024
e391eda
Update daprdocs/content/en/python-sdk-docs/python-actor.md
lor1113 Dec 4, 2024
97bc300
Update examples/demo_actor/README.md
lor1113 Dec 10, 2024
9314788
documentation changes
Dec 10, 2024
7c4e5fc
Merge branch 'mock-actors-3' of github.com:lor1113/python-sdk into mo…
Dec 10, 2024
e180073
Merge branch 'main' into mock-actors-3
elena-kolevska Dec 19, 2024
55d9106
now requires #type: ignore
Jan 2, 2025
d72be8e
small docs change
elena-kolevska Jan 2, 2025
6b3b3ef
examples test fix
elena-kolevska Jan 2, 2025
d095086
Merge branch 'mock-actors-3' of github.com:lor1113/python-sdk into mo…
elena-kolevska Jan 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions dapr/actor/runtime/mock_actor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""
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 __future__ import annotations

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


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, "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}
"""

def __init__(self, actor_id: str, initstate: Optional[dict]):
self.id = ActorId(actor_id)
self._runtime_ctx = None # type: ignore
self._state_manager = 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 # type: ignore

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) # type: ignore

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 # type: ignore

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) # type: ignore


T = TypeVar('T', bound=Actor)


def create_mock_actor(cls1: type[T], actor_id: str, initstate: Optional[dict] = None) -> T:
class MockSuperClass(MockActor, cls1): # type: ignore
pass

return MockSuperClass(actor_id, initstate) # type: ignore
238 changes: 238 additions & 0 deletions dapr/actor/runtime/mock_state_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
"""
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

Check warning on line 24 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L24

Added line #L24 was not covered by tests

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(

Check warning on line 48 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L48

Added line #L48 was not covered by tests
value, StateChangeKind.update
)
return True

Check warning on line 51 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L51

Added line #L51 was not covered by tests
return False
existed = state_name in self._mock_state
if existed:
return False

Check warning on line 55 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L55

Added line #L55 was not covered by tests
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

Check warning on line 71 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L71

Added line #L71 was not covered by tests
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

Check warning on line 86 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L86

Added line #L86 was not covered by tests

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(

Check warning on line 104 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L104

Added line #L104 was not covered by tests
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

Check warning on line 121 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L121

Added line #L121 was not covered by tests
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(

Check warning on line 132 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L132

Added line #L132 was not covered by tests
None, StateChangeKind.remove
)
self._mock_state.pop(state_name, None)
return True

Check warning on line 136 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L135-L136

Added lines #L135 - L136 were not covered by tests
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._mock_state[state_name] = value
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')

Check warning on line 162 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L162

Added line #L162 was not covered by tests

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(

Check warning on line 167 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L167

Added line #L167 was not covered by tests
value, StateChangeKind.update
)
self._mock_state[state_name] = value
return value

Check warning on line 171 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L170-L171

Added lines #L170 - L171 were not covered by tests
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

Check warning on line 175 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L175

Added line #L175 was not covered by tests
self._default_state_change_tracker[state_name] = state_metadata
self._mock_state[state_name] = new_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(

Check warning on line 184 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L183-L184

Added lines #L183 - L184 were not covered by tests
new_value, StateChangeKind.update
)
self._mock_state[state_name] = new_value
return new_value

Check warning on line 188 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L187-L188

Added lines #L187 - L188 were not covered by tests
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]:
# 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)

Check warning on line 201 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L200-L201

Added lines #L200 - L201 were not covered by tests
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

Check warning on line 212 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L212

Added line #L212 was not covered by tests

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

Check warning on line 218 in dapr/actor/runtime/mock_state_manager.py

View check run for this annotation

Codecov / codecov/patch

dapr/actor/runtime/mock_state_manager.py#L218

Added line #L218 was not covered by tests
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
)
5 changes: 2 additions & 3 deletions dapr/actor/runtime/state_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@

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.actor import Actor
Expand Down
Loading
Loading