Skip to content

Commit

Permalink
Remove the deferred() shortcut, and discourage using DeferredEvent di…
Browse files Browse the repository at this point in the history
…rectly. This means that event.deferred() is the one-true-way-tm to generate a deferred event in a test.
  • Loading branch information
tonyandrewmeyer committed Aug 14, 2024
1 parent 2894855 commit c6f05db
Show file tree
Hide file tree
Showing 4 changed files with 15 additions and 122 deletions.
54 changes: 5 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ with scenario.capture_events.capture_events() as emitted:
ctx = scenario.Context(SimpleCharm, meta={"name": "capture"})
state_out = ctx.run(
ctx.on.update_status(),
scenario.State(deferred=[scenario.deferred("start", SimpleCharm._on_start)])
scenario.State(deferred=[ctx.on.start().deferred(SimpleCharm._on_start)])
)

# deferred events get reemitted first
Expand Down Expand Up @@ -1050,7 +1050,7 @@ def test_backup_action():
Scenario allows you to accurately simulate the Operator Framework's event queue. The event queue is responsible for
keeping track of the deferred events. On the input side, you can verify that if the charm triggers with this and that
event in its queue (they would be there because they had been deferred in the previous run), then the output state is
valid.
valid. You generate the deferred data structure using the event's `deferred()` method:

```python
class MyCharm(ops.CharmBase):
Expand All @@ -1065,26 +1065,17 @@ class MyCharm(ops.CharmBase):

def test_start_on_deferred_update_status(MyCharm):
"""Test charm execution if a 'start' is dispatched when in the previous run an update-status had been deferred."""
ctx = scenario.Context(MyCharm)
state_in = scenario.State(
deferred=[
scenario.deferred('update_status', handler=MyCharm._on_update_status)
ctx.on.update_status().deferred(handler=MyCharm._on_update_status)
]
)
state_out = scenario.Context(MyCharm).run(ctx.on.start(), state_in)
state_out = ctx.run(ctx.on.start(), state_in)
assert len(state_out.deferred) == 1
assert state_out.deferred[0].name == 'start'
```

You can also generate the 'deferred' data structure (called a DeferredEvent) from the corresponding Event (and the
handler):

```python continuation
ctx = scenario.Context(MyCharm, meta={"name": "deferring"})

deferred_start = ctx.on.start().deferred(MyCharm._on_start)
deferred_install = ctx.on.install().deferred(MyCharm._on_start)
```

On the output side, you can verify that an event that you expect to have been deferred during this trigger, has indeed
been deferred.

Expand All @@ -1102,41 +1093,6 @@ def test_defer(MyCharm):
assert out.deferred[0].name == 'start'
```

## Deferring relation events

If you want to test relation event deferrals, some extra care needs to be taken. RelationEvents hold references to the
Relation instance they are about. So do they in Scenario. You can use the deferred helper to generate the data
structure:

```python
class MyCharm(ops.CharmBase):
...

def _on_foo_relation_changed(self, event):
event.defer()


def test_start_on_deferred_update_status(MyCharm):
foo_relation = scenario.Relation('foo')
scenario.State(
relations={foo_relation},
deferred=[
scenario.deferred('foo_relation_changed',
handler=MyCharm._on_foo_relation_changed,
relation=foo_relation)
]
)
```

but you can also use a shortcut from the relation event itself:

```python continuation
ctx = scenario.Context(MyCharm, meta={"name": "deferring"})

foo_relation = scenario.Relation('foo')
deferred_event = ctx.on.relation_changed(foo_relation).deferred(handler=MyCharm._on_foo_relation_changed)
```

# Live charm introspection

Scenario is a black-box, state-transition testing framework. It makes it trivial to assert that a status went from A to
Expand Down
2 changes: 0 additions & 2 deletions scenario/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
UDPPort,
UnknownStatus,
WaitingStatus,
deferred,
)

__all__ = [
Expand All @@ -44,7 +43,6 @@
"CloudCredential",
"CloudSpec",
"Context",
"deferred",
"StateValidationError",
"Secret",
"Relation",
Expand Down
30 changes: 7 additions & 23 deletions scenario/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -1555,8 +1555,13 @@ def get_all_relations(self) -> List[Tuple[str, Dict[str, str]]]:
class DeferredEvent:
"""An event that has been deferred to run prior to the next Juju event.
In most cases, the :func:`deferred` function should be used to create a
``DeferredEvent`` instance."""
Tests should not instantiate this class directly: use :meth:`_Event.deferred`
instead. For example:
ctx = Context(MyCharm)
deferred_start = ctx.on.start().deferred()
state = State(deferred=[deferred_start])
"""

handle_path: str
owner: str
Expand Down Expand Up @@ -1862,24 +1867,3 @@ def test_backup_action():
Every action invocation is automatically assigned a new one. Override in
the rare cases where a specific ID is required."""


def deferred(
event: Union[str, _Event],
handler: Callable,
event_id: int = 1,
relation: Optional["Relation"] = None,
container: Optional["Container"] = None,
notice: Optional["Notice"] = None,
check_info: Optional["CheckInfo"] = None,
):
"""Construct a DeferredEvent from an Event or an event name."""
if isinstance(event, str):
event = _Event(
event,
relation=relation,
container=container,
notice=notice,
check_info=check_info,
)
return event.deferred(handler=handler, event_id=event_id)
51 changes: 3 additions & 48 deletions tests/test_e2e/test_deferred.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from ops.framework import Framework

from scenario import Context
from scenario.state import Container, Notice, Relation, State, _Event, deferred
from scenario.state import Container, Notice, Relation, State, _Event
from tests.helpers import trigger

CHARM_CALLED = 0
Expand Down Expand Up @@ -54,7 +54,7 @@ def test_deferred_evt_emitted(mycharm):
mycharm.defer_next = 2

out = trigger(
State(deferred=[deferred(event="update_status", handler=mycharm._on_event)]),
State(deferred=[_Event("update_status").deferred(handler=mycharm._on_event)]),
"start",
mycharm,
meta=mycharm.META,
Expand All @@ -72,49 +72,6 @@ def test_deferred_evt_emitted(mycharm):
assert isinstance(start, StartEvent)


def test_deferred_relation_event_without_relation_raises(mycharm):
with pytest.raises(AttributeError):
deferred(event="foo_relation_changed", handler=mycharm._on_event)


def test_deferred_relation_evt(mycharm):
rel = Relation(endpoint="foo", remote_app_name="remote")
evt1 = _Event("foo_relation_changed", relation=rel).deferred(
handler=mycharm._on_event
)
evt2 = deferred(
event="foo_relation_changed",
handler=mycharm._on_event,
relation=rel,
)

assert asdict(evt2) == asdict(evt1)


def test_deferred_workload_evt(mycharm):
ctr = Container("foo")
evt1 = _Event("foo_pebble_ready", container=ctr).deferred(handler=mycharm._on_event)
evt2 = deferred(event="foo_pebble_ready", handler=mycharm._on_event, container=ctr)

assert asdict(evt2) == asdict(evt1)


def test_deferred_notice_evt(mycharm):
notice = Notice(key="example.com/bar")
ctr = Container("foo", notices=[notice])
evt1 = _Event("foo_pebble_custom_notice", notice=notice, container=ctr).deferred(
handler=mycharm._on_event
)
evt2 = deferred(
event="foo_pebble_custom_notice",
handler=mycharm._on_event,
container=ctr,
notice=notice,
)

assert asdict(evt2) == asdict(evt1)


def test_deferred_relation_event(mycharm):
mycharm.defer_next = 2

Expand All @@ -124,10 +81,8 @@ def test_deferred_relation_event(mycharm):
State(
relations={rel},
deferred=[
deferred(
event="foo_relation_changed",
_Event("foo_relation_changed", relation=rel).deferred(
handler=mycharm._on_event,
relation=rel,
)
],
),
Expand Down

0 comments on commit c6f05db

Please sign in to comment.