Skip to content

Commit

Permalink
Merge pull request #103 from tonyandrewmeyer/actionevent-id-101
Browse files Browse the repository at this point in the history
feat: add support for ActionEvent.id
  • Loading branch information
PietroPasotti authored Mar 1, 2024
2 parents 4a1ec5f + e03b3ee commit 94ff159
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 11 deletions.
17 changes: 9 additions & 8 deletions scenario/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,25 +180,26 @@ def _cleanup_env(env):

def _get_event_env(self, state: "State", event: "Event", charm_root: Path):
"""Build the simulated environment the operator framework expects."""
if event.name.endswith("_action"):
# todo: do we need some special metadata, or can we assume action names
# are always dashes?
action_name = event.name[: -len("_action")].replace("_", "-")
else:
action_name = ""

env = {
"JUJU_VERSION": self._juju_version,
"JUJU_UNIT_NAME": f"{self._app_name}/{self._unit_id}",
"_": "./dispatch",
"JUJU_DISPATCH_PATH": f"hooks/{event.name}",
"JUJU_MODEL_NAME": state.model.name,
"JUJU_ACTION_NAME": action_name,
"JUJU_MODEL_UUID": state.model.uuid,
"JUJU_CHARM_DIR": str(charm_root.absolute()),
# todo consider setting pwd, (python)path
}

if event._is_action_event and (action := event.action):
env.update(
{
# TODO: we should check we're doing the right thing here.
"JUJU_ACTION_NAME": action.name.replace("_", "-"),
"JUJU_ACTION_UUID": action.id,
},
)

if event._is_relation_event and (relation := event.relation):
if isinstance(relation, PeerRelation):
remote_app_name = self._app_name
Expand Down
8 changes: 5 additions & 3 deletions scenario/sequences.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,11 @@ def generate_startup_sequence(state_template: State):
(
(
Event(
"leader_elected"
if state_template.leader
else "leader_settings_changed",
(
"leader_elected"
if state_template.leader
else "leader_settings_changed"
),
),
state_template.copy(),
),
Expand Down
19 changes: 19 additions & 0 deletions scenario/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -1423,12 +1423,31 @@ def deferred(self, handler: Callable, event_id: int = 1) -> DeferredEvent:
)


_next_action_id_counter = 1


def next_action_id(update=True):
global _next_action_id_counter
cur = _next_action_id_counter
if update:
_next_action_id_counter += 1
# Juju currently uses numbers for the ID, but in the past used UUIDs, so
# we need these to be strings.
return str(cur)


@dataclasses.dataclass(frozen=True)
class Action(_DCBase):
name: str

params: Dict[str, "AnyJson"] = dataclasses.field(default_factory=dict)

id: str = dataclasses.field(default_factory=next_action_id)
"""Juju action ID.
Every action invocation is automatically assigned a new one. Override in
the rare cases where a specific ID is required."""

@property
def event(self) -> Event:
"""Helper to generate an action event from this action."""
Expand Down
3 changes: 3 additions & 0 deletions tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from ops import CharmBase

from scenario import Action, Context, Event, State
from scenario.state import next_action_id


class MyCharm(CharmBase):
Expand Down Expand Up @@ -31,6 +32,7 @@ def test_run():
def test_run_action():
ctx = Context(MyCharm, meta={"name": "foo"})
state = State()
expected_id = next_action_id(update=False)

with patch.object(ctx, "_run_action") as p:
ctx._output_state = (
Expand All @@ -46,6 +48,7 @@ def test_run_action():
assert isinstance(a, Action)
assert a.event.name == "do_foo_action"
assert s is state
assert a.id == expected_id


def test_clear():
Expand Down
44 changes: 44 additions & 0 deletions tests/test_e2e/test_actions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
from ops import __version__ as ops_version
from ops.charm import ActionEvent, CharmBase
from ops.framework import Framework

Expand Down Expand Up @@ -135,3 +136,46 @@ def handle_evt(charm: CharmBase, evt: ActionEvent):
assert out.failure == "failed becozz"
assert out.logs == ["log1", "log2"]
assert out.success is False


def _ops_less_than(wanted_major, wanted_minor):
major, minor = (int(v) for v in ops_version.split(".")[:2])
if major < wanted_major:
return True
if major == wanted_major and minor < wanted_minor:
return True
return False


@pytest.mark.skipif(
_ops_less_than(2, 11), reason="ops 2.10 and earlier don't have ActionEvent.id"
)
def test_action_event_has_id(mycharm):
def handle_evt(charm: CharmBase, evt: ActionEvent):
if not isinstance(evt, ActionEvent):
return
assert isinstance(evt.id, str) and evt.id != ""

mycharm._evt_handler = handle_evt

action = Action("foo")
ctx = Context(mycharm, meta={"name": "foo"}, actions={"foo": {}})
ctx.run_action(action, State())


@pytest.mark.skipif(
_ops_less_than(2, 11), reason="ops 2.10 and earlier don't have ActionEvent.id"
)
def test_action_event_has_override_id(mycharm):
uuid = "0ddba11-cafe-ba1d-5a1e-dec0debad"

def handle_evt(charm: CharmBase, evt: ActionEvent):
if not isinstance(evt, ActionEvent):
return
assert evt.id == uuid

mycharm._evt_handler = handle_evt

action = Action("foo", id=uuid)
ctx = Context(mycharm, meta={"name": "foo"}, actions={"foo": {}})
ctx.run_action(action, State())

0 comments on commit 94ff159

Please sign in to comment.