Skip to content

Commit

Permalink
Add comment, per review.
Browse files Browse the repository at this point in the history
  • Loading branch information
tonyandrewmeyer committed Dec 11, 2024
1 parent 20786fa commit 8220ff9
Show file tree
Hide file tree
Showing 20 changed files with 7,319 additions and 0 deletions.
Binary file added .coverage.data
Binary file not shown.
3 changes: 3 additions & 0 deletions ops/framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,9 @@ def _event_is_in_storage(
if (
existing_observer_path != observer_path
or existing_method_name != method_name
# The notices all have paths that include [id] at the end. If one
# was somehow missing, then the split would be the empty string and
# match anyway.
or existing_event_path.split('[')[0] != event_path.split('[')[0]
):
continue
Expand Down
150 changes: 150 additions & 0 deletions testing/build/lib/scenario/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

"""Charm state-transition testing SDK for Ops charms.
Write tests that declaratively define the Juju state all at once, define the
Juju context against which to test the charm, and fire a single event on the
charm to execute its logic. The tests can then assert that the Juju state has
changed as expected.
These tests are 'state-transition' tests, a way to test isolated units of charm
functionality (how the state changes in reaction to events). They are not
necessarily tests of individual methods or functions (but might be, depending on
the charm's event observers); they are testing the 'contract' of the charm: given
a certain state, when a certain event happens, the charm should transition to a
certain (likely different) state. They do not test against a real Juju
controller and model, and focus on a single Juju unit, unlike integration tests.
For simplicity, we refer to them as 'unit' tests in the charm context.
Writing these tests should nudge you into thinking of a charm as a black-box
input->output function. The input is the union of an `Event` (why am I, charm,
being executed), a `State` (am I leader? what is my integration data? what is my
config?...) and the charm's execution `Context` (what integrations can I have?
what containers can I have?...). The output is another `State`: the state after
the charm has had a chance to interact with the mocked Juju model and affect the
state.
.. image:: https://raw.githubusercontent.com/canonical/ops-scenario/main/resources/state-transition-model.png
:alt: Transition diagram, with the input state and event on the left, the context including the charm in the centre, and the state out on the right
Writing unit tests for a charm, then, means verifying that:
- the charm does not raise uncaught exceptions while handling the event
- the output state (as compared with the input state) is as expected.
A test consists of three broad steps:
- **Arrange**:
- declare the context
- declare the input state
- **Act**:
- select an event to fire
- run the context (i.e. obtain the output state, given the input state and the event)
- **Assert**:
- verify that the output state (as compared with the input state) is how you expect it to be
- verify that the charm has seen a certain sequence of statuses, events, and `juju-log` calls
- optionally, you can use a context manager to get a hold of the charm instance and run
assertions on APIs and state internal to it.
The most basic scenario is one in which all is defaulted and barely any data is
available. The charm has no config, no integrations, no leadership, and its
status is `unknown`. With that, we can write the simplest possible test:
.. code-block:: python
def test_base():
ctx = Context(MyCharm)
out = ctx.run(ctx.on.start(), State())
assert out.unit_status == UnknownStatus()
"""

from ops._private.harness import ActionFailed # For backwards compatibility.

from .context import CharmEvents, Context, Manager
from .errors import StateValidationError # For backwards compatibility.
from .state import (
ActiveStatus,
Address,
AnyJson,
BindAddress,
BlockedStatus,
CharmType,
CheckInfo,
CloudCredential,
CloudSpec,
Container,
DeferredEvent,
ErrorStatus,
Exec,
ICMPPort,
JujuLogLine,
MaintenanceStatus,
Model,
Mount,
Network,
Notice,
PeerRelation,
Port,
RawDataBagContents,
RawSecretRevisionContents,
Relation,
RelationBase,
Resource,
Secret,
State,
Storage,
StoredState,
SubordinateRelation,
TCPPort,
UDPPort,
UnitID,
UnknownStatus,
WaitingStatus,
)

__all__ = [
"ActionFailed",
"ActiveStatus",
"Address",
"AnyJson",
"BindAddress",
"BlockedStatus",
"CharmEvents",
"CharmType",
"CheckInfo",
"CloudCredential",
"CloudSpec",
"Container",
"Context",
"DeferredEvent",
"ErrorStatus",
"Exec",
"ICMPPort",
"JujuLogLine",
"MaintenanceStatus",
"Manager",
"Model",
"Mount",
"Network",
"Notice",
"PeerRelation",
"Port",
"RawDataBagContents",
"RawSecretRevisionContents",
"Relation",
"RelationBase",
"Resource",
"Secret",
"State",
"StateValidationError",
"Storage",
"StoredState",
"SubordinateRelation",
"TCPPort",
"UDPPort",
"UnitID",
"UnknownStatus",
"WaitingStatus",
]
Loading

0 comments on commit 8220ff9

Please sign in to comment.