From 7a3449cce5425f260a0c6bb64a0cc0929fb2df83 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Tue, 6 Aug 2024 19:11:10 +1200 Subject: [PATCH] Simplify the task interface and rename back to ActionOutput. --- README.md | 10 +++++----- scenario/__init__.py | 4 ++-- scenario/context.py | 11 ++++++----- scenario/mocking.py | 11 +++++++---- scenario/state.py | 2 +- tests/test_context.py | 15 +++++++-------- tests/test_e2e/test_actions.py | 12 +++++------- 7 files changed, 33 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 1ba0300e..c6aac364 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/scenario/__init__.py b/scenario/__init__.py index 0fcac537..6023ed57 100644 --- a/scenario/__init__.py +++ b/scenario/__init__.py @@ -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, @@ -39,7 +39,7 @@ ) __all__ = [ - "Task", + "ActionOutput", "ActionFailed", "CheckInfo", "CloudCredential", diff --git a/scenario/context.py b/scenario/context.py index 7f1515f9..bd6628bc 100644 --- a/scenario/context.py +++ b/scenario/context.py @@ -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. """ @@ -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() @@ -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") diff --git a/scenario/mocking.py b/scenario/mocking.py index 38c32dbb..8d400c78 100644 --- a/scenario/mocking.py +++ b/scenario/mocking.py @@ -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 diff --git a/scenario/state.py b/scenario/state.py index 64cb79e5..5d18842d 100644 --- a/scenario/state.py +++ b/scenario/state.py @@ -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 diff --git a/tests/test_context.py b/tests/test_context.py index a47b6d23..62d3e854 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -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 @@ -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(): @@ -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 == "" diff --git a/tests/test_e2e/test_actions.py b/tests/test_e2e/test_actions.py index 780e616c..a6c335bc 100644 --- a/tests/test_e2e/test_actions.py +++ b/tests/test_e2e/test_actions.py @@ -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"})) @@ -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" @@ -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):