Skip to content

Commit

Permalink
Simplify the task interface and rename back to ActionOutput.
Browse files Browse the repository at this point in the history
  • Loading branch information
tonyandrewmeyer committed Aug 6, 2024
1 parent 9885216 commit 7a3449c
Show file tree
Hide file tree
Showing 7 changed files with 33 additions and 32 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -977,15 +977,15 @@ def test_backup_action():
state = ctx.run(ctx.on.action("do_backup"), scenario.State())

# You can assert action results and logs using the action history:
assert ctx.action_history[0].logs == ['baz', 'qux']
assert ctx.action_history[0].results == {'foo': 'bar'}
assert ctx.action_output.logs == ['baz', 'qux']
assert ctx.action_output.results == {'foo': 'bar'}
```

## Failing Actions

If the charm code calls `event.fail()` to indicate that the action has failed,
an `ActionFailed` exception will be raised. This avoids having to include
`assert ctx.action_history[0].status == "completed"` code in every test where
`assert ctx.action_output.status == "completed"` code in every test where
the action is successful.

```python
Expand All @@ -997,8 +997,8 @@ def test_backup_action_failed():
assert exc_info.value.message == "sorry, couldn't do the backup"

# You can still assert action results and logs that occured before the failure:
assert ctx.action_history[0].logs == ['baz', 'qux']
assert ctx.action_history[0].results == {'foo': 'bar'}
assert ctx.action_output.logs == ['baz', 'qux']
assert ctx.action_output.results == {'foo': 'bar'}
```

## Parametrized Actions
Expand Down
4 changes: 2 additions & 2 deletions scenario/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.
from scenario.context import Context, Manager, Task
from scenario.context import ActionOutput, Context, Manager
from scenario.state import (
ActionFailed,
ActiveStatus,
Expand Down Expand Up @@ -39,7 +39,7 @@
)

__all__ = [
"Task",
"ActionOutput",
"ActionFailed",
"CheckInfo",
"CloudCredential",
Expand Down
11 changes: 6 additions & 5 deletions scenario/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@


@dataclasses.dataclass(frozen=True)
class Task(_max_posargs(0)):
class ActionOutput(_max_posargs(0)):
"""Wraps the results of running an action event on a unit.
Tests should generally not create instances of this class directly, but
rather use the :attr:`Context.action_history` attribute to inspect the
rather use the :attr:`Context.action_output` attribute to inspect the
results of running actions.
"""

Expand Down Expand Up @@ -496,7 +496,7 @@ def __init__(
self._output_state: Optional["State"] = None

# operations (and embedded tasks) from running actions
self.action_history: List[Task] = []
self.action_output: Optional[ActionOutput] = None

self.on = _CharmEvents()

Expand Down Expand Up @@ -563,11 +563,12 @@ def run(self, event: "_Event", state: "State") -> "State":
charm will invoke when handling the Event.
"""
if event.action:
self.action_history.append(Task())
self.action_output = ActionOutput()
with self._run(event=event, state=state) as ops:
ops.emit()
if event.action:
current_task = self.action_history[-1]
current_task = self.action_output
assert current_task is not None
if current_task.status == "failed":
raise ActionFailed(current_task.failure_message, self.output_state)
current_task.set_status("completed")
Expand Down
11 changes: 7 additions & 4 deletions scenario/mocking.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,22 +528,25 @@ def action_set(self, results: Dict[str, Any]):
_format_action_result_dict(results)
# but then we will store it in its unformatted,
# original form for testing ease
self._context.action_history[-1].update_results(results)
assert self._context.action_output is not None
self._context.action_output.update_results(results)

def action_fail(self, message: str = ""):
if not self._event.action:
raise ActionMissingFromContextError(
"not in the context of an action event: cannot action-fail",
)
self._context.action_history[-1].set_status("failed")
self._context.action_history[-1].set_failure_message(message)
assert self._context.action_output is not None
self._context.action_output.set_status("failed")
self._context.action_output.set_failure_message(message)

def action_log(self, message: str):
if not self._event.action:
raise ActionMissingFromContextError(
"not in the context of an action event: cannot action-log",
)
self._context.action_history[-1].logs.append(message)
assert self._context.action_output is not None
self._context.action_output.logs.append(message)

def action_get(self):
action = self._event.action
Expand Down
2 changes: 1 addition & 1 deletion scenario/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -1853,7 +1853,7 @@ def test_backup_action():
ctx.on.action('do_backup', params={'filename': 'foo'}),
scenario.State()
)
assert ctx.action_history[0].results == ...
assert ctx.action_output.results == ...
"""

name: str
Expand Down
15 changes: 7 additions & 8 deletions tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest
from ops import CharmBase

from scenario import Context, State, Task
from scenario import ActionOutput, Context, State
from scenario.state import _Event, next_action_id


Expand Down Expand Up @@ -70,9 +70,9 @@ def test_context_manager():
assert mgr.charm.meta.name == "foo"


def test_task_no_positional_arguments():
def test_action_output_no_positional_arguments():
with pytest.raises(TypeError):
Task(None)
ActionOutput(None)


def test_action_output_no_results():
Expand All @@ -86,8 +86,7 @@ def _on_act_action(self, _):

ctx = Context(MyCharm, meta={"name": "foo"}, actions={"act": {}})
ctx.run(ctx.on.action("act"), State())
assert len(ctx.action_history) == 1
task = ctx.action_history[0]
assert task.results is None
assert task.status == "completed"
assert task.failure_message == ""
action_output = ctx.action_output
assert action_output.results is None
assert action_output.status == "completed"
assert action_output.failure_message == ""
12 changes: 5 additions & 7 deletions tests/test_e2e/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,8 @@ def handle_evt(_: CharmBase, evt):

ctx.run(ctx.on.action("foo"), State())

assert len(ctx.action_history) == 1
assert ctx.action_history[0].results == res_value
assert ctx.action_history[0].status == "completed"
assert ctx.action_output.results == res_value
assert ctx.action_output.status == "completed"


@pytest.mark.parametrize("res_value", ({"a": {"b": {"c"}}}, {"d": "e"}))
Expand All @@ -90,8 +89,7 @@ def handle_evt(_: CharmBase, evt: ActionEvent):
with pytest.raises(ActionFailed) as out:
ctx.run(ctx.on.action("foo"), State())
assert out.value.message == "failed becozz"
assert len(ctx.action_history) == 1
task = ctx.action_history[0]
task = ctx.action_output
assert task.results == {"my-res": res_value}
assert task.logs == ["log1", "log2"]
assert task.failure_message == "failed becozz"
Expand All @@ -114,8 +112,8 @@ def _on_foo_action(self, event):
with pytest.raises(ActionFailed) as exc_info:
ctx.run(ctx.on.action("foo"), State())
assert exc_info.value.message == "oh no!"
assert ctx.action_history[0].logs == ["starting"]
assert ctx.action_history[0].results == {"initial": "result", "final": "result"}
assert ctx.action_output.logs == ["starting"]
assert ctx.action_output.results == {"initial": "result", "final": "result"}


def _ops_less_than(wanted_major, wanted_minor):
Expand Down

0 comments on commit 7a3449c

Please sign in to comment.