Skip to content

Commit

Permalink
Merge pull request #62 from canonical/version-cli-tool
Browse files Browse the repository at this point in the history
Version cli tool
  • Loading branch information
PietroPasotti authored Sep 19, 2023
2 parents 0cd0c15 + 181f103 commit 22ae93e
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 14 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "[email protected]" }
Expand Down
45 changes: 43 additions & 2 deletions scenario/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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)):
Expand Down
22 changes: 13 additions & 9 deletions scenario/scripts/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -874,15 +874,19 @@ def if_include(key, fn, default):
else:
raise ValueError(f"unknown format {format}")

controller_timestamp = juju_status["controller"]["timestamp"]
local_timestamp = datetime.datetime.now().strftime("%m/%d/%Y, %H:%M:%S")
print(
f"# Generated by scenario.snapshot. \n"
f"# Snapshot of {state_model.name}:{target.unit_name} at {local_timestamp}. \n"
f"# Controller timestamp := {controller_timestamp}. \n"
f"# Juju version := {juju_version} \n"
f"# Charm fingerprint := {charm_version} \n",
)
# json does not support comments, so it would be invalid output.
if format != FormatOption.json:
# print out some metadata
controller_timestamp = juju_status["controller"]["timestamp"]
local_timestamp = datetime.datetime.now().strftime("%m/%d/%Y, %H:%M:%S")
print(
f"# Generated by scenario.snapshot. \n"
f"# Snapshot of {state_model.name}:{target.unit_name} at {local_timestamp}. \n"
f"# Controller timestamp := {controller_timestamp}. \n"
f"# Juju version := {juju_version} \n"
f"# Charm fingerprint := {charm_version} \n",
)

print(txt)

return state
Expand Down
8 changes: 6 additions & 2 deletions scenario/scripts/state_apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,14 +223,18 @@ 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.
Usage: state-apply myapp/0 > ./tests/scenario/case1.py
"""
push_files_ = json.loads(push_files.read_text()) if push_files else None
state_ = json.loads(state.read_text())
state_json = json.loads(state.read_text())

# TODO: state_json to State
raise NotImplementedError("WIP: implement State.from_json")
state_: State = State.from_json(state_json)

return _state_apply(
target=target,
Expand Down

0 comments on commit 22ae93e

Please sign in to comment.