From 181f103379e2b29ec33c40e09a8a9f29cd2681a0 Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Tue, 19 Sep 2023 10:41:10 +0200 Subject: [PATCH] better docstring for Context --- pyproject.toml | 2 +- scenario/context.py | 45 +++++++++++++++++++++++++++++++-- scenario/scripts/state_apply.py | 2 +- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7fa96266..398f63b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta" [project] name = "ops-scenario" -version = "5.2.1" +version = "5.2.2" authors = [ { name = "Pietro Pasotti", email = "pietro.pasotti@canonical.com" } diff --git a/scenario/context.py b/scenario/context.py index 51498b97..ad985974 100644 --- a/scenario/context.py +++ b/scenario/context.py @@ -150,7 +150,49 @@ def __init__( charm_root: "PathLike" = None, juju_version: str = "3.0", ): - """Initializer. + """Represents a simulated charm's execution context. + + It is the main entry point to running a scenario test. + + It contains: the charm source code being executed, the metadata files associated with it, + a charm project repository root, and the juju version to be simulated. + + After you have instantiated Context, typically you will call one of `run()` or + `run_action()` to execute the charm once, write any assertions you like on the output + state returned by the call, write any assertions you like on the Context attributes, + then discard the Context. + Each Context instance is in principle designed to be single-use: + Context is not cleaned up automatically between charm runs. + You can call `.clear()` to do some clean up, but we don't guarantee all state will be gone. + + Any side effects generated by executing the charm, that are not rightful part of the State, + are in fact stored in the Context: + - ``juju_log``: record of what the charm has sent to juju-log + - ``app_status_history``: record of the app statuses the charm has set + - ``unit_status_history``: record of the unit statuses the charm has set + - ``workload_version_history``: record of the workload versions the charm has set + - ``emitted_events``: record of the events (including custom ones) that the charm has + processed + + This allows you to write assertions not only on the output state, but also, to some + extent, on the path the charm took to get there. + + A typical scenario test will look like: + + >>> from scenario import Context, State + >>> from ops import ActiveStatus + >>> from charm import MyCharm, MyCustomEvent + >>> + >>> def test_foo(): + >>> # Arrange: set the context up + >>> c = Context(MyCharm) + >>> # Act: prepare the state and emit an event + >>> state_out = c.run('update-status', State()) + >>> # Assert: verify the output state is what you think it should be + >>> assert state_out.unit_status == ActiveStatus('foobar') + >>> # Assert: verify the Context contains what you think it should + >>> assert len(c.emitted_events) == 4 + >>> assert isinstance(c.emitted_events[3], MyCustomEvent) :arg charm_type: the CharmBase subclass to call ``ops.main()`` on. :arg meta: charm metadata to use. Needs to be a valid metadata.yaml format (as a dict). @@ -171,7 +213,6 @@ def __init__( >>> (local_path / 'foo').mkdir() >>> (local_path / 'foo' / 'bar.yaml').write_text('foo: bar') >>> scenario.Context(... charm_root=virtual_root).run(...) - """ if not any((meta, actions, config)): diff --git a/scenario/scripts/state_apply.py b/scenario/scripts/state_apply.py index 8af5e905..f864b141 100644 --- a/scenario/scripts/state_apply.py +++ b/scenario/scripts/state_apply.py @@ -223,7 +223,7 @@ def state_apply( "of k8s charms, this might mean files obtained through Mounts,", ), ): - """Gather and output the State of a remote target unit. + """Apply a State to a remote target unit. If black is available, the output will be piped through it for formatting.