From 84b2830a9ad5e948f4297cd3b77fba77b379bd6b Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Fri, 6 Dec 2024 12:41:30 +1300 Subject: [PATCH] Add comment, per review. --- .coverage.data | Bin 0 -> 401408 bytes ops/framework.py | 3 + testing/build/lib/scenario/__init__.py | 150 ++ .../lib/scenario/_consistency_checker.py | 720 ++++++ testing/build/lib/scenario/_ops_main_mock.py | 197 ++ testing/build/lib/scenario/_runtime.py | 438 ++++ testing/build/lib/scenario/context.py | 692 ++++++ testing/build/lib/scenario/errors.py | 56 + testing/build/lib/scenario/logger.py | 11 + testing/build/lib/scenario/mocking.py | 911 ++++++++ testing/build/lib/scenario/ops_main_mock.py | 270 +++ testing/build/lib/scenario/py.typed | 0 testing/build/lib/scenario/runtime.py | 577 +++++ testing/build/lib/scenario/state.py | 2030 +++++++++++++++++ .../dist/ops_scenario-7.0.5-py3-none-any.whl | Bin 0 -> 70372 bytes testing/src/ops_scenario.egg-info/PKG-INFO | 1208 ++++++++++ testing/src/ops_scenario.egg-info/SOURCES.txt | 52 + .../dependency_links.txt | 1 + .../src/ops_scenario.egg-info/requires.txt | 2 + .../src/ops_scenario.egg-info/top_level.txt | 1 + 20 files changed, 7319 insertions(+) create mode 100644 .coverage.data create mode 100644 testing/build/lib/scenario/__init__.py create mode 100644 testing/build/lib/scenario/_consistency_checker.py create mode 100644 testing/build/lib/scenario/_ops_main_mock.py create mode 100644 testing/build/lib/scenario/_runtime.py create mode 100644 testing/build/lib/scenario/context.py create mode 100644 testing/build/lib/scenario/errors.py create mode 100644 testing/build/lib/scenario/logger.py create mode 100644 testing/build/lib/scenario/mocking.py create mode 100644 testing/build/lib/scenario/ops_main_mock.py create mode 100644 testing/build/lib/scenario/py.typed create mode 100644 testing/build/lib/scenario/runtime.py create mode 100644 testing/build/lib/scenario/state.py create mode 100644 testing/dist/ops_scenario-7.0.5-py3-none-any.whl create mode 100644 testing/src/ops_scenario.egg-info/PKG-INFO create mode 100644 testing/src/ops_scenario.egg-info/SOURCES.txt create mode 100644 testing/src/ops_scenario.egg-info/dependency_links.txt create mode 100644 testing/src/ops_scenario.egg-info/requires.txt create mode 100644 testing/src/ops_scenario.egg-info/top_level.txt diff --git a/.coverage.data b/.coverage.data new file mode 100644 index 0000000000000000000000000000000000000000..2079bf72e58e9f6aae46c211c9b5e630ffe9280f GIT binary patch literal 401408 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

PJTXGZ??Ig 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� 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 Sl@S1Lc~FhlMlhh`@qYoot~=BK literal 0 HcmV?d00001 diff --git a/ops/framework.py b/ops/framework.py index f05a20aba..f90037bbd 100644 --- a/ops/framework.py +++ b/ops/framework.py @@ -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 diff --git a/testing/build/lib/scenario/__init__.py b/testing/build/lib/scenario/__init__.py new file mode 100644 index 000000000..2cf042cc6 --- /dev/null +++ b/testing/build/lib/scenario/__init__.py @@ -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", +] diff --git a/testing/build/lib/scenario/_consistency_checker.py b/testing/build/lib/scenario/_consistency_checker.py new file mode 100644 index 000000000..1d21a60ac --- /dev/null +++ b/testing/build/lib/scenario/_consistency_checker.py @@ -0,0 +1,720 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +""" +The :meth:`check_consistency` function is the primary entry point for the +consistency checks. Calling it ensures that the :class:`State` and the event, +in combination with the ``Context``, is viable in Juju. For example, Juju can't +emit a ``foo-relation-changed`` event on your charm unless your charm has +declared a ``foo`` relation endpoint in its metadata. + +Normally, there is no need to explicitly call this function; that happens +automatically behind the scenes whenever you trigger an event. + +If you have a clear false negative, are explicitly testing 'edge', +inconsistent situations, or for whatever reason the checker is in your way, you +can set the ``SCENARIO_SKIP_CONSISTENCY_CHECKS`` environment variable and skip +it altogether. +""" + +import marshal +import os +import re +from collections import Counter, defaultdict +from collections.abc import Sequence +from numbers import Number +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterable, + List, + NamedTuple, + Optional, + Set, + Tuple, + Union, +) + +from .errors import InconsistentScenarioError +from .runtime import logger as scenario_logger +from .state import ( + CharmType, + PeerRelation, + SubordinateRelation, + _Action, + _CharmSpec, + _normalise_name, +) + +if TYPE_CHECKING: # pragma: no cover + from .state import State, _Event + +logger = scenario_logger.getChild("consistency_checker") + + +class Results(NamedTuple): + """Consistency checker return type. + + Each consistency check function returns a ``Results`` instance with the + warnings and errors found during the check. + """ + + errors: Iterable[str] + warnings: Iterable[str] + + +def check_consistency( + state: "State", + event: "_Event", + charm_spec: "_CharmSpec[Any]", + juju_version: str, +): + """Validate the combination of a state, an event, a charm spec, and a Juju version. + + When invoked, it performs a series of checks that validate that the state is + consistent with itself, with the event being emitted, the charm metadata, + and so on. + + This function performs some basic validation of the combination of inputs + that goes into a test and determines if the scenario is a + realistic/plausible/consistent one. + + A scenario is inconsistent if it can practically never occur because it + contradicts the Juju model. For example: Juju guarantees that upon calling + ``config-get``, a charm will only ever get the keys it declared in its + config metadata, so a :class:`scenario.State` declaring some config keys + that are not in the charm's ``charmcraft.yaml`` is nonsense, and the + combination of the two is inconsistent. + """ + juju_version_: Tuple[int, ...] = tuple(map(int, juju_version.split("."))) + + if os.getenv("SCENARIO_SKIP_CONSISTENCY_CHECKS"): + logger.info("skipping consistency checks.") + return + + errors: List[str] = [] + warnings: List[str] = [] + + checks: Tuple[Callable[..., Results]] = ( + check_containers_consistency, + check_config_consistency, + check_resource_consistency, + check_event_consistency, + check_secrets_consistency, + check_storages_consistency, + check_relation_consistency, + check_network_consistency, + check_cloudspec_consistency, + check_storedstate_consistency, + ) # type: ignore + for check in checks: + results = check( + state=state, + event=event, + charm_spec=charm_spec, + juju_version=juju_version_, + ) + errors.extend(results.errors) + warnings.extend(results.warnings) + + if errors: + err_fmt = "\n".join(errors) + raise InconsistentScenarioError( + f"Inconsistent scenario. The following errors were found: {err_fmt}", + ) + if warnings: + err_fmt = "\n".join(warnings) + logger.warning( + f"This scenario is probably inconsistent. Double check, and ignore this " + f"warning if you're sure. " + f"The following warnings were found: {err_fmt}", + ) + + +def check_resource_consistency( + *, + state: "State", + charm_spec: "_CharmSpec[CharmType]", + **_kwargs: Any, # noqa: U101 +) -> Results: + """Check the internal consistency of the resources from metadata and in :class:`scenario.State`.""" + errors: List[str] = [] + warnings: List[str] = [] + + resources_from_meta = set(charm_spec.meta.get("resources", {})) + resources_from_state = {resource.name for resource in state.resources} + if not resources_from_meta.issuperset(resources_from_state): + errors.append( + f"any and all resources passed to State.resources need to have been defined in " + f"metadata.yaml. Metadata resources: {resources_from_meta}; " + f"State.resources: {resources_from_state}.", + ) + return Results(errors, warnings) + + +def check_event_consistency( + *, + event: "_Event", + charm_spec: "_CharmSpec[CharmType]", + state: "State", + **_kwargs: Any, # noqa: U101 +) -> Results: + """Check the internal consistency of the ``_Event`` data structure. + + For example, it checks that a relation event has a relation instance, and that + the relation endpoint name matches the event prefix. + """ + errors: List[str] = [] + warnings: List[str] = [] + + if not event._is_builtin_event(charm_spec): + # This is a custom event - we can't make assumptions about its name and + # semantics. It doesn't really make sense to do checks that are designed + # for relations, workloads, and so on - most likely those will end up + # with false positives. Realistically, we can't know about what the + # requirements for the custom event are (in terms of the state), so we + # skip everything here. Perhaps in the future, custom events could + # optionally include some sort of state metadata that made testing + # consistency possible? + return Results(errors, warnings) + + if event._is_relation_event: + _check_relation_event(charm_spec, event, state, errors, warnings) + + if event._is_workload_event: + _check_workload_event(charm_spec, event, state, errors, warnings) + + if event._is_action_event: + _check_action_event(charm_spec, event, state, errors, warnings) + + if event._is_storage_event: + _check_storage_event(charm_spec, event, state, errors, warnings) + + return Results(errors, warnings) + + +def _check_relation_event( + charm_spec: _CharmSpec[CharmType], # noqa: U100 + event: "_Event", + state: "State", + errors: List[str], + warnings: List[str], # noqa: U100 +): + if not event.relation: + errors.append( + "cannot construct a relation event without the relation instance. " + "Please pass one.", + ) + else: + if not event.name.startswith(_normalise_name(event.relation.endpoint)): + errors.append( + f"relation event should start with relation endpoint name. {event.name} does " + f"not start with {event.relation.endpoint}.", + ) + if event.relation not in state.relations: + errors.append( + f"cannot emit {event.name} because relation {event.relation.id} is not in the " + f"state (a relation with the same ID is not sufficient - you must " + f"pass the object in the state to the event).", + ) + + +def _check_workload_event( + charm_spec: _CharmSpec[CharmType], # noqa: U100 + event: "_Event", + state: "State", + errors: List[str], + warnings: List[str], +): + if not event.container: + errors.append( + "cannot construct a workload event without the container instance. " + "Please pass one.", + ) + else: + if not event.name.startswith(_normalise_name(event.container.name)): + errors.append( + f"workload event should start with container name. {event.name} does " + f"not start with {event.container.name}.", + ) + if event.container not in state.containers: + errors.append( + f"cannot emit {event.name} because container {event.container.name} " + f"is not in the state (a container with the same name is not " + f"sufficient - you must pass the object in the state to the event).", + ) + if not event.container.can_connect: + warnings.append( + "you **can** fire fire pebble-ready while the container cannot connect, " + "but that's most likely not what you want.", + ) + names = Counter(exec.command_prefix for exec in event.container.execs) + if dupes := [n for n in names if names[n] > 1]: + errors.append( + f"container {event.container.name} has duplicate command prefixes: {dupes}", + ) + + +def _check_action_event( + charm_spec: _CharmSpec[CharmType], + event: "_Event", + state: "State", # noqa: U100 + errors: List[str], + warnings: List[str], +): + action = event.action + if not action: + errors.append( + "cannot construct a workload event without the container instance. " + "Please pass one.", + ) + return + + elif not event.name.startswith(_normalise_name(action.name)): + errors.append( + f"action event should start with action name. {event.name} does " + f"not start with {action.name}.", + ) + if action.name not in (charm_spec.actions or ()): + errors.append( + f"action event {event.name} refers to action {action.name} " + f"which is not declared in the charm metadata (actions.yaml).", + ) + return + + _check_action_param_types(charm_spec, action, errors, warnings) + + +def _check_storage_event( + charm_spec: _CharmSpec[CharmType], + event: "_Event", + state: "State", + errors: List[str], + warnings: List[str], # noqa: U100 +): + storage = event.storage + meta = charm_spec.meta + + if not storage: + errors.append( + "cannot construct a storage event without the Storage instance. " + "Please pass one.", + ) + elif not event.name.startswith(_normalise_name(storage.name)): + errors.append( + f"storage event should start with storage name. {event.name} does " + f"not start with {storage.name}.", + ) + elif storage.name not in meta["storage"]: + errors.append( + f"storage event {event.name} refers to storage {storage.name} " + f"which is not declared in the charm metadata (metadata.yaml) under 'storage'.", + ) + elif storage not in state.storages: + errors.append( + f"cannot emit {event.name} because storage {storage.name} " + f"is not in the state (an object with the same name and index is not " + f"sufficient - you must pass the object in the state to the event).", + ) + + +def _check_action_param_types( + charm_spec: _CharmSpec[CharmType], + action: _Action, + errors: List[str], + warnings: List[str], +): + actions = charm_spec.actions + if not actions: + return + + to_python_type = { + "string": str, + "boolean": bool, + "integer": int, + "number": Number, + "array": Sequence, + "object": dict, + } + expected_param_type: Dict[str, Any] = {} + for par_name, par_spec in actions[action.name].get("params", {}).items(): + value = par_spec.get("type") + if not value: + errors.append( + f"action parameter {par_name} has no type. " + f"Charmcraft will be unhappy about this. ", + ) + continue + + try: + expected_param_type[par_name] = to_python_type[value] + except KeyError: + warnings.append( + f"unknown data type declared for parameter {par_name}: type={value}. " + f"Cannot consistency-check.", + ) + + for provided_param_name, provided_param_value in action.params.items(): + expected_type = expected_param_type.get(provided_param_name) + if not expected_type: + errors.append( + f"param {provided_param_name} is not a valid parameter for {action.name}: " + "missing from action specification", + ) + continue + if not isinstance(provided_param_value, expected_type): + errors.append( + f"param {provided_param_name} is of type {type(provided_param_value)}: " + f"expecting {expected_type}", + ) + + +def check_storages_consistency( + *, + state: "State", + charm_spec: "_CharmSpec[CharmType]", + **_kwargs: Any, # noqa: U101 +) -> Results: + """Check the consistency of the :class:`scenario.State` storages with the charm_spec metadata.""" + state_storage = state.storages + meta_storage = (charm_spec.meta or {}).get("storage", {}) + errors: List[str] = [] + + if missing := {s.name for s in state_storage}.difference( + set(meta_storage.keys()), + ): + errors.append( + f"some storages passed to State were not defined in metadata.yaml: {missing}", + ) + + seen: List[Tuple[str, int]] = [] + for s in state_storage: + tag = (s.name, s.index) + if tag in seen: + errors.append( + f"duplicate storage in State: storage {s.name} with index {s.index} " + f"occurs multiple times in State.storages.", + ) + seen.append(tag) + + return Results(errors, []) + + +def _is_secret_identifier(value: Union[str, int, float, bool]) -> bool: + """Return true iff the value is in the form `secret:{secret id}`.""" + # cf. https://github.com/juju/juju/blob/13eb9df3df16a84fd471af8a3c95ddbd04389b71/core/secrets/secret.go#L48 + return bool(re.match(r"secret:[0-9a-z]{20}$", str(value))) + + +def check_config_consistency( + *, + state: "State", + charm_spec: "_CharmSpec[CharmType]", + juju_version: Tuple[int, ...], + **_kwargs: Any, # noqa: U101 +) -> Results: + """Check the consistency of the :class:`scenario.State` config with the charm_spec config.""" + state_config = state.config + meta_config = (charm_spec.config or {}).get("options", {}) + errors: List[str] = [] + + for key, value in state_config.items(): + if key not in meta_config: + errors.append( + f"config option {key!r} in state.config but not specified in config.yaml or " + f"charmcraft.yaml.", + ) + continue + + converters = { + "string": str, + "int": int, + "float": float, + "boolean": bool, + } + if juju_version >= (3, 4): + converters["secret"] = str + + validators = { + "secret": _is_secret_identifier, + } + + expected_type_name = meta_config[key].get("type", None) + if not expected_type_name: + errors.append(f"config.yaml invalid; option {key!r} has no 'type'.") + continue + validator = validators.get(expected_type_name) + + expected_type = converters.get(expected_type_name) + if not expected_type: + errors.append( + f"config invalid for option {key!r}: 'type' {expected_type_name} unknown", + ) + + elif not isinstance(value, expected_type): + errors.append( + f"config invalid; option {key!r} should be of type {expected_type} " + f"but is of type {type(value)}.", + ) + + elif validator and not validator(value): + errors.append( + f"config invalid: option {key!r} value {value!r} is not valid.", + ) + + return Results(errors, []) + + +def check_secrets_consistency( + *, + event: "_Event", + state: "State", + juju_version: Tuple[int, ...], + **_kwargs: Any, # noqa: U101 +) -> Results: + """Check the consistency of any :class:`scenario.Secret` in the :class:`scenario.State`.""" + errors: List[str] = [] + if not event._is_secret_event: + return Results(errors, []) + + assert event.secret is not None + if event.secret not in state.secrets: + secret_key = event.secret.id if event.secret.id else event.secret.label + errors.append( + f"cannot emit {event.name} because secret {secret_key} is not in the state " + f"(a secret with the same ID is not sufficient - you must pass the object " + f"in the state to the event).", + ) + elif juju_version < (3,): + errors.append( + f"secrets are not supported in the specified juju version {juju_version}. " + f"Should be at least 3.0.", + ) + + return Results(errors, []) + + +def check_network_consistency( + *, + state: "State", + event: "_Event", # noqa: U100 + charm_spec: "_CharmSpec[CharmType]", + **_kwargs: Any, # noqa: U101 +) -> Results: + """Check the consistency of any :class:`scenario.Network` in the :class:`scenario.State`.""" + errors: List[str] = [] + + meta_bindings = set(charm_spec.meta.get("extra-bindings", ())) + # add the implicit juju-info binding so we can override its network without + # having to declare a relation for it in metadata + implicit_bindings = {"juju-info"} + all_relations = charm_spec.get_all_relations() + non_sub_relations = { + endpoint + for endpoint, metadata in all_relations + if metadata.get("scope") != "container" # mark of a sub + } + + state_bindings = {network.binding_name for network in state.networks} + if diff := state_bindings.difference( + meta_bindings.union(non_sub_relations).union(implicit_bindings), + ): + errors.append( + f"Some network bindings defined in State are not in the metadata: {diff}.", + ) + + endpoints = {endpoint for endpoint, _ in all_relations} + if collisions := endpoints.intersection(meta_bindings): + errors.append( + f"Extra bindings and integration endpoints cannot share the same name: {collisions}.", + ) + + return Results(errors, []) + + +def check_relation_consistency( + *, + state: "State", + event: "_Event", # noqa: U100 + charm_spec: "_CharmSpec[CharmType]", + **_kwargs: Any, # noqa: U101 +) -> Results: + """Check the consistency of any relations in the :class:`scenario.State`.""" + errors: List[str] = [] + + peer_relations_meta = charm_spec.meta.get("peers", {}).items() + all_relations_meta = charm_spec.get_all_relations() + + def _get_relations(r: str): + try: + return state.get_relations(r) + except ValueError: + return () + + # check relation types + for endpoint, _ in peer_relations_meta: + for relation in _get_relations(endpoint): + if not isinstance(relation, PeerRelation): + errors.append( + f"endpoint {endpoint} is a peer relation; " + f"expecting relation to be of type PeerRelation, got {type(relation)}", + ) + + known_endpoints = [a[0] for a in all_relations_meta] + for relation in state.relations: + if (ep := relation.endpoint) not in known_endpoints: + errors.append(f"relation endpoint {ep} is not declared in metadata.") + + seen_ids: Set[int] = set() + for endpoint, relation_meta in all_relations_meta: + expected_sub = relation_meta.get("scope", "") == "container" + relations = _get_relations(endpoint) + for relation in relations: + if relation.id in seen_ids: + errors.append( + f"duplicate relation ID: {relation.id} is claimed " + f"by multiple Relation instances", + ) + + seen_ids.add(relation.id) + is_sub = isinstance(relation, SubordinateRelation) + if is_sub and not expected_sub: + errors.append( + f"endpoint {endpoint} is not a subordinate relation; " + f"expecting relation to be of type Relation, " + f"got {type(relation)}", + ) + if expected_sub and not is_sub: + errors.append( + f"endpoint {endpoint} is not a subordinate relation; " + f"expecting relation to be of type SubordinateRelation, " + f"got {type(relation)}", + ) + + # check for duplicate endpoint names + seen_endpoints: Set[str] = set() + for endpoint, _ in all_relations_meta: + if endpoint in seen_endpoints: + errors.append("duplicate endpoint name in metadata.") + break + seen_endpoints.add(endpoint) + + return Results(errors, []) + + +def check_containers_consistency( + *, + state: "State", + event: "_Event", + charm_spec: "_CharmSpec[CharmType]", + **_kwargs: Any, # noqa: U101 +) -> Results: + """Check the consistency of :class:`scenario.State` containers with the charm_spec metadata.""" + + # event names will be normalized; need to compare against normalized container names. + meta = charm_spec.meta + meta_containers = list(map(_normalise_name, meta.get("containers", {}))) + state_containers = [_normalise_name(c.name) for c in state.containers] + all_notices = {notice.id for c in state.containers for notice in c.notices} + all_checks = { + (c.name, check.name) for c in state.containers for check in c.check_infos + } + errors: List[str] = [] + + # it's fine if you have containers in meta that are not in state.containers (yet), but it's + # not fine if: + # - you're processing a Pebble event and that container is not in state.containers or + # meta.containers + if event._is_workload_event: + evt_container_name = event.name.split("_pebble_")[0] + if evt_container_name not in meta_containers: + errors.append( + f"the event being processed concerns container {evt_container_name!r}, but a " + f"container with that name is not declared in the charm metadata", + ) + if evt_container_name not in state_containers: + errors.append( + f"the event being processed concerns container {evt_container_name!r}, but a " + f"container with that name is not present in the state. It's odd, but " + f"consistent, if it cannot connect; but it should at least be there.", + ) + # - you're processing a Notice event and that notice is not in any of the containers + if event.notice and event.notice.id not in all_notices: + errors.append( + f"the event being processed concerns notice {event.notice!r}, but that " + "notice is not in any of the containers present in the state.", + ) + # - you're processing a Check event and that check is not in the check's container + if ( + event.check_info + and (evt_container_name, event.check_info.name) not in all_checks + ): + errors.append( + f"the event being processed concerns check {event.check_info.name}, but that " + f"check is not in the {evt_container_name} container.", + ) + + # - a container in state.containers is not in meta.containers + if diff := (set(state_containers).difference(set(meta_containers))): + errors.append( + f"some containers declared in the state are not specified in metadata. " + f"That's not possible. " + f"Missing from metadata: {diff}.", + ) + + return Results(errors, []) + + +def check_cloudspec_consistency( + *, + state: "State", + event: "_Event", + charm_spec: "_CharmSpec[CharmType]", + **_kwargs: Any, # noqa: U101 +) -> Results: + """Check that Kubernetes models don't have :attr:`scenario.State.cloud_spec` set.""" + + errors: List[str] = [] + warnings: List[str] = [] + + if state.model.type == "kubernetes" and state.model.cloud_spec: + errors.append( + "CloudSpec is only available for machine charms, not Kubernetes charms. " + "Simulate a machine substrate with: `State(..., model=Model(type='lxd'))`.", + ) + + return Results(errors, warnings) + + +def check_storedstate_consistency( + *, + state: "State", + **_kwargs: Any, # noqa: U101 +) -> Results: + """Check the internal consistency of any :class:`scenario.StoredState` in the :class:`scenario.State`.""" + errors: List[str] = [] + + # Attribute names must be unique on each object. + names: defaultdict[Optional[str], List[str]] = defaultdict(list) + for ss in state.stored_states: + names[ss.owner_path].append(ss.name) + for owner, owner_names in names.items(): + if len(owner_names) != len(set(owner_names)): + errors.append( + f"{owner} has multiple StoredState objects with the same name.", + ) + + # The content must be marshallable. + for ss in state.stored_states: + # We don't need the marshalled state, just to know that it can be. + # This is the same "only simple types" check that ops does. + try: + marshal.dumps(ss.content) + except ValueError: + errors.append( + f"The StoredState object {ss.owner_path}.{ss.name} should contain only simple types.", + ) + return Results(errors, []) diff --git a/testing/build/lib/scenario/_ops_main_mock.py b/testing/build/lib/scenario/_ops_main_mock.py new file mode 100644 index 000000000..87f2ec1ab --- /dev/null +++ b/testing/build/lib/scenario/_ops_main_mock.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +import dataclasses +import marshal +import re +import sys +from typing import TYPE_CHECKING, Any, Dict, FrozenSet, List, Sequence, Set + +import ops +import ops.jujucontext +import ops.storage + +from ops.framework import _event_regex +from ops._main import _Dispatcher, _Manager +from ops._main import logger as ops_logger + +from .errors import BadOwnerPath, NoObserverError +from .logger import logger as scenario_logger +from .mocking import _MockModelBackend +from .state import DeferredEvent, StoredState + +if TYPE_CHECKING: # pragma: no cover + from .context import Context + from .state import CharmType, State, _CharmSpec, _Event + +EVENT_REGEX = re.compile(_event_regex) +STORED_STATE_REGEX = re.compile( + r"((?P<owner_path>.*)\/)?(?P<_data_type_name>\D+)\[(?P<name>.*)\]", +) + +logger = scenario_logger.getChild("ops_main_mock") + +# pyright: reportPrivateUsage=false + + +class UnitStateDB: + """Wraps the unit-state database with convenience methods for adjusting the state.""" + + def __init__(self, underlying_store: ops.storage.SQLiteStorage): + self._db = underlying_store + + def get_stored_states(self) -> FrozenSet["StoredState"]: + """Load any StoredState data structures from the db.""" + db = self._db + stored_states: Set[StoredState] = set() + for handle_path in db.list_snapshots(): + if not EVENT_REGEX.match(handle_path) and ( + match := STORED_STATE_REGEX.match(handle_path) + ): + stored_state_snapshot = db.load_snapshot(handle_path) + kwargs = match.groupdict() + sst = StoredState(content=stored_state_snapshot, **kwargs) + stored_states.add(sst) + + return frozenset(stored_states) + + def get_deferred_events(self) -> List["DeferredEvent"]: + """Load any DeferredEvent data structures from the db.""" + db = self._db + deferred: List[DeferredEvent] = [] + for handle_path in db.list_snapshots(): + if EVENT_REGEX.match(handle_path): + notices = db.notices(handle_path) + for handle, owner, observer in notices: + try: + snapshot_data = db.load_snapshot(handle) + except ops.storage.NoSnapshotError: + snapshot_data: Dict[str, Any] = {} + + event = DeferredEvent( + handle_path=handle, + owner=owner, + observer=observer, + snapshot_data=snapshot_data, + ) + deferred.append(event) + + return deferred + + def apply_state(self, state: "State"): + """Add DeferredEvent and StoredState from this State instance to the storage.""" + db = self._db + for event in state.deferred: + db.save_notice(event.handle_path, event.owner, event.observer) + try: + marshal.dumps(event.snapshot_data) + except ValueError as e: + raise ValueError( + f"unable to save the data for {event}, it must contain only simple types.", + ) from e + db.save_snapshot(event.handle_path, event.snapshot_data) + + for stored_state in state.stored_states: + db.save_snapshot(stored_state._handle_path, stored_state.content) + + +class Ops(_Manager): + """Class to manage stepping through ops setup, event emission and framework commit.""" + + def __init__( + self, + state: "State", + event: "_Event", + context: "Context", + charm_spec: "_CharmSpec[CharmType]", + juju_context: ops.jujucontext._JujuContext, + ): + self.state = state + self.event = event + self.context = context + self.charm_spec = charm_spec + self.store = None + + model_backend = _MockModelBackend( + state=state, + event=event, + context=context, + charm_spec=charm_spec, + juju_context=juju_context, + ) + + super().__init__( + self.charm_spec.charm_type, model_backend, juju_context=juju_context + ) + + def _load_charm_meta(self): + metadata = (self._charm_root / "metadata.yaml").read_text() + actions_meta = self._charm_root / "actions.yaml" + if actions_meta.exists(): + actions_metadata = actions_meta.read_text() + else: + actions_metadata = None + + return ops.CharmMeta.from_yaml(metadata, actions_metadata) + + def _setup_root_logging(self): + # Ops sets sys.excepthook to go to Juju's debug-log, but that's not + # useful in a testing context, so we reset it here. + super()._setup_root_logging() + sys.excepthook = sys.__excepthook__ + + def _make_storage(self, _: _Dispatcher): + # TODO: add use_juju_for_storage support + # TODO: Pass a charm_state_path that is ':memory:' when appropriate. + charm_state_path = self._charm_root / self._charm_state_path + storage = ops.storage.SQLiteStorage(charm_state_path) + logger.info("Copying input state to storage.") + self.store = UnitStateDB(storage) + self.store.apply_state(self.state) + return storage + + def _get_event_to_emit(self, event_name: str): + owner = ( + self._get_owner(self.charm, self.event.owner_path) + if self.event + else self.charm.on + ) + + try: + event_to_emit = getattr(owner, event_name) + except AttributeError: + ops_logger.debug("Event %s not defined for %s.", event_name, self.charm) + raise NoObserverError( + f"Cannot fire {event_name!r} on {owner}: " + f"invalid event (not on charm.on).", + ) + return event_to_emit + + @staticmethod + def _get_owner(root: Any, path: Sequence[str]) -> ops.ObjectEvents: + """Walk path on root to an ObjectEvents instance.""" + obj = root + for step in path: + try: + obj = getattr(obj, step) + except AttributeError: + raise BadOwnerPath( + f"event_owner_path {path!r} invalid: {step!r} leads to nowhere.", + ) + if not isinstance(obj, ops.ObjectEvents): + raise BadOwnerPath( + f"event_owner_path {path!r} invalid: does not lead to " + f"an ObjectEvents instance.", + ) + return obj + + def _close(self): + """Now that we're done processing this event, read the charm state and expose it.""" + logger.info("Copying storage to output state.") + assert self.store is not None + deferred = self.store.get_deferred_events() + stored_state = self.store.get_stored_states() + self.state = dataclasses.replace( + self.state, deferred=deferred, stored_states=stored_state + ) diff --git a/testing/build/lib/scenario/_runtime.py b/testing/build/lib/scenario/_runtime.py new file mode 100644 index 000000000..889a2d96a --- /dev/null +++ b/testing/build/lib/scenario/_runtime.py @@ -0,0 +1,438 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +import copy +import dataclasses +import tempfile +import typing +from contextlib import contextmanager +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Dict, + List, + Optional, + Type, + TypeVar, + Union, +) + +import yaml +from ops import ( + CollectStatusEvent, + pebble, + CommitEvent, + EventBase, + Framework, + Handle, + NoTypeError, + PreCommitEvent, +) +from ops.jujucontext import _JujuContext +from ops._private.harness import ActionFailed + +from .errors import NoObserverError, UncaughtCharmError +from .logger import logger as scenario_logger +from .state import ( + PeerRelation, + Relation, + SubordinateRelation, +) + +if TYPE_CHECKING: # pragma: no cover + from .context import Context + from .state import CharmType, State, _CharmSpec, _Event + +logger = scenario_logger.getChild("runtime") + +RUNTIME_MODULE = Path(__file__).parent + + +class Runtime: + """Charm runtime wrapper. + + This object bridges a local environment and a charm artifact. + """ + + def __init__( + self, + charm_spec: "_CharmSpec[CharmType]", + charm_root: Optional[Union[str, Path]] = None, + juju_version: str = "3.0.0", + app_name: Optional[str] = None, + unit_id: Optional[int] = 0, + ): + self._charm_spec = charm_spec + self._juju_version = juju_version + self._charm_root = charm_root + + app_name = app_name or self._charm_spec.meta.get("name") + if not app_name: + raise ValueError('invalid metadata: mandatory "name" field is missing.') + + self._app_name = app_name + self._unit_id = unit_id + + def _get_event_env(self, state: "State", event: "_Event", charm_root: Path): + """Build the simulated environment the operator framework expects.""" + env = { + "JUJU_VERSION": self._juju_version, + "JUJU_UNIT_NAME": f"{self._app_name}/{self._unit_id}", + "_": "./dispatch", + "JUJU_DISPATCH_PATH": f"hooks/{event.name}", + "JUJU_MODEL_NAME": state.model.name, + "JUJU_MODEL_UUID": state.model.uuid, + "JUJU_CHARM_DIR": str(charm_root.absolute()), + # todo consider setting pwd, (python)path + } + + if event._is_action_event and (action := event.action): + env.update( + { + # TODO: we should check we're doing the right thing here. + "JUJU_ACTION_NAME": action.name.replace("_", "-"), + "JUJU_ACTION_UUID": action.id, + }, + ) + + if event._is_relation_event and (relation := event.relation): + if isinstance(relation, PeerRelation): + remote_app_name = self._app_name + elif isinstance(relation, (Relation, SubordinateRelation)): + remote_app_name = relation.remote_app_name + else: + raise ValueError(f"Unknown relation type: {relation}") + env.update( + { + "JUJU_RELATION": relation.endpoint, + "JUJU_RELATION_ID": str(relation.id), + "JUJU_REMOTE_APP": remote_app_name, + }, + ) + + remote_unit_id = event.relation_remote_unit_id + + # don't check truthiness because remote_unit_id could be 0 + if remote_unit_id is None and not event.name.endswith( + ("_relation_created", "relation_broken"), + ): + remote_unit_ids = relation._remote_unit_ids + + if len(remote_unit_ids) == 1: + remote_unit_id = remote_unit_ids[0] + logger.info( + "there's only one remote unit, so we set JUJU_REMOTE_UNIT to it, " + "but you probably should be parametrizing the event with `remote_unit_id` " + "to be explicit.", + ) + elif len(remote_unit_ids) > 1: + remote_unit_id = remote_unit_ids[0] + logger.warning( + "remote unit ID unset, and multiple remote unit IDs are present; " + "We will pick the first one and hope for the best. You should be passing " + "`remote_unit_id` to the Event constructor.", + ) + else: + logger.warning( + "remote unit ID unset; no remote unit data present. " + "Is this a realistic scenario?", # TODO: is it? + ) + + if remote_unit_id is not None: + remote_unit = f"{remote_app_name}/{remote_unit_id}" + env["JUJU_REMOTE_UNIT"] = remote_unit + if event.name.endswith("_relation_departed"): + if event.relation_departed_unit_id: + env["JUJU_DEPARTING_UNIT"] = ( + f"{remote_app_name}/{event.relation_departed_unit_id}" + ) + else: + env["JUJU_DEPARTING_UNIT"] = remote_unit + + if container := event.container: + env.update({"JUJU_WORKLOAD_NAME": container.name}) + + if notice := event.notice: + if hasattr(notice.type, "value"): + notice_type = typing.cast(pebble.NoticeType, notice.type).value + else: + notice_type = str(notice.type) + env.update( + { + "JUJU_NOTICE_ID": notice.id, + "JUJU_NOTICE_TYPE": notice_type, + "JUJU_NOTICE_KEY": notice.key, + }, + ) + + if check_info := event.check_info: + env["JUJU_PEBBLE_CHECK_NAME"] = check_info.name + + if storage := event.storage: + env.update({"JUJU_STORAGE_ID": f"{storage.name}/{storage.index}"}) + + if secret := event.secret: + env.update( + { + "JUJU_SECRET_ID": secret.id, + "JUJU_SECRET_LABEL": secret.label or "", + }, + ) + # Don't check truthiness because revision could be 0. + if event.secret_revision is not None: + env["JUJU_SECRET_REVISION"] = str(event.secret_revision) + + return env + + @staticmethod + def _wrap(charm_type: Type["CharmType"]) -> Type["CharmType"]: + # dark sorcery to work around framework using class attrs to hold on to event sources + # todo this should only be needed if we call play multiple times on the same runtime. + # can we avoid it? + class WrappedEvents(charm_type.on.__class__): + pass + + WrappedEvents.__name__ = charm_type.on.__class__.__name__ + + class WrappedCharm(charm_type): + on = WrappedEvents() + + WrappedCharm.__name__ = charm_type.__name__ + return typing.cast(Type["CharmType"], WrappedCharm) + + @contextmanager + def _virtual_charm_root(self): + # If we are using runtime on a real charm, we can make some assumptions about the + # directory structure we are going to find. + # If we're, say, dynamically defining charm types and doing tests on them, we'll have to + # generate the metadata files ourselves. To be sure, we ALWAYS use a tempdir. Ground truth + # is what the user passed via the CharmSpec + spec = self._charm_spec + + if charm_virtual_root := self._charm_root: + charm_virtual_root_is_custom = True + virtual_charm_root = Path(charm_virtual_root) + else: + charm_virtual_root = tempfile.TemporaryDirectory() + virtual_charm_root = Path(charm_virtual_root.name) + charm_virtual_root_is_custom = False + + metadata_yaml = virtual_charm_root / "metadata.yaml" + config_yaml = virtual_charm_root / "config.yaml" + actions_yaml = virtual_charm_root / "actions.yaml" + + metadata_files_present: Dict[Path, Optional[str]] = { + file: file.read_text() if file.exists() else None + for file in (metadata_yaml, config_yaml, actions_yaml) + } + + any_metadata_files_present_in_charm_virtual_root = any( + v is not None for v in metadata_files_present.values() + ) + + if spec.is_autoloaded and charm_virtual_root_is_custom: + # since the spec is autoloaded, in theory the metadata contents won't differ, so we can + # overwrite away even if the custom vroot is the real charm root (the local repo). + # Still, log it for clarity. + if any_metadata_files_present_in_charm_virtual_root: + logger.debug( + f"metadata files found in custom charm_root {charm_virtual_root}. " + f"The spec was autoloaded so the contents should be identical. " + f"Proceeding...", + ) + + elif ( + not spec.is_autoloaded and any_metadata_files_present_in_charm_virtual_root + ): + logger.warning( + f"Some metadata files found in custom user-provided charm_root " + f"{charm_virtual_root} while you have passed meta, config or actions to " + f"Context.run(). " + "Single source of truth are the arguments passed to Context.run(). " + "charm_root metadata files will be overwritten for the " + "duration of this test, and restored afterwards. " + "To avoid this, clean any metadata files from the charm_root before calling run.", + ) + + metadata_yaml.write_text(yaml.safe_dump(spec.meta)) + config_yaml.write_text(yaml.safe_dump(spec.config or {})) + actions_yaml.write_text(yaml.safe_dump(spec.actions or {})) + + yield virtual_charm_root + + if charm_virtual_root_is_custom: + for file, previous_content in metadata_files_present.items(): + if previous_content is None: # None == file did not exist before + file.unlink() + else: + file.write_text(previous_content) + + else: + # charm_virtual_root is a tempdir + typing.cast(tempfile.TemporaryDirectory, charm_virtual_root).cleanup() # type: ignore + + @contextmanager + def _exec_ctx(self, ctx: "Context"): + """python 3.8 compatibility shim""" + with self._virtual_charm_root() as temporary_charm_root: + # TODO: allow customising capture_events + with capture_events( + include_deferred=ctx.capture_deferred_events, + include_framework=ctx.capture_framework_events, + ) as captured: + yield (temporary_charm_root, captured) + + @contextmanager + def exec( + self, + state: "State", + event: "_Event", + context: "Context", + ): + """Runs an event with this state as initial state on a charm. + + Returns the 'output state', that is, the state as mutated by the charm during the + event handling. + + This will set the environment up and call ops.main(). + After that it's up to ops. + """ + # todo consider forking out a real subprocess and do the mocking by + # mocking hook tool executables + + from ._consistency_checker import check_consistency # avoid cycles + + check_consistency(state, event, self._charm_spec, self._juju_version) + + charm_type = self._charm_spec.charm_type + logger.info(f"Preparing to fire {event.name} on {charm_type.__name__}") + + # we make a copy to avoid mutating the input state + output_state = copy.deepcopy(state) + + logger.info(" - generating virtual charm root") + with self._exec_ctx(context) as (temporary_charm_root, captured): + logger.info(" - preparing env") + env = self._get_event_env( + state=state, + event=event, + charm_root=temporary_charm_root, + ) + juju_context = _JujuContext.from_dict(env) + + logger.info(" - entering ops.main (mocked)") + from ._ops_main_mock import Ops # noqa: F811 + + try: + ops = Ops( + state=output_state, + event=event, + context=context, + charm_spec=dataclasses.replace( + self._charm_spec, + charm_type=self._wrap(charm_type), + ), + juju_context=juju_context, + ) + + yield ops + + except (NoObserverError, ActionFailed): + raise # propagate along + except Exception as e: + raise UncaughtCharmError( + f"Uncaught exception ({type(e)}) in operator/charm code: {e!r}", + ) from e + + finally: + logger.info(" - exited ops.main") + + context.emitted_events.extend(captured) + logger.info("event dispatched. done.") + context._set_output_state(ops.state) + + +_T = TypeVar("_T", bound=EventBase) + + +@contextmanager +def capture_events( + *types: Type[EventBase], + include_framework: bool = False, + include_deferred: bool = True, +): + """Capture all events of type `*types` (using instance checks). + + Arguments exposed so that you can define your own fixtures if you want to. + + Example:: + >>> from ops import StartEvent + >>> from scenario import Event, State + >>> from charm import MyCustomEvent, MyCharm # noqa + >>> + >>> def test_my_event(): + >>> with capture_events(StartEvent, MyCustomEvent) as captured: + >>> trigger(State(), ("start", MyCharm, meta=MyCharm.META) + >>> + >>> assert len(captured) == 2 + >>> e1, e2 = captured + >>> assert isinstance(e2, MyCustomEvent) + >>> assert e2.custom_attr == 'foo' + """ + allowed_types = types or (EventBase,) + + captured: List[EventBase] = [] + _real_emit = Framework._emit + _real_reemit = Framework.reemit + + def _wrapped_emit(self: Framework, evt: EventBase): + if not include_framework and isinstance( + evt, + (PreCommitEvent, CommitEvent, CollectStatusEvent), + ): + return _real_emit(self, evt) + + if isinstance(evt, allowed_types): + # dump/undump the event to ensure any custom attributes are (re)set by restore() + evt.restore(evt.snapshot()) + captured.append(evt) + + return _real_emit(self, evt) + + def _wrapped_reemit(self: Framework): + # Framework calls reemit() before emitting the main juju event. We intercept that call + # and capture all events in storage. + + if not include_deferred: + return _real_reemit(self) + + # load all notices from storage as events. + for event_path, _, _ in self._storage.notices(): + event_handle = Handle.from_path(event_path) + try: + event = self.load_snapshot(event_handle) + except NoTypeError: + continue + event = typing.cast(EventBase, event) + event.deferred = False + self._forget(event) # prevent tracking conflicts + + if not include_framework and isinstance( + event, + (PreCommitEvent, CommitEvent), + ): + continue + + if isinstance(event, allowed_types): + captured.append(event) + + return _real_reemit(self) + + Framework._emit = _wrapped_emit # type: ignore + Framework.reemit = _wrapped_reemit + + yield captured + + Framework._emit = _real_emit + Framework.reemit = _real_reemit diff --git a/testing/build/lib/scenario/context.py b/testing/build/lib/scenario/context.py new file mode 100644 index 000000000..8087480f2 --- /dev/null +++ b/testing/build/lib/scenario/context.py @@ -0,0 +1,692 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Test Context + +The test `Context` object provides the context of the wider Juju system that the +specific `State` exists in, and the events that can be executed on that `State`. +""" + +from __future__ import annotations + +import functools +import tempfile +from contextlib import contextmanager +from pathlib import Path +from typing import ( + Generic, + TYPE_CHECKING, + Any, + Callable, + Mapping, +) + +import ops +from ops._private.harness import ActionFailed + +from .errors import ( + AlreadyEmittedError, + ContextSetupError, + MetadataNotFoundError, +) +from .logger import logger as scenario_logger +from .runtime import Runtime +from .state import ( + CharmType, + CheckInfo, + Container, + Notice, + Secret, + Storage, + _Action, + _CharmSpec, + _Event, +) + +if TYPE_CHECKING: # pragma: no cover + from ops._private.harness import ExecArgs + from .ops_main_mock import Ops + from .state import ( + AnyJson, + JujuLogLine, + RelationBase, + State, + _EntityStatus, + ) + +logger = scenario_logger.getChild("runtime") + +_DEFAULT_JUJU_VERSION = "3.5" + + +class Manager(Generic[CharmType]): + """Context manager to offer test code some runtime charm object introspection. + + This class should not be instantiated directly: use a :class:`Context` + in a ``with`` statement instead, for example:: + + ctx = Context(MyCharm) + with ctx(ctx.on.start(), State()) as manager: + manager.charm.setup() + manager.run() + """ + + def __init__( + self, + ctx: Context[CharmType], + arg: _Event, + state_in: State, + ): + self._ctx = ctx + self._arg = arg + self._state_in = state_in + + self._emitted: bool = False + self._wrapped_ctx = None + + self.ops: Ops[CharmType] | None = None + + @property + def charm(self) -> CharmType: + """The charm object instantiated by ops to handle the event. + + The charm is only available during the context manager scope. + """ + if self.ops is None or self.ops.charm is None: + raise RuntimeError( + "you should __enter__ this context manager before accessing this", + ) + return self.ops.charm + + @property + def _runner(self): + return self._ctx._run # noqa + + def __enter__(self): + self._wrapped_ctx = wrapped_ctx = self._runner(self._arg, self._state_in) + self.ops = wrapped_ctx.__enter__() + return self + + def run(self) -> State: + """Emit the event and proceed with charm execution. + + This can only be done once. + """ + if self._emitted: + raise AlreadyEmittedError("Can only run once.") + self._emitted = True + + # wrap up Runtime.exec() so that we can gather the output state + assert self._wrapped_ctx is not None + self._wrapped_ctx.__exit__(None, None, None) + + assert self._ctx._output_state is not None + return self._ctx._output_state + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any): # noqa: U100 + if not self._emitted: + logger.debug( + "user didn't emit the event within the context manager scope. Doing so implicitly upon exit...", + ) + self.run() + + +def _copy_doc(original_func: Callable[..., Any]): + """Copy the docstring from `original_func` to the wrapped function.""" + + def decorator(wrapper_func: Callable[..., Any]): + @functools.wraps(wrapper_func) + def wrapped(*args: Any, **kwargs: Any): + return wrapper_func(*args, **kwargs) + + wrapped.__doc__ = original_func.__doc__ + return wrapped + + return decorator + + +class CharmEvents: + """Events generated by Juju or ops pertaining to the application lifecycle. + + The events listed as attributes of this class should be accessed via the + :attr:`Context.on` attribute. For example:: + + ctx.run(ctx.on.config_changed(), state) + + This behaves similarly to the :class:`ops.CharmEvents` class but is much + simpler as there are no dynamically named attributes, and no ``__getattr__`` + version to get events. In addition, all of the attributes are methods, + which are used to connect the event to the specific object that they relate + to (or, for simpler events like "start" or "stop", take no arguments). + """ + + @staticmethod + @_copy_doc(ops.InstallEvent) + def install(): + return _Event("install") + + @staticmethod + @_copy_doc(ops.StartEvent) + def start(): + return _Event("start") + + @staticmethod + @_copy_doc(ops.StopEvent) + def stop(): + return _Event("stop") + + @staticmethod + @_copy_doc(ops.RemoveEvent) + def remove(): + return _Event("remove") + + @staticmethod + @_copy_doc(ops.UpdateStatusEvent) + def update_status(): + return _Event("update_status") + + @staticmethod + @_copy_doc(ops.ConfigChangedEvent) + def config_changed(): + return _Event("config_changed") + + @staticmethod + @_copy_doc(ops.UpgradeCharmEvent) + def upgrade_charm(): + return _Event("upgrade_charm") + + @staticmethod + @_copy_doc(ops.PreSeriesUpgradeEvent) + def pre_series_upgrade(): + return _Event("pre_series_upgrade") + + @staticmethod + @_copy_doc(ops.PostSeriesUpgradeEvent) + def post_series_upgrade(): + return _Event("post_series_upgrade") + + @staticmethod + @_copy_doc(ops.LeaderElectedEvent) + def leader_elected(): + return _Event("leader_elected") + + @staticmethod + @_copy_doc(ops.SecretChangedEvent) + def secret_changed(secret: Secret): + if secret.owner: + raise ValueError( + "This unit will never receive secret-changed for a secret it owns.", + ) + return _Event("secret_changed", secret=secret) + + @staticmethod + @_copy_doc(ops.SecretExpiredEvent) + def secret_expired(secret: Secret, *, revision: int): + if not secret.owner: + raise ValueError( + "This unit will never receive secret-expire for a secret it does not own.", + ) + return _Event("secret_expired", secret=secret, secret_revision=revision) + + @staticmethod + @_copy_doc(ops.SecretRotateEvent) + def secret_rotate(secret: Secret): + if not secret.owner: + raise ValueError( + "This unit will never receive secret-rotate for a secret it does not own.", + ) + return _Event("secret_rotate", secret=secret) + + @staticmethod + @_copy_doc(ops.SecretRemoveEvent) + def secret_remove(secret: Secret, *, revision: int): + if not secret.owner: + raise ValueError( + "This unit will never receive secret-removed for a secret it does not own.", + ) + return _Event("secret_remove", secret=secret, secret_revision=revision) + + @staticmethod + def collect_app_status(): + """Event triggered at the end of every hook to collect app statuses for evaluation""" + return _Event("collect_app_status") + + @staticmethod + def collect_unit_status(): + """Event triggered at the end of every hook to collect unit statuses for evaluation""" + return _Event("collect_unit_status") + + @staticmethod + @_copy_doc(ops.RelationCreatedEvent) + def relation_created(relation: RelationBase): + return _Event(f"{relation.endpoint}_relation_created", relation=relation) + + @staticmethod + @_copy_doc(ops.RelationJoinedEvent) + def relation_joined(relation: RelationBase, *, remote_unit: int | None = None): + return _Event( + f"{relation.endpoint}_relation_joined", + relation=relation, + relation_remote_unit_id=remote_unit, + ) + + @staticmethod + @_copy_doc(ops.RelationChangedEvent) + def relation_changed( + relation: RelationBase, + *, + remote_unit: int | None = None, + ): + return _Event( + f"{relation.endpoint}_relation_changed", + relation=relation, + relation_remote_unit_id=remote_unit, + ) + + @staticmethod + @_copy_doc(ops.RelationDepartedEvent) + def relation_departed( + relation: RelationBase, + *, + remote_unit: int | None = None, + departing_unit: int | None = None, + ): + return _Event( + f"{relation.endpoint}_relation_departed", + relation=relation, + relation_remote_unit_id=remote_unit, + relation_departed_unit_id=departing_unit, + ) + + @staticmethod + @_copy_doc(ops.RelationBrokenEvent) + def relation_broken(relation: RelationBase): + return _Event(f"{relation.endpoint}_relation_broken", relation=relation) + + @staticmethod + @_copy_doc(ops.StorageAttachedEvent) + def storage_attached(storage: Storage): + return _Event(f"{storage.name}_storage_attached", storage=storage) + + @staticmethod + @_copy_doc(ops.StorageDetachingEvent) + def storage_detaching(storage: Storage): + return _Event(f"{storage.name}_storage_detaching", storage=storage) + + @staticmethod + @_copy_doc(ops.PebbleReadyEvent) + def pebble_ready(container: Container): + return _Event(f"{container.name}_pebble_ready", container=container) + + @staticmethod + @_copy_doc(ops.PebbleCustomNoticeEvent) + def pebble_custom_notice(container: Container, notice: Notice): + return _Event( + f"{container.name}_pebble_custom_notice", + container=container, + notice=notice, + ) + + @staticmethod + @_copy_doc(ops.PebbleCheckFailedEvent) + def pebble_check_failed(container: Container, info: CheckInfo): + return _Event( + f"{container.name}_pebble_check_failed", + container=container, + check_info=info, + ) + + @staticmethod + @_copy_doc(ops.PebbleCheckRecoveredEvent) + def pebble_check_recovered(container: Container, info: CheckInfo): + return _Event( + f"{container.name}_pebble_check_recovered", + container=container, + check_info=info, + ) + + @staticmethod + @_copy_doc(ops.ActionEvent) + def action( + name: str, + params: Mapping[str, AnyJson] | None = None, + id: str | None = None, + ): + kwargs: dict[str, Any] = {} + if params: + kwargs["params"] = params + if id: + kwargs["id"] = id + return _Event(f"{name}_action", action=_Action(name, **kwargs)) + + +class Context(Generic[CharmType]): + """Represents a simulated charm's execution context. + + The main entry point to running a test. It contains: + + - the charm source code being executed + - the metadata files associated with it + - a charm project repository root + - the Juju version to be simulated + + After you have instantiated ``Context``, typically you will call :meth:`run()` 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. + + Any side effects generated by executing the charm, that are not rightful part of the + ``State``, are in fact stored in the ``Context``: + + - :attr:`juju_log` + - :attr:`app_status_history` + - :attr:`unit_status_history` + - :attr:`workload_version_history` + - :attr:`removed_secret_revisions` + - :attr:`requested_storages` + - :attr:`emitted_events` + - :attr:`action_logs` + - :attr:`action_results` + + 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 test will look like:: + + from charm import MyCharm, MyCustomEvent # noqa + + def test_foo(): + # Arrange: set the context up + ctx = Context(MyCharm) + # Act: prepare the state and emit an event + state_out = ctx.run(ctx.on.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) + + If you need access to the charm object that will handle the event, use the + class in a ``with`` statement, like:: + + def test_foo(): + ctx = Context(MyCharm) + with ctx(ctx.on.start(), State()) as manager: + manager.charm._some_private_setup() + manager.run() + """ + + juju_log: list[JujuLogLine] + """A record of what the charm has sent to juju-log""" + app_status_history: list[_EntityStatus] + """A record of the app statuses the charm has set""" + unit_status_history: list[_EntityStatus] + """A record of the unit statuses the charm has set""" + workload_version_history: list[str] + """A record of the workload versions the charm has set""" + removed_secret_revisions: list[int] + """A record of the secret revisions the charm has removed""" + emitted_events: list[ops.EventBase] + """A record of the events (including custom) that the charm has processed""" + requested_storages: dict[str, int] + """A record of the storages the charm has requested""" + action_logs: list[str] + """The logs associated with the action output, set by the charm with :meth:`ops.ActionEvent.log` + + This will be empty when handling a non-action event. + """ + action_results: dict[str, Any] | None + """A key-value mapping assigned by the charm as a result of the action. + + This will be ``None`` if the charm never calls :meth:`ops.ActionEvent.set_results` + """ + on: CharmEvents + """The events that this charm can respond to. + + Use this when calling :meth:`run` to specify the event to emit. + """ + + def __init__( + self, + charm_type: type[CharmType], + meta: dict[str, Any] | None = None, + *, + actions: dict[str, Any] | None = None, + config: dict[str, Any] | None = None, + charm_root: str | Path | None = None, + juju_version: str = _DEFAULT_JUJU_VERSION, + capture_deferred_events: bool = False, + capture_framework_events: bool = False, + app_name: str | None = None, + unit_id: int | None = 0, + app_trusted: bool = False, + ): + """Represents a simulated charm's execution context. + + If the charm, say, expects a ``./src/foo/bar.yaml`` file present relative to the + execution cwd, you need to use the ``charm_root`` argument. For example:: + + import tempfile + virtual_root = tempfile.TemporaryDirectory() + local_path = Path(local_path.name) + (local_path / 'foo').mkdir() + (local_path / 'foo' / 'bar.yaml').write_text('foo: bar') + Context(... charm_root=virtual_root).run(...) + + :arg charm_type: the :class:`ops.CharmBase` subclass to handle the event. + :arg meta: charm metadata to use. Needs to be a valid metadata.yaml format (as a dict). + If none is provided, we will search for a ``metadata.yaml`` file in the charm root. + :arg actions: charm actions to use. Needs to be a valid actions.yaml format (as a dict). + If none is provided, we will search for a ``actions.yaml`` file in the charm root. + :arg config: charm config to use. Needs to be a valid config.yaml format (as a dict). + If none is provided, we will search for a ``config.yaml`` file in the charm root. + :arg juju_version: Juju agent version to simulate. + :arg app_name: App name that this charm is deployed as. Defaults to the charm name as + defined in the metadata. + :arg unit_id: Unit ID that this charm is deployed as. + :arg app_trusted: whether the charm has Juju trust (deployed with ``--trust`` or added with + ``juju trust``). + :arg charm_root: virtual charm filesystem root the charm will be executed with. + """ + + if not any((meta, actions, config)): + logger.debug("Autoloading charmspec...") + try: + spec: _CharmSpec[CharmType] = _CharmSpec.autoload(charm_type) + except MetadataNotFoundError as e: + raise ContextSetupError( + f"Cannot setup scenario with `charm_type`={charm_type}. " + f"Did you forget to pass `meta` to this Context?", + ) from e + + else: + if not meta: + meta = {"name": str(charm_type.__name__)} + spec = _CharmSpec( + charm_type=charm_type, + meta=meta, + actions=actions, + config=config, + ) + + self.charm_spec = spec + self.charm_root = charm_root + self.juju_version = juju_version + if juju_version.split(".")[0] == "2": + logger.warning( + "Juju 2.x is closed and unsupported. You may encounter inconsistencies.", + ) + + self._app_name = app_name + self._unit_id = unit_id + self.app_trusted = app_trusted + self._tmp = tempfile.TemporaryDirectory() + + # config for what events to be captured in emitted_events. + self.capture_deferred_events = capture_deferred_events + self.capture_framework_events = capture_framework_events + + # streaming side effects from running an event + self.juju_log: list[JujuLogLine] = [] + self.app_status_history: list[_EntityStatus] = [] + self.unit_status_history: list[_EntityStatus] = [] + self.exec_history: dict[str, list[ExecArgs]] = {} + self.workload_version_history: list[str] = [] + self.removed_secret_revisions: list[int] = [] + self.emitted_events: list[ops.EventBase] = [] + self.requested_storages: dict[str, int] = {} + + # set by Runtime.exec() in self._run() + self._output_state: State | None = None + + # operations (and embedded tasks) from running actions + self.action_logs: list[str] = [] + self.action_results: dict[str, Any] | None = None + self._action_failure_message: str | None = None + + self.on = CharmEvents() + + def _set_output_state(self, output_state: State): + """Hook for Runtime to set the output state.""" + self._output_state = output_state + + def _get_container_root(self, container_name: str): + """Get the path to a tempdir where this container's simulated root will live.""" + return Path(self._tmp.name) / "containers" / container_name + + def _get_storage_root(self, name: str, index: int) -> Path: + """Get the path to a tempdir where this storage's simulated root will live.""" + storage_root = Path(self._tmp.name) / "storages" / f"{name}-{index}" + # in the case of _get_container_root, _MockPebbleClient will ensure the dir exists. + storage_root.mkdir(parents=True, exist_ok=True) + return storage_root + + def _record_status(self, state: State, is_app: bool): + """Record the previous status before a status change.""" + if is_app: + self.app_status_history.append(state.app_status) + else: + self.unit_status_history.append(state.unit_status) + + def __call__(self, event: _Event, state: State) -> Manager[CharmType]: + """Context manager to introspect live charm object before and after the event is emitted. + + Usage:: + + ctx = Context(MyCharm) + with ctx(ctx.on.start(), State()) as manager: + manager.charm._some_private_setup() + manager.run() # this will fire the event + assert manager.charm._some_private_attribute == "bar" # noqa + + Args: + event: the event that the charm will respond to. + state: the :class:`State` instance to use when handling the event. + """ + return Manager(self, event, state) + + def run_action(self, action: str, state: State): + """Use `run()` instead. + + :private: + """ + raise AttributeError( + f"call with `ctx.run`, like `ctx.run(ctx.on.action({action!r})` " + "and find the results in `ctx.action_results`", + ) + + def run(self, event: _Event, state: State) -> State: + """Trigger a charm execution with an event and a State. + + Calling this function will call ``ops.main`` and set up the context according to the + specified :class:`State`, then emit the event on the charm. + + :arg event: the event that the charm will respond to. Use the :attr:`on` attribute to + specify the event; for example: ``ctx.on.start()``. + :arg state: the :class:`State` instance to use as data source for the hook tool calls that + the charm will invoke when handling the event. + """ + # Help people transition from Scenario 6: + if isinstance(event, str): + event = event.replace("-", "_") # type: ignore + if event in ( + "install", + "start", + "stop", + "remove", + "update_status", + "config_changed", + "upgrade_charm", + "pre_series_upgrade", + "post_series_upgrade", + "leader_elected", + "collect_app_status", + "collect_unit_status", + ): # type: ignore + suggested = f"{event}()" + elif event in ("secret_changed", "secret_rotate"): # type: ignore + suggested = f"{event}(my_secret)" + elif event in ("secret_expired", "secret_remove"): # type: ignore + suggested = f"{event}(my_secret, revision=1)" + elif event in ( + "relation_created", + "relation_joined", + "relation_changed", + "relation_departed", + "relation_broken", + ): # type: ignore + suggested = f"{event}(my_relation)" + elif event in ("storage_attached", "storage_detaching"): # type: ignore + suggested = f"{event}(my_storage)" + elif event == "pebble_ready": + suggested = f"{event}(my_container)" + elif event == "pebble_custom_notice": + suggested = f"{event}(my_container, my_notice)" + else: + suggested = "event()" + raise TypeError( + f"call with an event from `ctx.on`, like `ctx.on.{suggested}`", + ) + if callable(event): + raise TypeError( + "You should call the event method. Did you forget to add parentheses?", + ) + + if event.action: + # Reset the logs, failure status, and results, in case the context + # is reused. + self.action_logs.clear() + if self.action_results is not None: + self.action_results.clear() + self._action_failure_message = None + with self._run(event=event, state=state) as manager: + manager.emit() + # We know that the output state will have been set by this point, + # so let the type checkers know that too. + assert self._output_state is not None + if event.action: + if self._action_failure_message is not None: + raise ActionFailed( + self._action_failure_message, + state=self._output_state, # type: ignore + ) + return self._output_state + + @contextmanager + def _run(self, event: _Event, state: State): + runtime = Runtime( + charm_spec=self.charm_spec, + juju_version=self.juju_version, + charm_root=self.charm_root, + app_name=self._app_name, + unit_id=self._unit_id, + ) + with runtime.exec( + state=state, + event=event, + context=self, # type: ignore + ) as ops: + yield ops diff --git a/testing/build/lib/scenario/errors.py b/testing/build/lib/scenario/errors.py new file mode 100644 index 000000000..81049eaa6 --- /dev/null +++ b/testing/build/lib/scenario/errors.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Exceptions raised by the framework. + +Note that these exceptions are not meant to be caught by charm authors. They are +used by the framework to signal errors or inconsistencies in the charm tests +themselves. +""" + + +class ContextSetupError(RuntimeError): + """Raised by Context when setup fails.""" + + +class AlreadyEmittedError(RuntimeError): + """Raised when ``run()`` is called more than once.""" + + +class ScenarioRuntimeError(RuntimeError): + """Base class for exceptions raised by the runtime module.""" + + +class UncaughtCharmError(ScenarioRuntimeError): + """Error raised if the charm raises while handling the event being dispatched.""" + + +class InconsistentScenarioError(ScenarioRuntimeError): + """Error raised when the combination of state and event is inconsistent.""" + + +class StateValidationError(RuntimeError): + """Raised when individual parts of the State are inconsistent.""" + + # as opposed to InconsistentScenario error where the **combination** of + # several parts of the State are. + + +class MetadataNotFoundError(RuntimeError): + """Raised when a metadata file can't be found in the provided charm root.""" + + +class ActionMissingFromContextError(Exception): + """Raised when the user attempts to invoke action hook tools outside an action context.""" + + # This is not an ops error: in ops, you'd have to go exceptionally out of + # your way to trigger this flow. + + +class NoObserverError(RuntimeError): + """Error raised when the event being dispatched has no registered observers.""" + + +class BadOwnerPath(RuntimeError): + """Error raised when the owner path does not lead to a valid ObjectEvents instance.""" diff --git a/testing/build/lib/scenario/logger.py b/testing/build/lib/scenario/logger.py new file mode 100644 index 000000000..a7909f397 --- /dev/null +++ b/testing/build/lib/scenario/logger.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Test framework logger""" + +import logging +import os + +logger = logging.getLogger("ops-scenario") +logger.setLevel(os.getenv("OPS_SCENARIO_LOGGING", "WARNING")) diff --git a/testing/build/lib/scenario/mocking.py b/testing/build/lib/scenario/mocking.py new file mode 100644 index 000000000..b95b553be --- /dev/null +++ b/testing/build/lib/scenario/mocking.py @@ -0,0 +1,911 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Juju and Pebble mocking + +This module contains mocks for the Juju and Pebble APIs that are used by ops +to interact with the Juju controller and the Pebble service manager. +""" + +import datetime +import io +import shutil +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + Dict, + List, + Literal, + Mapping, + NoReturn, + Optional, + Set, + TextIO, + Tuple, + Union, + cast, + get_args, +) + +from ops import ( + JujuVersion, + pebble, + SecretInfo, + SecretNotFoundError, + RelationNotFoundError, + SecretRotate, + ModelError, +) +from ops._private.harness import ExecArgs, _TestingPebbleClient +from ops.jujucontext import _JujuContext +from ops.model import CloudSpec as CloudSpec_Ops +from ops.model import Port as Port_Ops +from ops.model import Secret as Secret_Ops # lol +from ops.model import ( + _format_action_result_dict, + _ModelBackend, + _SettableStatusName, +) +from ops.pebble import Client, ExecError + +from .errors import ActionMissingFromContextError +from .logger import logger as scenario_logger +from .state import ( + CharmType, + JujuLogLine, + Mount, + Network, + PeerRelation, + Relation, + RelationBase, + Storage, + SubordinateRelation, + _EntityStatus, + _port_cls_by_protocol, + _RawPortProtocolLiteral, +) + +if TYPE_CHECKING: # pragma: no cover + from .context import Context + from .state import Container as ContainerSpec + from .state import Exec, Secret, State, _CharmSpec, _Event + +logger = scenario_logger.getChild("mocking") + + +class _MockExecProcess: + def __init__( + self, + change_id: int, + args: ExecArgs, + return_code: int, + stdin: Optional[Union[TextIO, io.BytesIO]], + stdout: Optional[Union[TextIO, io.BytesIO]], + stderr: Optional[Union[TextIO, io.BytesIO]], + ): + self._change_id = change_id + self._args = args + self._return_code = return_code + self._waited = False + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + + def __del__(self): + if not self._waited: + self._close_stdin() + + def _close_stdin(self): + if self._args.stdin is None and self.stdin is not None: + self.stdin.seek(0) + self._args.stdin = self.stdin.read() + + def wait(self): + """Wait for the (mock) process to finish.""" + self._close_stdin() + self._waited = True + if self._return_code != 0: + raise ExecError(list(self._args.command), self._return_code, None, None) + + def wait_output(self): + """Wait for the (mock) process to finish and return tuple of (stdout, stderr).""" + self._close_stdin() + self._waited = True + stdout = self.stdout.read() if self.stdout is not None else None + stderr = self.stderr.read() if self.stderr is not None else None + if self._return_code != 0: + raise ExecError( + list(self._args.command), + self._return_code, + stdout, # type: ignore + stderr, # type: ignore + ) + return stdout, stderr + + def send_signal(self, sig: Union[int, str]) -> NoReturn: # noqa: U100 + """Send the given signal to the (mock) process.""" + raise NotImplementedError() + + +_NOT_GIVEN = object() # non-None default value sentinel + + +# pyright: reportIncompatibleMethodOverride=false +class _MockModelBackend(_ModelBackend): # type: ignore + def __init__( + self, + state: "State", + event: "_Event", + charm_spec: "_CharmSpec[CharmType]", + context: "Context", + juju_context: "_JujuContext", + ): + super().__init__(juju_context=juju_context) + self._state = state + self._event = event + self._context = context + self._charm_spec = charm_spec + + def opened_ports(self) -> Set[Port_Ops]: + return { + Port_Ops(protocol=port.protocol, port=port.port) + for port in self._state.opened_ports + } + + def open_port( + self, + protocol: "_RawPortProtocolLiteral", + port: Optional[int] = None, + ): + port_ = _port_cls_by_protocol[protocol](port=port) # type: ignore + ports = set(self._state.opened_ports) + if port_ not in ports: + ports.add(port_) + if ports != self._state.opened_ports: + self._state._update_opened_ports(frozenset(ports)) + + def close_port( + self, + protocol: "_RawPortProtocolLiteral", + port: Optional[int] = None, + ): + _port = _port_cls_by_protocol[protocol](port=port) # type: ignore + ports = set(self._state.opened_ports) + if _port in ports: + ports.remove(_port) + if ports != self._state.opened_ports: + self._state._update_opened_ports(frozenset(ports)) + + def get_pebble(self, socket_path: str) -> "Client": + container_name = socket_path.split("/")[ + 3 + ] # /charm/containers/<container_name>/pebble.socket + container_root = self._context._get_container_root(container_name) + try: + mounts = self._state.get_container(container_name).mounts + except KeyError: + # container not defined in state. + mounts = {} + + return cast( + Client, + _MockPebbleClient( + socket_path=socket_path, + container_root=container_root, + mounts=mounts, + state=self._state, + event=self._event, + charm_spec=self._charm_spec, + context=self._context, + container_name=container_name, + ), + ) + + def _get_relation_by_id(self, rel_id: int) -> "RelationBase": + try: + return self._state.get_relation(rel_id) + except KeyError: + raise RelationNotFoundError() from None + + def _get_secret(self, id: Optional[str] = None, label: Optional[str] = None): + if self._context.juju_version < "3.0.2": + raise ModelError( + "secrets are only available in juju >= 3.0.2." + "Set ``Context.juju_version`` to 3.0.2+ to use them.", + ) + + canonicalize_id = Secret_Ops._canonicalize_id + + if id: + # in scenario, you can create Secret(id="foo"), + # but ops.Secret will prepend a "secret:" prefix to that ID. + # we allow getting secret by either version. + secrets = [ + s + for s in self._state.secrets + if canonicalize_id(s.id, model_uuid=self._state.model.uuid) + == canonicalize_id(id, model_uuid=self._state.model.uuid) + ] + if not secrets: + raise SecretNotFoundError(id) + return secrets[0] + + if label: + try: + return self._state.get_secret(label=label) + except KeyError: + raise SecretNotFoundError(label) from None + + # if all goes well, this should never be reached. ops.model.Secret will check upon + # instantiation that either an id or a label are set, and raise a TypeError if not. + raise RuntimeError("need id or label.") + + def _check_app_data_access(self, is_app: bool): + if not isinstance(is_app, bool): + raise TypeError("is_app parameter to relation_get must be a boolean") + + if not is_app: + return + + version = JujuVersion(self._context.juju_version) + if not version.has_app_data(): + raise RuntimeError( + f"setting application data is not supported on Juju version {version}", + ) + + def relation_get(self, relation_id: int, member_name: str, is_app: bool): + self._check_app_data_access(is_app) + relation = self._get_relation_by_id(relation_id) + if is_app and member_name == self.app_name: + return relation.local_app_data + if is_app: + if isinstance(relation, PeerRelation): + return relation.local_app_data + if isinstance(relation, (Relation, SubordinateRelation)): + return relation.remote_app_data + raise TypeError("relation_get: unknown relation type") + if member_name == self.unit_name: + return relation.local_unit_data + + unit_id = int(member_name.split("/")[-1]) + return relation._get_databag_for_remote(unit_id) # noqa + + def is_leader(self): + return self._state.leader + + def status_get(self, *, is_app: bool = False): + status = self._state.app_status if is_app else self._state.unit_status + return {"status": status.name, "message": status.message} + + def relation_ids(self, relation_name: str): + return [ + rel.id for rel in self._state.relations if rel.endpoint == relation_name + ] + + def relation_list(self, relation_id: int) -> Tuple[str, ...]: + relation = self._get_relation_by_id(relation_id) + + if isinstance(relation, PeerRelation): + return tuple( + f"{self.app_name}/{unit_id}" for unit_id in relation.peers_data + ) + remote_name = self.relation_remote_app_name(relation_id) + return tuple( + f"{remote_name}/{unit_id}" for unit_id in relation._remote_unit_ids + ) + + def config_get(self): + state_config = self._state.config.copy() # dedup or we'll mutate the state! + + # add defaults + charm_config = self._charm_spec.config + if not charm_config: + return state_config + + for key, value in charm_config["options"].items(): + # if it has a default, and it's not overwritten from State, use it: + if key not in state_config: + default_value = value.get("default", _NOT_GIVEN) + if default_value is not _NOT_GIVEN: # accept False as default value + state_config[key] = default_value + + return state_config # full config + + def network_get(self, binding_name: str, relation_id: Optional[int] = None): + # validation: + extra_bindings = self._charm_spec.meta.get("extra-bindings", ()) + all_endpoints = self._charm_spec.get_all_relations() + non_sub_relations = { + name for name, meta in all_endpoints if meta.get("scope") != "container" + } + + # - is binding_name a valid binding name? + if binding_name in extra_bindings: + logger.warning("extra-bindings is a deprecated feature") # fyi + + # - verify that if the binding is an extra binding, we're not ignoring a relation_id + if relation_id is not None: + # this should not happen + logger.error( + "cannot pass relation_id to network_get if the binding name is " + "that of an extra-binding. Extra-bindings are not mapped to relation IDs.", + ) + elif binding_name == "juju-info": + # implicit relation that always exists + pass + # - verify that the binding is a relation endpoint name, but not a subordinate one + elif binding_name not in non_sub_relations: + logger.error( + f"cannot get network binding for {binding_name}: is not a valid relation " + f"endpoint name nor an extra-binding.", + ) + raise RelationNotFoundError() + + # We look in State.networks for an override. If not given, we return a default network. + try: + network = self._state.get_network(binding_name) + except KeyError: + network = Network("default") # The name is not used in the output. + return network._hook_tool_output_fmt() + + # setter methods: these can mutate the state. + def application_version_set(self, version: str): + if workload_version := self._state.workload_version: + # do not record if empty = unset + self._context.workload_version_history.append(workload_version) + + self._state._update_workload_version(version) + + def status_set( + self, + status: _SettableStatusName, + message: str = "", + *, + is_app: bool = False, + ): + valid_names = get_args(_SettableStatusName) + if status not in valid_names: + raise ModelError( + f'ERROR invalid status "{status}", expected one of [{", ".join(valid_names)}]', + ) + self._context._record_status(self._state, is_app) + status_obj = _EntityStatus.from_status_name(status, message) + self._state._update_status(status_obj, is_app) + + def juju_log(self, level: str, message: str): + self._context.juju_log.append(JujuLogLine(level, message)) + + def relation_set(self, relation_id: int, key: str, value: str, is_app: bool): + self._check_app_data_access(is_app) + relation = self._get_relation_by_id(relation_id) + if is_app: + if not self._state.leader: + # will in practice not be reached because RelationData will check leadership + # and raise RelationDataAccessError upstream on this path + raise RuntimeError("needs leadership to set app data") + tgt = relation.local_app_data + else: + tgt = relation.local_unit_data + tgt[key] = value + + def secret_add( + self, + content: Dict[str, str], + *, + label: Optional[str] = None, + description: Optional[str] = None, + expire: Optional[datetime.datetime] = None, + rotate: Optional[SecretRotate] = None, + owner: Optional[Literal["unit", "app"]] = None, + ) -> str: + from .state import Secret + + secret = Secret( + content, + label=label, + description=description, + expire=expire, + rotate=rotate, + owner=owner, + ) + secrets = set(self._state.secrets) + secrets.add(secret) + self._state._update_secrets(frozenset(secrets)) + return secret.id + + def _check_can_manage_secret( + self, + secret: "Secret", + ): + if secret.owner is None: + raise SecretNotFoundError( + "this secret is not owned by this unit/app or granted to it. " + "Did you forget passing it to State.secrets?", + ) + if secret.owner == "app" and not self.is_leader(): + understandable_error = SecretNotFoundError( + f"App-owned secret {secret.id!r} can only be " + f"managed by the leader.", + ) + # charm-facing side: respect ops error + raise ModelError("ERROR permission denied") from understandable_error + + def secret_get( + self, + *, + id: Optional[str] = None, + label: Optional[str] = None, + refresh: bool = False, + peek: bool = False, + ) -> Dict[str, str]: + secret = self._get_secret(id, label) + # If both the id and label are provided, then update the label. + if id is not None and label is not None: + secret._set_label(label) + juju_version = self._context.juju_version + if not (juju_version == "3.1.7" or juju_version >= "3.3.1"): + # In this medieval Juju chapter, + # secret owners always used to track the latest revision. + # ref: https://bugs.launchpad.net/juju/+bug/2037120 + if secret.owner is not None: + refresh = True + + if peek or refresh: + if refresh: + secret._track_latest_revision() + assert secret.latest_content is not None + return secret.latest_content + + return secret.tracked_content + + def secret_info_get( + self, + *, + id: Optional[str] = None, + label: Optional[str] = None, + ) -> SecretInfo: + secret = self._get_secret(id, label) + # If both the id and label are provided, then update the label. + if id is not None and label is not None: + secret._set_label(label) + + # only "manage"=write access level can read secret info + self._check_can_manage_secret(secret) + + return SecretInfo( + id=secret.id, + label=secret.label, + revision=secret._latest_revision, + expires=secret.expire, + rotation=secret.rotate, + rotates=None, # not implemented yet. + ) + + def secret_set( + self, + id: str, + *, + content: Optional[Dict[str, str]] = None, + label: Optional[str] = None, + description: Optional[str] = None, + expire: Optional[datetime.datetime] = None, + rotate: Optional[SecretRotate] = None, + ): + secret = self._get_secret(id, label) + self._check_can_manage_secret(secret) + + if content == secret.latest_content: + # In Juju 3.6 and higher, this is a no-op, but it's good to warn + # charmers if they are doing this, because it's not generally good + # practice. + # https://bugs.launchpad.net/juju/+bug/2069238 + logger.warning( + f"secret {id} contents set to the existing value: new revision not needed", + ) + + secret._update_metadata( + content=content, + label=label, + description=description, + expire=expire, + rotate=rotate, + ) + + def secret_grant(self, id: str, relation_id: int, *, unit: Optional[str] = None): + secret = self._get_secret(id) + self._check_can_manage_secret(secret) + + grantee = unit or self.relation_remote_app_name( + relation_id, + _raise_on_error=True, + ) + + if not secret.remote_grants.get(relation_id): + secret.remote_grants[relation_id] = set() + + secret.remote_grants[relation_id].add(cast(str, grantee)) + + def secret_revoke(self, id: str, relation_id: int, *, unit: Optional[str] = None): + secret = self._get_secret(id) + self._check_can_manage_secret(secret) + + grantee = unit or self.relation_remote_app_name( + relation_id, + _raise_on_error=True, + ) + secret.remote_grants[relation_id].remove(cast(str, grantee)) + if not secret.remote_grants[relation_id]: + del secret.remote_grants[relation_id] + + def secret_remove(self, id: str, *, revision: Optional[int] = None): + secret = self._get_secret(id) + self._check_can_manage_secret(secret) + + # Removing all revisions means that the secret is removed, so is no + # longer in the state. + if revision is None: + secrets = set(self._state.secrets) + secrets.remove(secret) + self._state._update_secrets(frozenset(secrets)) + return + + # Juju does not prevent removing the tracked or latest revision, but it + # is always a mistake to do this. Rather than having the state model a + # secret where the tracked/latest revision cannot be retrieved but the + # secret still exists, we raise instead, so that charms know that there + # is a problem with their code. + if revision in (secret._tracked_revision, secret._latest_revision): + raise ValueError( + "Charms should not remove the latest revision of a secret. " + "Add a new revision with `set_content()` instead, and the previous " + "revision will be cleaned up by the secret owner when no longer in use.", + ) + + # For all other revisions, the content is not visible to the charm + # (this is as designed: the secret is being removed, so it should no + # longer be in use). That means that the state does not need to be + # modified - however, unit tests should be able to verify that the remove call was + # executed, so we track that in a history list in the context. + self._context.removed_secret_revisions.append(revision) + + def relation_remote_app_name( + self, + relation_id: int, + _raise_on_error: bool = False, + ) -> Optional[str]: + # ops catches RelationNotFoundErrors and returns None: + try: + relation = self._get_relation_by_id(relation_id) + except RelationNotFoundError: + if _raise_on_error: + raise + return None + + if isinstance(relation, PeerRelation): + return self.app_name + if isinstance(relation, (Relation, SubordinateRelation)): + return relation.remote_app_name + raise TypeError("relation_remote_app_name: unknown relation type") + + def action_set(self, results: Dict[str, Any]): + if not self._event.action: + raise ActionMissingFromContextError( + "not in the context of an action event: cannot action-set", + ) + # let ops validate the results dict + _format_action_result_dict(results) + # but then we will store it in its unformatted, + # original form for testing ease + if self._context.action_results: + self._context.action_results.update(results) + else: + self._context.action_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_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_logs.append(message) + + def action_get(self): + action = self._event.action + if not action: + raise ActionMissingFromContextError( + "not in the context of an action event: cannot action-get", + ) + return action.params + + def storage_add(self, name: str, count: int = 1): + if not isinstance(count, int) or isinstance(count, bool): + raise TypeError( + f"storage count must be integer, got: {count} ({type(count)})", + ) + + if "/" in name: + # this error is raised by Harness but not by ops at runtime + raise ModelError('storage name cannot contain "/"') + + self._context.requested_storages[name] = count + + def storage_list(self, name: str) -> List[int]: + return [ + storage.index for storage in self._state.storages if storage.name == name + ] + + def _storage_event_details(self) -> Tuple[int, str]: + storage = self._event.storage + if not storage: + # only occurs if this method is called when outside the scope of a storage event + raise RuntimeError('unable to find storage key in ""') + fs_path = storage.get_filesystem(self._context) + return storage.index, str(fs_path) + + def storage_get(self, storage_name_id: str, attribute: str) -> str: + if not len(attribute) > 0: # assume it's an empty string. + raise RuntimeError( + 'calling storage_get with `attribute=""` will return a dict ' + "and not a string. This usage is not supported.", + ) + + if attribute != "location": + # this should not happen: in ops it's hardcoded to be "location" + raise NotImplementedError( + f"storage-get not implemented for attribute={attribute}", + ) + + name, index = storage_name_id.split("/") + index = int(index) + storages: List[Storage] = [ + s for s in self._state.storages if s.name == name and s.index == index + ] + + # should not really happen: sanity checks. In practice, ops will guard against these paths. + if not storages: + raise RuntimeError(f"Storage with name={name} and index={index} not found.") + if len(storages) > 1: + raise RuntimeError( + f"Multiple Storage instances with name={name} and index={index} found. " + f"Inconsistent state.", + ) + + storage = storages[0] + fs_path = storage.get_filesystem(self._context) + return str(fs_path) + + def planned_units(self) -> int: + return self._state.planned_units + + # legacy ops API that we don't intend to mock: + def pod_spec_set( + self, + spec: Mapping[str, Any], # noqa: U100 + k8s_resources: Optional[Mapping[str, Any]] = None, # noqa: U100 + ) -> NoReturn: + raise NotImplementedError( + "pod-spec-set is not implemented in Scenario (and probably never will be: " + "it's deprecated API)", + ) + + def add_metrics( + self, + metrics: Mapping[str, Union[int, float]], # noqa: U100 + labels: Optional[Mapping[str, str]] = None, # noqa: U100 + ) -> NoReturn: + raise NotImplementedError( + "add-metrics is not implemented in Scenario (and probably never will be: " + "it's deprecated API)", + ) + + def resource_get(self, resource_name: str) -> str: + # We assume that there are few enough resources that a linear search + # will perform well enough. + for resource in self._state.resources: + if resource.name == resource_name: + return str(resource.path) + # ops will not let us get there if the resource name is unknown from metadata. + # but if the user forgot to add it in State, then we remind you of that. + raise RuntimeError( + f"Inconsistent state: " + f"resource {resource_name} not found in State. please pass it.", + ) + + def credential_get(self) -> CloudSpec_Ops: + if not self._context.app_trusted: + raise ModelError( + "ERROR charm is not trusted, initialise Context with `app_trusted=True`", + ) + if not self._state.model.cloud_spec: + raise ModelError( + "ERROR cloud spec is empty, initialise it with " + "`State(model=Model(..., cloud_spec=ops.CloudSpec(...)))`", + ) + return self._state.model.cloud_spec._to_ops() + + +class _MockPebbleClient(_TestingPebbleClient): + def __init__( + self, + socket_path: str, + container_root: Path, + mounts: Dict[str, Mount], + *, + state: "State", + event: "_Event", + charm_spec: "_CharmSpec[CharmType]", + context: "Context", + container_name: str, + ): + self._state = state + self.socket_path = socket_path + self._event = event + self._charm_spec = charm_spec + self._context = context + self._container_name = container_name + + # wipe just in case + if container_root.exists(): + # Path.rmdir will fail if root is nonempty + shutil.rmtree(container_root) + + # initialize simulated filesystem + container_root.mkdir(parents=True) + for _, mount in mounts.items(): + path = Path(mount.location).parts + mounting_dir = container_root.joinpath(*path[1:]) + mounting_dir.parent.mkdir(parents=True, exist_ok=True) + mounting_dir.symlink_to(mount.source) + + self._root = container_root + + self._notices: Dict[Tuple[str, str], pebble.Notice] = {} + self._last_notice_id = 0 + self._changes: Dict[str, pebble.Change] = {} + + # load any existing notices and check information from the state + self._notices: Dict[Tuple[str, str], pebble.Notice] = {} + self._check_infos: Dict[str, pebble.CheckInfo] = {} + for container in state.containers: + for notice in container.notices: + if hasattr(notice.type, "value"): + notice_type = cast(pebble.NoticeType, notice.type).value + else: + notice_type = str(notice.type) + self._notices[notice_type, notice.key] = notice._to_ops() + for check in container.check_infos: + self._check_infos[check.name] = check._to_ops() + + def get_plan(self) -> pebble.Plan: + return self._container.plan + + @property + def _container(self) -> "ContainerSpec": + container_name = self.socket_path.split("/")[-2] + try: + return next( + filter(lambda x: x.name == container_name, self._state.containers), + ) + except StopIteration: + raise RuntimeError( + f"container with name={container_name!r} not found. " + f"Did you forget a Container, or is the socket path " + f"{self.socket_path!r} wrong?", + ) + + @property + def _layers(self) -> Dict[str, pebble.Layer]: + return self._container.layers + + @property + def _service_status(self) -> Dict[str, pebble.ServiceStatus]: + return self._container.service_statuses + + # Based on a method of the same name from Harness. + def _find_exec_handler(self, command: List[str]) -> Optional["Exec"]: + handlers = {exec.command_prefix: exec for exec in self._container.execs} + # Start with the full command and, each loop iteration, drop the last + # element, until it matches one of the command prefixes in the execs. + # This includes matching against the empty list, which will match any + # command, if there is not a more specific match. + for prefix_len in reversed(range(len(command) + 1)): + command_prefix = tuple(command[:prefix_len]) + if command_prefix in handlers: + return handlers[command_prefix] + # None of the command prefixes in the execs matched the command, no + # matter how much of it was used, so we have failed to find a handler. + return None + + def exec( + self, + command: List[str], + *, + environment: Optional[Dict[str, str]] = None, + working_dir: Optional[str] = None, + timeout: Optional[float] = None, + user_id: Optional[int] = None, + user: Optional[str] = None, + group_id: Optional[int] = None, + group: Optional[str] = None, + stdin: Optional[Union[str, bytes, TextIO]] = None, + stdout: Optional[TextIO] = None, + stderr: Optional[TextIO] = None, + encoding: Optional[str] = "utf-8", + combine_stderr: bool = False, + **kwargs: Any, + ): + handler = self._find_exec_handler(command) + if not handler: + raise ExecError( + command, + 127, + "", + f"mock for cmd {command} not found. Please patch out whatever " + f"leads to the call, or pass to the Container {self._container.name} " + f"a scenario.Exec mock for the command your charm is attempting " + f"to run, such as " + f"'Container(..., execs={{scenario.Exec({list(command)}, ...)}})'", + ) + + if stdin is None: + proc_stdin = self._transform_exec_handler_output("", encoding) + else: + proc_stdin = None + stdin = stdin.read() if hasattr(stdin, "read") else stdin # type: ignore + if stdout is None: + proc_stdout = self._transform_exec_handler_output(handler.stdout, encoding) + else: + proc_stdout = None + stdout.write(handler.stdout) + if stderr is None: + proc_stderr = self._transform_exec_handler_output(handler.stderr, encoding) + else: + proc_stderr = None + stderr.write(handler.stderr) + + args = ExecArgs( + command=command, + environment=environment or {}, + working_dir=working_dir, + timeout=timeout, + user_id=user_id, + user=user, + group_id=group_id, + group=group, + stdin=stdin, # type:ignore # If None, will be replaced by proc_stdin.read() later. + encoding=encoding, + combine_stderr=combine_stderr, + ) + try: + self._context.exec_history[self._container_name].append(args) + except KeyError: + self._context.exec_history[self._container_name] = [args] + + change_id = handler._run() + return cast( + pebble.ExecProcess[Any], + _MockExecProcess( + change_id=change_id, + args=args, + return_code=handler.return_code, + stdin=proc_stdin, + stdout=proc_stdout, + stderr=proc_stderr, + ), + ) + + def _check_connection(self): + if not self._container.can_connect: + msg = ( + f"Cannot connect to Pebble; did you forget to set " + f"can_connect=True for container {self._container.name}?" + ) + raise pebble.ConnectionError(msg) diff --git a/testing/build/lib/scenario/ops_main_mock.py b/testing/build/lib/scenario/ops_main_mock.py new file mode 100644 index 000000000..5e4846eba --- /dev/null +++ b/testing/build/lib/scenario/ops_main_mock.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +import inspect +import os +import pathlib +import sys +from typing import TYPE_CHECKING, Any, Generic, Optional, Sequence, Type, cast + +import ops.charm +import ops.framework +import ops.jujucontext +import ops.model +import ops.storage +from ops import CharmBase + +# use logger from ops._main so that juju_log will be triggered +from ops._main import CHARM_STATE_FILE, _Dispatcher, _get_event_args +from ops._main import logger as ops_logger +from ops.charm import CharmMeta +from ops.log import setup_root_logging + +from .errors import BadOwnerPath, NoObserverError +from .state import CharmType + +if TYPE_CHECKING: # pragma: no cover + from .context import Context + from .state import State, _CharmSpec, _Event + +# pyright: reportPrivateUsage=false + + +def _get_owner(root: Any, path: Sequence[str]) -> ops.ObjectEvents: + """Walk path on root to an ObjectEvents instance.""" + obj = root + for step in path: + try: + obj = getattr(obj, step) + except AttributeError: + raise BadOwnerPath( + f"event_owner_path {path!r} invalid: {step!r} leads to nowhere.", + ) + if not isinstance(obj, ops.ObjectEvents): + raise BadOwnerPath( + f"event_owner_path {path!r} invalid: does not lead to " + f"an ObjectEvents instance.", + ) + return obj + + +def _emit_charm_event( + charm: "CharmBase", + event_name: str, + juju_context: ops.jujucontext._JujuContext, + event: Optional["_Event"] = None, +): + """Emits a charm event based on a Juju event name. + + Args: + charm: A charm instance to emit an event from. + event_name: A Juju event name to emit on a charm. + event: Event to emit. + juju_context: Juju context to use for the event. + """ + owner = _get_owner(charm, event.owner_path) if event else charm.on + + try: + event_to_emit = getattr(owner, event_name) + except AttributeError: + ops_logger.debug("Event %s not defined for %s.", event_name, charm) + raise NoObserverError( + f"Cannot fire {event_name!r} on {owner}: " + f"invalid event (not on charm.on).", + ) + + args, kwargs = _get_event_args(charm, event_to_emit, juju_context) + ops_logger.debug("Emitting Juju event %s.", event_name) + event_to_emit.emit(*args, **kwargs) + + +def setup_framework( + charm_dir: pathlib.Path, + state: "State", + event: "_Event", + context: "Context[CharmType]", + charm_spec: "_CharmSpec[CharmType]", + juju_context: Optional[ops.jujucontext._JujuContext] = None, +): + from .mocking import _MockModelBackend + + if juju_context is None: + juju_context = ops.jujucontext._JujuContext.from_dict(os.environ) + model_backend = _MockModelBackend( + state=state, + event=event, + context=context, + charm_spec=charm_spec, + juju_context=juju_context, + ) + setup_root_logging(model_backend, debug=juju_context.debug) + # ops sets sys.excepthook to go to Juju's debug-log, but that's not useful + # in a testing context, so reset it. + sys.excepthook = sys.__excepthook__ + ops_logger.debug( + "Operator Framework %s up and running.", + ops.__version__, + ) + + metadata = (charm_dir / "metadata.yaml").read_text() + actions_meta = charm_dir / "actions.yaml" + if actions_meta.exists(): + actions_metadata = actions_meta.read_text() + else: + actions_metadata = None + + meta = CharmMeta.from_yaml(metadata, actions_metadata) + + # ops >= 2.10 + if inspect.signature(ops.model.Model).parameters.get("broken_relation_id"): + # If we are in a RelationBroken event, we want to know which relation is + # broken within the model, not only in the event's `.relation` attribute. + broken_relation_id = ( + event.relation.id # type: ignore + if event.name.endswith("_relation_broken") + else None + ) + + model = ops.model.Model( + meta, + model_backend, + broken_relation_id=broken_relation_id, + ) + else: + ops_logger.warning( + "It looks like this charm is using an older `ops` version. " + "You may experience weirdness. Please update ops.", + ) + model = ops.model.Model(meta, model_backend) + + charm_state_path = charm_dir / CHARM_STATE_FILE + + # TODO: add use_juju_for_storage support + store = ops.storage.SQLiteStorage(charm_state_path) + framework = ops.Framework(store, charm_dir, meta, model) + framework.set_breakpointhook() + return framework + + +def setup_charm( + charm_class: Type[ops.CharmBase], framework: ops.Framework, dispatcher: _Dispatcher +): + sig = inspect.signature(charm_class) + sig.bind(framework) # signature check + + charm = charm_class(framework) + dispatcher.ensure_event_links(charm) + return charm + + +def setup( + state: "State", + event: "_Event", + context: "Context[CharmType]", + charm_spec: "_CharmSpec[CharmType]", + juju_context: Optional[ops.jujucontext._JujuContext] = None, +): + """Setup dispatcher, framework and charm objects.""" + charm_class = charm_spec.charm_type + if juju_context is None: + juju_context = ops.jujucontext._JujuContext.from_dict(os.environ) + charm_dir = juju_context.charm_dir + + dispatcher = _Dispatcher(charm_dir, juju_context) + dispatcher.run_any_legacy_hook() + + framework = setup_framework( + charm_dir, state, event, context, charm_spec, juju_context + ) + charm = setup_charm(charm_class, framework, dispatcher) + return dispatcher, framework, charm + + +class Ops(Generic[CharmType]): + """Class to manage stepping through ops setup, event emission and framework commit.""" + + def __init__( + self, + state: "State", + event: "_Event", + context: "Context[CharmType]", + charm_spec: "_CharmSpec[CharmType]", + juju_context: Optional[ops.jujucontext._JujuContext] = None, + ): + self.state = state + self.event = event + self.context = context + self.charm_spec = charm_spec + if juju_context is None: + juju_context = ops.jujucontext._JujuContext.from_dict(os.environ) + self.juju_context = juju_context + + # set by setup() + self.dispatcher: Optional[_Dispatcher] = None + self.framework: Optional[ops.Framework] = None + self.charm: Optional["CharmType"] = None + + self._has_setup = False + self._has_emitted = False + self._has_committed = False + + def setup(self): + """Setup framework, charm and dispatcher.""" + self._has_setup = True + self.dispatcher, self.framework, self.charm = setup( + self.state, + self.event, + self.context, + self.charm_spec, + self.juju_context, + ) + + def emit(self): + """Emit the event on the charm.""" + if not self._has_setup: + raise RuntimeError("should .setup() before you .emit()") + self._has_emitted = True + + dispatcher = cast(_Dispatcher, self.dispatcher) + charm = cast(CharmBase, self.charm) + framework = cast(ops.Framework, self.framework) + + try: + if not dispatcher.is_restricted_context(): + framework.reemit() + + _emit_charm_event( + charm, dispatcher.event_name, self.juju_context, self.event + ) + + except Exception: + framework.close() + raise + + def commit(self): + """Commit the framework and teardown.""" + if not self._has_emitted: + raise RuntimeError("should .emit() before you .commit()") + + framework = cast(ops.Framework, self.framework) + charm = cast(CharmBase, self.charm) + + # emit collect-status events + ops.charm._evaluate_status(charm) + + self._has_committed = True + + try: + framework.commit() + finally: + framework.close() + + def finalize(self): + """Step through all non-manually-called procedures and run them.""" + if not self._has_setup: + self.setup() + if not self._has_emitted: + self.emit() + if not self._has_committed: + self.commit() diff --git a/testing/build/lib/scenario/py.typed b/testing/build/lib/scenario/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/testing/build/lib/scenario/runtime.py b/testing/build/lib/scenario/runtime.py new file mode 100644 index 000000000..3ad2fd0a2 --- /dev/null +++ b/testing/build/lib/scenario/runtime.py @@ -0,0 +1,577 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Test framework runtime.""" + +import copy +import dataclasses +import marshal +import re +import tempfile +import typing +from contextlib import contextmanager +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + Dict, + FrozenSet, + List, + Optional, + Set, + Type, + TypeVar, + Union, +) + +import yaml +from ops import ( + CollectStatusEvent, + pebble, + CommitEvent, + EventBase, + Framework, + Handle, + NoTypeError, + PreCommitEvent, +) +from ops.jujucontext import _JujuContext +from ops.storage import NoSnapshotError, SQLiteStorage +from ops.framework import _event_regex +from ops._private.harness import ActionFailed + +from .errors import NoObserverError, UncaughtCharmError +from .logger import logger as scenario_logger +from .state import ( + DeferredEvent, + PeerRelation, + Relation, + StoredState, + SubordinateRelation, +) + +if TYPE_CHECKING: # pragma: no cover + from .context import Context + from .state import CharmType, State, _CharmSpec, _Event + +logger = scenario_logger.getChild("runtime") +STORED_STATE_REGEX = re.compile( + r"((?P<owner_path>.*)\/)?(?P<_data_type_name>\D+)\[(?P<name>.*)\]", +) +EVENT_REGEX = re.compile(_event_regex) + +RUNTIME_MODULE = Path(__file__).parent + + +class UnitStateDB: + """Represents the unit-state.db.""" + + def __init__(self, db_path: Union[Path, str]): + self._db_path = db_path + self._state_file = Path(self._db_path) + + def _open_db(self) -> SQLiteStorage: + """Open the db.""" + return SQLiteStorage(self._state_file) + + def get_stored_states(self) -> FrozenSet["StoredState"]: + """Load any StoredState data structures from the db.""" + + db = self._open_db() + + stored_states: Set[StoredState] = set() + for handle_path in db.list_snapshots(): + if not EVENT_REGEX.match(handle_path) and ( + match := STORED_STATE_REGEX.match(handle_path) + ): + stored_state_snapshot = db.load_snapshot(handle_path) + kwargs = match.groupdict() + sst = StoredState(content=stored_state_snapshot, **kwargs) + stored_states.add(sst) + + db.close() + return frozenset(stored_states) + + def get_deferred_events(self) -> List["DeferredEvent"]: + """Load any DeferredEvent data structures from the db.""" + + db = self._open_db() + + deferred: List[DeferredEvent] = [] + for handle_path in db.list_snapshots(): + if EVENT_REGEX.match(handle_path): + notices = db.notices(handle_path) + for handle, owner, observer in notices: + try: + snapshot_data = db.load_snapshot(handle) + except NoSnapshotError: + snapshot_data: Dict[str, Any] = {} + + event = DeferredEvent( + handle_path=handle, + owner=owner, + observer=observer, + snapshot_data=snapshot_data, + ) + deferred.append(event) + + db.close() + return deferred + + def apply_state(self, state: "State"): + """Add DeferredEvent and StoredState from this State instance to the storage.""" + db = self._open_db() + for event in state.deferred: + db.save_notice(event.handle_path, event.owner, event.observer) + try: + marshal.dumps(event.snapshot_data) + except ValueError as e: + raise ValueError( + f"unable to save the data for {event}, it must contain only simple types.", + ) from e + db.save_snapshot(event.handle_path, event.snapshot_data) + + for stored_state in state.stored_states: + db.save_snapshot(stored_state._handle_path, stored_state.content) + + db.close() + + +class _OpsMainContext: # type: ignore + """Context manager representing ops.main execution context. + + When entered, ops.main sets up everything up until the charm. + When .emit() is called, ops.main proceeds with emitting the event. + When exited, if .emit has not been called manually, it is called automatically. + """ + + def __init__(self): + self._has_emitted = False + + def __enter__(self): + pass + + def emit(self): + """Emit the event. + + Within the test framework, this only requires recording that it was emitted. + """ + self._has_emitted = True + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any): # noqa: U100 + if not self._has_emitted: + self.emit() + + +class Runtime: + """Charm runtime wrapper. + + This object bridges a local environment and a charm artifact. + """ + + def __init__( + self, + charm_spec: "_CharmSpec[CharmType]", + charm_root: Optional[Union[str, Path]] = None, + juju_version: str = "3.0.0", + app_name: Optional[str] = None, + unit_id: Optional[int] = 0, + ): + self._charm_spec = charm_spec + self._juju_version = juju_version + self._charm_root = charm_root + + app_name = app_name or self._charm_spec.meta.get("name") + if not app_name: + raise ValueError('invalid metadata: mandatory "name" field is missing.') + + self._app_name = app_name + self._unit_id = unit_id + + def _get_event_env(self, state: "State", event: "_Event", charm_root: Path): + """Build the simulated environment the operator framework expects.""" + env = { + "JUJU_VERSION": self._juju_version, + "JUJU_UNIT_NAME": f"{self._app_name}/{self._unit_id}", + "_": "./dispatch", + "JUJU_DISPATCH_PATH": f"hooks/{event.name}", + "JUJU_MODEL_NAME": state.model.name, + "JUJU_MODEL_UUID": state.model.uuid, + "JUJU_CHARM_DIR": str(charm_root.absolute()), + } + + if event._is_action_event and (action := event.action): + env.update( + { + "JUJU_ACTION_NAME": action.name.replace("_", "-"), + "JUJU_ACTION_UUID": action.id, + }, + ) + + if event._is_relation_event and (relation := event.relation): + if isinstance(relation, PeerRelation): + remote_app_name = self._app_name + elif isinstance(relation, (Relation, SubordinateRelation)): + remote_app_name = relation.remote_app_name + else: + raise ValueError(f"Unknown relation type: {relation}") + env.update( + { + "JUJU_RELATION": relation.endpoint, + "JUJU_RELATION_ID": str(relation.id), + "JUJU_REMOTE_APP": remote_app_name, + }, + ) + + remote_unit_id = event.relation_remote_unit_id + + # don't check truthiness because remote_unit_id could be 0 + if remote_unit_id is None and not event.name.endswith( + ("_relation_created", "relation_broken"), + ): + remote_unit_ids = relation._remote_unit_ids + + if len(remote_unit_ids) == 1: + remote_unit_id = remote_unit_ids[0] + logger.info( + "there's only one remote unit, so we set JUJU_REMOTE_UNIT to it, " + "but you probably should be parametrizing the event with `remote_unit_id` " + "to be explicit.", + ) + elif len(remote_unit_ids) > 1: + remote_unit_id = remote_unit_ids[0] + logger.warning( + "remote unit ID unset, and multiple remote unit IDs are present; " + "We will pick the first one and hope for the best. You should be passing " + "`remote_unit_id` to the Event constructor.", + ) + else: + logger.warning( + "remote unit ID unset; no remote unit data present. " + "Is this a realistic scenario?", + ) + + if remote_unit_id is not None: + remote_unit = f"{remote_app_name}/{remote_unit_id}" + env["JUJU_REMOTE_UNIT"] = remote_unit + if event.name.endswith("_relation_departed"): + if event.relation_departed_unit_id: + env["JUJU_DEPARTING_UNIT"] = ( + f"{remote_app_name}/{event.relation_departed_unit_id}" + ) + else: + env["JUJU_DEPARTING_UNIT"] = remote_unit + + if container := event.container: + env.update({"JUJU_WORKLOAD_NAME": container.name}) + + if notice := event.notice: + if hasattr(notice.type, "value"): + notice_type = typing.cast(pebble.NoticeType, notice.type).value + else: + notice_type = str(notice.type) + env.update( + { + "JUJU_NOTICE_ID": notice.id, + "JUJU_NOTICE_TYPE": notice_type, + "JUJU_NOTICE_KEY": notice.key, + }, + ) + + if check_info := event.check_info: + env["JUJU_PEBBLE_CHECK_NAME"] = check_info.name + + if storage := event.storage: + env.update({"JUJU_STORAGE_ID": f"{storage.name}/{storage.index}"}) + + if secret := event.secret: + env.update( + { + "JUJU_SECRET_ID": secret.id, + "JUJU_SECRET_LABEL": secret.label or "", + }, + ) + # Don't check truthiness because revision could be 0. + if event.secret_revision is not None: + env["JUJU_SECRET_REVISION"] = str(event.secret_revision) + + return env + + @staticmethod + def _wrap(charm_type: Type["CharmType"]) -> Type["CharmType"]: + # dark sorcery to work around framework using class attrs to hold on to event sources + # this should only be needed if we call play multiple times on the same runtime. + class WrappedEvents(charm_type.on.__class__): + """The charm's event sources, but wrapped.""" + + pass + + WrappedEvents.__name__ = charm_type.on.__class__.__name__ + + class WrappedCharm(charm_type): + """The test charm's type, but with events wrapped.""" + + on = WrappedEvents() + + WrappedCharm.__name__ = charm_type.__name__ + return typing.cast(Type["CharmType"], WrappedCharm) + + @contextmanager + def _virtual_charm_root(self): + # If we are using runtime on a real charm, we can make some assumptions about the + # directory structure we are going to find. + # If we're, say, dynamically defining charm types and doing tests on them, we'll have to + # generate the metadata files ourselves. To be sure, we ALWAYS use a tempdir. Ground truth + # is what the user passed via the CharmSpec + spec = self._charm_spec + + if charm_virtual_root := self._charm_root: + charm_virtual_root_is_custom = True + virtual_charm_root = Path(charm_virtual_root) + else: + charm_virtual_root = tempfile.TemporaryDirectory() + virtual_charm_root = Path(charm_virtual_root.name) + charm_virtual_root_is_custom = False + + metadata_yaml = virtual_charm_root / "metadata.yaml" + config_yaml = virtual_charm_root / "config.yaml" + actions_yaml = virtual_charm_root / "actions.yaml" + + metadata_files_present: Dict[Path, Optional[str]] = { + file: file.read_text() if file.exists() else None + for file in (metadata_yaml, config_yaml, actions_yaml) + } + + any_metadata_files_present_in_charm_virtual_root = any( + v is not None for v in metadata_files_present.values() + ) + + if spec.is_autoloaded and charm_virtual_root_is_custom: + # since the spec is autoloaded, in theory the metadata contents won't differ, so we can + # overwrite away even if the custom vroot is the real charm root (the local repo). + # Still, log it for clarity. + if any_metadata_files_present_in_charm_virtual_root: + logger.debug( + f"metadata files found in custom charm_root {charm_virtual_root}. " + f"The spec was autoloaded so the contents should be identical. " + f"Proceeding...", + ) + + elif ( + not spec.is_autoloaded and any_metadata_files_present_in_charm_virtual_root + ): + logger.warning( + f"Some metadata files found in custom user-provided charm_root " + f"{charm_virtual_root} while you have passed meta, config or actions to " + f"Context.run(). " + "Single source of truth are the arguments passed to Context.run(). " + "charm_root metadata files will be overwritten for the " + "duration of this test, and restored afterwards. " + "To avoid this, clean any metadata files from the charm_root before calling run.", + ) + + metadata_yaml.write_text(yaml.safe_dump(spec.meta)) + config_yaml.write_text(yaml.safe_dump(spec.config or {})) + actions_yaml.write_text(yaml.safe_dump(spec.actions or {})) + + yield virtual_charm_root + + if charm_virtual_root_is_custom: + for file, previous_content in metadata_files_present.items(): + if previous_content is None: # None == file did not exist before + file.unlink() + else: + file.write_text(previous_content) + + else: + # charm_virtual_root is a tempdir + typing.cast(tempfile.TemporaryDirectory, charm_virtual_root).cleanup() # type: ignore + + @staticmethod + def _get_state_db(temporary_charm_root: Path): + charm_state_path = temporary_charm_root / ".unit-state.db" + return UnitStateDB(charm_state_path) + + def _initialize_storage(self, state: "State", temporary_charm_root: Path): + """Before we start processing this event, store the relevant parts of State.""" + store = self._get_state_db(temporary_charm_root) + store.apply_state(state) + + def _close_storage(self, state: "State", temporary_charm_root: Path): + """Now that we're done processing this event, read the charm state and expose it.""" + store = self._get_state_db(temporary_charm_root) + deferred = store.get_deferred_events() + stored_state = store.get_stored_states() + return dataclasses.replace(state, deferred=deferred, stored_states=stored_state) + + @contextmanager + def _exec_ctx(self, ctx: "Context"): + """python 3.8 compatibility shim""" + with self._virtual_charm_root() as temporary_charm_root: + with _capture_events( + include_deferred=ctx.capture_deferred_events, + include_framework=ctx.capture_framework_events, + ) as captured: + yield (temporary_charm_root, captured) + + @contextmanager + def exec( + self, + state: "State", + event: "_Event", + context: "Context", + ): + """Runs an event with this state as initial state on a charm. + + Returns the 'output state', that is, the state as mutated by the charm during the + event handling. + + This will set the environment up and call ops.main(). + After that it's up to ops. + """ + from ._consistency_checker import check_consistency # avoid cycles + + check_consistency(state, event, self._charm_spec, self._juju_version) + + charm_type = self._charm_spec.charm_type + logger.info(f"Preparing to fire {event.name} on {charm_type.__name__}") + + # we make a copy to avoid mutating the input state + output_state = copy.deepcopy(state) + + logger.info(" - generating virtual charm root") + with self._exec_ctx(context) as (temporary_charm_root, captured): + logger.info(" - initializing storage") + self._initialize_storage(state, temporary_charm_root) + + logger.info(" - preparing env") + env = self._get_event_env( + state=state, + event=event, + charm_root=temporary_charm_root, + ) + juju_context = _JujuContext.from_dict(env) + + logger.info(" - Entering ops.main (mocked).") + from .ops_main_mock import Ops # noqa: F811 + + try: + ops = Ops( + state=output_state, + event=event, + context=context, + charm_spec=dataclasses.replace( + self._charm_spec, + charm_type=self._wrap(charm_type), + ), + juju_context=juju_context, + ) + ops.setup() + + yield ops + + # if the caller did not manually emit or commit: do that. + ops.finalize() + + except (NoObserverError, ActionFailed): + raise # propagate along + except Exception as e: + raise UncaughtCharmError( + f"Uncaught exception ({type(e)}) in operator/charm code: {e!r}", + ) from e + + finally: + logger.info(" - Exited ops.main.") + + logger.info(" - closing storage") + output_state = self._close_storage(output_state, temporary_charm_root) + + context.emitted_events.extend(captured) + logger.info("event dispatched. done.") + context._set_output_state(output_state) + + +_T = TypeVar("_T", bound=EventBase) + + +@contextmanager +def _capture_events( + *types: Type[EventBase], + include_framework: bool = False, + include_deferred: bool = True, +): + """Capture all events of type `*types` (using instance checks). + + Arguments exposed so that you can define your own fixtures if you want to. + + Example:: + >>> from ops import StartEvent + >>> from scenario import Event, State + >>> from charm import MyCustomEvent, MyCharm # noqa + >>> + >>> def test_my_event(): + >>> with capture_events(StartEvent, MyCustomEvent) as captured: + >>> trigger(State(), ("start", MyCharm, meta=MyCharm.META) + >>> + >>> assert len(captured) == 2 + >>> e1, e2 = captured + >>> assert isinstance(e2, MyCustomEvent) + >>> assert e2.custom_attr == 'foo' + """ + allowed_types = types or (EventBase,) + + captured: List[EventBase] = [] + _real_emit = Framework._emit + _real_reemit = Framework.reemit + + def _wrapped_emit(self: Framework, evt: EventBase): + if not include_framework and isinstance( + evt, + (PreCommitEvent, CommitEvent, CollectStatusEvent), + ): + return _real_emit(self, evt) + + if isinstance(evt, allowed_types): + # dump/undump the event to ensure any custom attributes are (re)set by restore() + evt.restore(evt.snapshot()) + captured.append(evt) + + return _real_emit(self, evt) + + def _wrapped_reemit(self: Framework): + # Framework calls reemit() before emitting the main juju event. We intercept that call + # and capture all events in storage. + + if not include_deferred: + return _real_reemit(self) + + # load all notices from storage as events. + for event_path, _, _ in self._storage.notices(): + event_handle = Handle.from_path(event_path) + try: + event = self.load_snapshot(event_handle) + except NoTypeError: + continue + event = typing.cast(EventBase, event) + event.deferred = False + self._forget(event) # prevent tracking conflicts + + if not include_framework and isinstance( + event, + (PreCommitEvent, CommitEvent), + ): + continue + + if isinstance(event, allowed_types): + captured.append(event) + + return _real_reemit(self) + + Framework._emit = _wrapped_emit # type: ignore + Framework.reemit = _wrapped_reemit + + yield captured + + Framework._emit = _real_emit + Framework.reemit = _real_reemit diff --git a/testing/build/lib/scenario/state.py b/testing/build/lib/scenario/state.py new file mode 100644 index 000000000..d7e24bffc --- /dev/null +++ b/testing/build/lib/scenario/state.py @@ -0,0 +1,2030 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""The core State object, and the components inside it.""" + +from __future__ import annotations + +import dataclasses +import datetime +import inspect +import pathlib +import random +import re +import string +from enum import Enum +from itertools import chain +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Dict, + Final, + Generic, + Iterable, + List, + Literal, + Mapping, + NoReturn, + Sequence, + TypeVar, + Union, + cast, +) +from uuid import uuid4 + +import yaml + +import ops +from ops import pebble, CharmBase, CharmEvents, SecretRotate, StatusBase +from ops import CloudCredential as CloudCredential_Ops +from ops import CloudSpec as CloudSpec_Ops + +from .errors import MetadataNotFoundError, StateValidationError +from .logger import logger as scenario_logger + +if TYPE_CHECKING: # pragma: no cover + from . import Context + +AnyJson = Union[str, bool, Dict[str, "AnyJson"], int, float, List["AnyJson"]] +RawSecretRevisionContents = RawDataBagContents = Dict[str, str] +UnitID = int + +CharmType = TypeVar("CharmType", bound=CharmBase) + +logger = scenario_logger.getChild("state") + +ATTACH_ALL_STORAGES = "ATTACH_ALL_STORAGES" +CREATE_ALL_RELATIONS = "CREATE_ALL_RELATIONS" +BREAK_ALL_RELATIONS = "BREAK_ALL_RELATIONS" +DETACH_ALL_STORAGES = "DETACH_ALL_STORAGES" + +_ACTION_EVENT_SUFFIX = "_action" +# all builtin events except secret events. They're special because they carry secret metadata. +_BUILTIN_EVENTS = { + "start", + "stop", + "install", + "install", + "start", + "stop", + "remove", + "update_status", + "config_changed", + "upgrade_charm", + "pre_series_upgrade", + "post_series_upgrade", + "leader_elected", + "leader_settings_changed", + "collect_metrics", +} +_FRAMEWORK_EVENTS = { + "pre_commit", + "commit", + "collect_app_status", + "collect_unit_status", +} +_PEBBLE_READY_EVENT_SUFFIX = "_pebble_ready" +_PEBBLE_CUSTOM_NOTICE_EVENT_SUFFIX = "_pebble_custom_notice" +_PEBBLE_CHECK_FAILED_EVENT_SUFFIX = "_pebble_check_failed" +_PEBBLE_CHECK_RECOVERED_EVENT_SUFFIX = "_pebble_check_recovered" +_RELATION_EVENTS_SUFFIX = { + "_relation_changed", + "_relation_broken", + "_relation_joined", + "_relation_departed", + "_relation_created", +} +_STORAGE_EVENTS_SUFFIX = { + "_storage_detaching", + "_storage_attached", +} + +_SECRET_EVENTS = { + "secret_changed", + "secret_remove", + "secret_rotate", + "secret_expired", +} + + +# This can be replaced with the KW_ONLY dataclasses functionality in Python 3.10+. +def _max_posargs(n: int): + class _MaxPositionalArgs: + """Raises TypeError when instantiating objects if arguments are not passed as keywords. + + Looks for a `_max_positional_args` class attribute, which should be an int + indicating the maximum number of positional arguments that can be passed to + `__init__` (excluding `self`). + """ + + _max_positional_args = n + + def __new__(cls, *args: Any, **kwargs: Any): + # inspect.signature guarantees the order of parameters is as + # declared, which aligns with dataclasses. Simpler ways of + # getting the arguments (like __annotations__) do not have that + # guarantee, although in practice it is the case. + parameters = inspect.signature(cls.__init__).parameters + required_args = [ + name + for name in tuple(parameters) + if parameters[name].default is inspect.Parameter.empty + and name not in kwargs + and name != "self" + ] + n_posargs = len(args) + max_n_posargs = cls._max_positional_args + kw_only = { + name + for name in tuple(parameters)[max_n_posargs:] + if not name.startswith("_") + } + if n_posargs > max_n_posargs: + raise TypeError( + f"{cls.__name__} takes {max_n_posargs} positional " + f"argument{'' if max_n_posargs == 1 else 's'} but " + f"{n_posargs} {'was' if n_posargs == 1 else 'were'} " + f"given. The following arguments are keyword-only: " + f"{', '.join(kw_only)}", + ) from None + # Also check if there are just not enough arguments at all, because + # the default TypeError message will incorrectly describe some of + # the arguments as positional. + if n_posargs < len(required_args): + required_pos = [ + f"'{arg}'" + for arg in required_args[n_posargs:] + if arg not in kw_only + ] + required_kw = { + f"'{arg}'" for arg in required_args[n_posargs:] if arg in kw_only + } + if required_pos and required_kw: + details = f"positional: {', '.join(required_pos)} and keyword: {', '.join(required_kw)} arguments" + elif required_pos: + details = f"positional argument{'' if len(required_pos) == 1 else 's'}: {', '.join(required_pos)}" + else: + details = f"keyword argument{'' if len(required_kw) == 1 else 's'}: {', '.join(required_kw)}" + raise TypeError(f"{cls.__name__} missing required {details}") from None + return super().__new__(cls) + + def __reduce__(self): + # The default __reduce__ doesn't understand that some arguments have + # to be passed as keywords, so using the copy module fails. + attrs = cast(Dict[str, Any], super().__reduce__()[2]) + return (lambda: self.__class__(**attrs), ()) + + return _MaxPositionalArgs + + +@dataclasses.dataclass(frozen=True) +class JujuLogLine(_max_posargs(2)): + """An entry in the Juju debug-log.""" + + level: str + """The level of the message, for example ``INFO`` or ``ERROR``.""" + message: str + """The log message.""" + + +@dataclasses.dataclass(frozen=True) +class CloudCredential(_max_posargs(0)): + __doc__ = ops.CloudCredential.__doc__ + + auth_type: str + """Authentication type.""" + + attributes: dict[str, str] = dataclasses.field(default_factory=dict) + """A dictionary containing cloud credentials. + For example, for AWS, it contains `access-key` and `secret-key`; + for Azure, `application-id`, `application-password` and `subscription-id` + can be found here. + """ + + redacted: list[str] = dataclasses.field(default_factory=list) + """A list of redacted generic cloud API secrets.""" + + def _to_ops(self) -> CloudCredential_Ops: + return CloudCredential_Ops( + auth_type=self.auth_type, + attributes=self.attributes, + redacted=self.redacted, + ) + + +@dataclasses.dataclass(frozen=True) +class CloudSpec(_max_posargs(1)): + __doc__ = ops.CloudSpec.__doc__ + + type: str + """Type of the cloud.""" + + name: str = "localhost" + """Juju cloud name.""" + + region: str | None = None + """Region of the cloud.""" + + endpoint: str | None = None + """Endpoint of the cloud.""" + + identity_endpoint: str | None = None + """Identity endpoint of the cloud.""" + + storage_endpoint: str | None = None + """Storage endpoint of the cloud.""" + + credential: CloudCredential | None = None + """Cloud credentials with key-value attributes.""" + + ca_certificates: list[str] = dataclasses.field(default_factory=list) + """A list of CA certificates.""" + + skip_tls_verify: bool = False + """Whether to skip TLS verification.""" + + is_controller_cloud: bool = False + """If this is the cloud used by the controller.""" + + def _to_ops(self) -> CloudSpec_Ops: + return CloudSpec_Ops( + type=self.type, + name=self.name, + region=self.region, + endpoint=self.endpoint, + identity_endpoint=self.identity_endpoint, + storage_endpoint=self.storage_endpoint, + credential=self.credential._to_ops() if self.credential else None, + ca_certificates=self.ca_certificates, + skip_tls_verify=self.skip_tls_verify, + is_controller_cloud=self.is_controller_cloud, + ) + + +def _generate_secret_id(): + # This doesn't account for collisions, but the odds are so low that it + # should not be possible in any realistic test run. + secret_id = "".join( + random.choice(string.ascii_lowercase + string.digits) for _ in range(20) + ) + return f"secret:{secret_id}" + + +@dataclasses.dataclass(frozen=True) +class Secret(_max_posargs(1)): + """A Juju secret. + + This class is used for both user and charm secrets. + """ + + tracked_content: RawSecretRevisionContents + """The content of the secret that the charm is currently tracking. + + This is the content the charm will receive with a + :meth:`ops.Secret.get_content` call.""" + latest_content: RawSecretRevisionContents | None = None + """The content of the latest revision of the secret. + + This is the content the charm will receive with a + :meth:`ops.Secret.peek_content` call.""" + + id: str = dataclasses.field(default_factory=_generate_secret_id) + """The Juju ID of the secret. + + This is automatically assigned and should not usually need to be explicitly set. + """ + + owner: Literal["unit", "app", None] = None + """Indicates if the secret is owned by *this* unit, *this* application, or + another application/unit. + + If None, the implication is that read access to the secret has been granted + to this unit. + """ + + remote_grants: dict[int, set[str]] = dataclasses.field(default_factory=dict) + """Mapping from relation IDs to remote units and applications to which this + secret has been granted.""" + + label: str | None = None + """A human-readable label the charm can use to retrieve the secret. + + If this is set, it implies that the charm has previously set the label. + """ + description: str | None = None + """A human-readable description of the secret.""" + expire: datetime.datetime | None = None + """The time at which the secret will expire.""" + rotate: SecretRotate | None = None + """The rotation policy for the secret.""" + + # what revision is currently tracked by this charm. Only meaningful if owner=False + _tracked_revision: int = 1 + + # what revision is the latest for this secret. + _latest_revision: int = 1 + + def __hash__(self) -> int: + return hash(self.id) + + def __post_init__(self): + if self.latest_content is None: + # bypass frozen dataclass + object.__setattr__(self, "latest_content", self.tracked_content) + + def _set_label(self, label: str): + # bypass frozen dataclass + object.__setattr__(self, "label", label) + + def _track_latest_revision(self): + """Set the current revision to the tracked revision.""" + # bypass frozen dataclass + object.__setattr__(self, "_tracked_revision", self._latest_revision) + object.__setattr__(self, "tracked_content", self.latest_content) + + def _update_metadata( + self, + content: RawSecretRevisionContents | None = None, + label: str | None = None, + description: str | None = None, + expire: datetime.datetime | None = None, + rotate: SecretRotate | None = None, + ): + """Update the metadata.""" + # bypass frozen dataclass + object.__setattr__(self, "_latest_revision", self._latest_revision + 1) + if content: + object.__setattr__(self, "latest_content", content) + if label: + object.__setattr__(self, "label", label) + if description: + object.__setattr__(self, "description", description) + if expire: + if isinstance(expire, datetime.timedelta): + expire = datetime.datetime.now() + expire + object.__setattr__(self, "expire", expire) + if rotate: + object.__setattr__(self, "rotate", rotate) + + +def _normalise_name(s: str): + """Event names, in Scenario, uniformly use underscores instead of dashes.""" + return s.replace("-", "_") + + +@dataclasses.dataclass(frozen=True) +class Address(_max_posargs(1)): + """An address in a Juju network space.""" + + value: str + """The IP address in the space.""" + hostname: str = "" + """A host name that maps to the address in :attr:`value`.""" + cidr: str = "" + """The CIDR of the address in :attr:`value`.""" + + @property + def address(self): + """A deprecated alias for :attr:`value`.""" + return self.value + + @address.setter + def address(self, value: str): + object.__setattr__(self, "value", value) + + +@dataclasses.dataclass(frozen=True) +class BindAddress(_max_posargs(1)): + """An address bound to a network interface in a Juju space.""" + + addresses: list[Address] + """The addresses in the space.""" + interface_name: str = "" + """The name of the network interface.""" + mac_address: str | None = None + """The MAC address of the interface.""" + + def _hook_tool_output_fmt(self): + """Dumps itself to dict in the same format the hook tool would.""" + dct = { + "interface-name": self.interface_name, + "addresses": [dataclasses.asdict(addr) for addr in self.addresses], + } + if self.mac_address: + dct["mac-address"] = self.mac_address + return dct + + +@dataclasses.dataclass(frozen=True) +class Network(_max_posargs(2)): + """A Juju network space.""" + + binding_name: str + """The name of the network space.""" + bind_addresses: list[BindAddress] = dataclasses.field( + default_factory=lambda: [BindAddress([Address("192.0.2.0")])], + ) + """Addresses that the charm's application should bind to.""" + ingress_addresses: list[str] = dataclasses.field( + default_factory=lambda: ["192.0.2.0"], + ) + """Addresses other applications should use to connect to the unit.""" + egress_subnets: list[str] = dataclasses.field( + default_factory=lambda: ["192.0.2.0/24"], + ) + """Subnets that other units will see the charm connecting from.""" + + def __hash__(self) -> int: + return hash(self.binding_name) + + def _hook_tool_output_fmt(self): + # dumps itself to dict in the same format the hook tool would + return { + "bind-addresses": [ + ba._hook_tool_output_fmt() for ba in self.bind_addresses + ], + "egress-subnets": self.egress_subnets, + "ingress-addresses": self.ingress_addresses, + } + + +_next_relation_id_counter = 1 + + +def _next_relation_id(*, update: bool = True): + """Get the ID the next relation to be created will get. + + Pass update=False if you're only inspecting it. + Pass update=True if you also want to bump it. + """ + global _next_relation_id_counter + cur = _next_relation_id_counter + if update: + _next_relation_id_counter += 1 + return cur + + +@dataclasses.dataclass(frozen=True) +class RelationBase(_max_posargs(2)): + """Base class for the various types of integration (relation).""" + + endpoint: str + """Relation endpoint name. Must match some endpoint name defined in the metadata.""" + + interface: str | None = None + """Interface name. Must match the interface name attached to this endpoint in the metadata. + If left empty, it will be automatically derived from the metadata.""" + + id: int = dataclasses.field(default_factory=_next_relation_id) + """Juju relation ID. Every new Relation instance gets a unique one, + if there's trouble, override.""" + + local_app_data: RawDataBagContents = dataclasses.field(default_factory=dict) + """This application's databag for this relation.""" + + local_unit_data: RawDataBagContents = dataclasses.field( + default_factory=lambda: _DEFAULT_JUJU_DATABAG.copy(), + ) + """This unit's databag for this relation.""" + + @property + def relation_id(self) -> NoReturn: + """Use `.id` instead of `.relation_id`. + + :private: + """ + raise AttributeError("use .id instead of .relation_id") + + @property + def _databags(self): + """Yield all databags in this relation.""" + yield self.local_app_data + yield self.local_unit_data + + @property + def _remote_unit_ids(self) -> tuple[UnitID, ...]: + """Ids of the units on the other end of this relation.""" + raise NotImplementedError() + + def _get_databag_for_remote( + self, + unit_id: int, # noqa: U100 + ) -> RawDataBagContents: + """Return the databag for some remote unit ID.""" + raise NotImplementedError() + + def __post_init__(self): + if type(self) is RelationBase: + raise RuntimeError( + "RelationBase cannot be instantiated directly; " + "please use Relation, PeerRelation, or SubordinateRelation", + ) + + for databag in self._databags: + self._validate_databag(databag) + + def __hash__(self) -> int: + return hash(self.id) + + def _validate_databag(self, databag: dict[str, str]): + if not isinstance(databag, dict): + raise StateValidationError( + f"all databags should be dicts, not {type(databag)}", + ) + for v in databag.values(): + if not isinstance(v, str): + raise StateValidationError( + f"all databags should be Dict[str,str]; " + f"found a value of type {type(v)}", + ) + + +_DEFAULT_IP = "192.0.2.0" +_DEFAULT_JUJU_DATABAG = { + "egress-subnets": _DEFAULT_IP, + "ingress-address": _DEFAULT_IP, + "private-address": _DEFAULT_IP, +} + + +@dataclasses.dataclass(frozen=True) +class Relation(RelationBase): + """An integration between the charm and another application.""" + + remote_app_name: str = "remote" + """The name of the remote application, as in the charm's metadata.""" + + # local limit + limit: int = 1 + """The maximum number of integrations on this endpoint.""" + + remote_app_data: RawDataBagContents = dataclasses.field(default_factory=dict) + """The current content of the application databag.""" + remote_units_data: dict[UnitID, RawDataBagContents] = dataclasses.field( + default_factory=lambda: {0: _DEFAULT_JUJU_DATABAG.copy()}, # dedup + ) + """The current content of the databag for each unit in the relation.""" + + def __hash__(self) -> int: + return hash(self.id) + + @property + def _remote_app_name(self) -> str: + """Who is on the other end of this relation?""" + return self.remote_app_name + + @property + def _remote_unit_ids(self) -> tuple[UnitID, ...]: + """Ids of the units on the other end of this relation.""" + return tuple(self.remote_units_data) + + def _get_databag_for_remote(self, unit_id: UnitID) -> RawDataBagContents: + """Return the databag for some remote unit ID.""" + return self.remote_units_data[unit_id] + + @property + def _databags(self): # type: ignore + """Yield all databags in this relation.""" + yield self.local_app_data + yield self.local_unit_data + yield self.remote_app_data + yield from self.remote_units_data.values() + + +@dataclasses.dataclass(frozen=True) +class SubordinateRelation(RelationBase): + """A relation to share data between a subordinate and a principal charm.""" + + remote_app_data: RawDataBagContents = dataclasses.field(default_factory=dict) + """The current content of the remote application databag.""" + remote_unit_data: RawDataBagContents = dataclasses.field( + default_factory=lambda: _DEFAULT_JUJU_DATABAG.copy(), + ) + """The current content of the remote unit databag.""" + + remote_app_name: str = "remote" + """The name of the remote application that *this unit* is attached to.""" + remote_unit_id: int = 0 + """The ID of the remote unit that *this unit* is attached to.""" + + def __hash__(self) -> int: + return hash(self.id) + + @property + def _remote_unit_ids(self) -> tuple[int]: + """Ids of the units on the other end of this relation.""" + return (self.remote_unit_id,) + + def _get_databag_for_remote(self, unit_id: int) -> RawDataBagContents: + """Return the databag for some remote unit ID.""" + if unit_id is not self.remote_unit_id: + raise ValueError( + f"invalid unit id ({unit_id}): subordinate relation only has one " + f"remote and that has id {self.remote_unit_id}", + ) + return self.remote_unit_data + + @property + def _databags(self): + """Yield all databags in this relation.""" + yield self.local_app_data + yield self.local_unit_data + yield self.remote_app_data + yield self.remote_unit_data + + @property + def remote_unit_name(self) -> str: + """The full name of the remote unit, in the form ``remote/0``.""" + return f"{self.remote_app_name}/{self.remote_unit_id}" + + +@dataclasses.dataclass(frozen=True) +class PeerRelation(RelationBase): + """A relation to share data between units of the charm.""" + + peers_data: dict[UnitID, RawDataBagContents] = dataclasses.field( + default_factory=lambda: {0: _DEFAULT_JUJU_DATABAG.copy()}, + ) + """Current contents of the peer databags.""" + # Consistency checks will validate that *this unit*'s ID is not in here. + + def __hash__(self) -> int: + return hash(self.id) + + @property + def _databags(self): # type: ignore + """Yield all databags in this relation.""" + yield self.local_app_data + yield self.local_unit_data + yield from self.peers_data.values() + + @property + def _remote_unit_ids(self) -> tuple[UnitID, ...]: + """Ids of the units on the other end of this relation.""" + return tuple(self.peers_data) + + def _get_databag_for_remote(self, unit_id: UnitID) -> RawDataBagContents: + """Return the databag for some remote unit ID.""" + return self.peers_data[unit_id] + + +def _random_model_name(): + space = string.ascii_letters + string.digits + return "".join(random.choice(space) for _ in range(20)) + + +@dataclasses.dataclass(frozen=True) +class Model(_max_posargs(1)): + """The Juju model in which the charm is deployed.""" + + name: str = dataclasses.field(default_factory=_random_model_name) + """The name of the model.""" + uuid: str = dataclasses.field(default_factory=lambda: str(uuid4())) + """A unique identifier for the model, typically generated by Juju.""" + + # whatever juju models --format=json | jq '.models[<current-model-index>].type' gives back. + type: Literal["kubernetes", "lxd"] = "kubernetes" + """The type of Juju model.""" + + cloud_spec: CloudSpec | None = None + """Cloud specification information (metadata) including credentials.""" + + +_CHANGE_IDS = 0 + + +def _generate_new_change_id(): + global _CHANGE_IDS + _CHANGE_IDS += 1 # type: ignore + logger.info( + f"change ID unset; automatically assigning {_CHANGE_IDS}. " + f"If there are problems, pass one manually.", + ) + return _CHANGE_IDS + + +@dataclasses.dataclass(frozen=True) +class Exec(_max_posargs(1)): + """Mock data for simulated :meth:`ops.Container.exec` calls.""" + + command_prefix: Sequence[str] + return_code: int = 0 + """The return code of the process. + + Use 0 to mock the process ending successfully, and other values for failure. + """ + stdout: str = "" + """Any content written to stdout by the process. + + Provide content that the real process would write to stdout, which can be + read by the charm. + """ + stderr: str = "" + """Any content written to stderr by the process. + + Provide content that the real process would write to stderr, which can be + read by the charm. + """ + + # change ID: used internally to keep track of mocked processes + _change_id: int = dataclasses.field(default_factory=_generate_new_change_id) + + def __post_init__(self): + # The command prefix can be any sequence type, and a list is tidier to + # write when there's only one string. However, this object needs to be + # hashable, so can't contain a list. We 'freeze' the sequence to a tuple + # to support that. + object.__setattr__(self, "command_prefix", tuple(self.command_prefix)) + + def _run(self) -> int: + return self._change_id + + +@dataclasses.dataclass(frozen=True) +class Mount(_max_posargs(0)): + """Maps local files to a :class:`Container` filesystem.""" + + location: str | pathlib.PurePosixPath + """The location inside of the container.""" + source: str | pathlib.Path + """The content to provide when the charm does :meth:`ops.Container.pull`.""" + + +def _now_utc(): + return datetime.datetime.now(tz=datetime.timezone.utc) + + +_next_notice_id_counter = 1 + + +def _next_notice_id(*, update: bool = True): + """Get the ID the next Pebble notice to be created will get. + + Pass update=False if you're only inspecting it. + Pass update=True if you also want to bump it. + """ + global _next_notice_id_counter + cur = _next_notice_id_counter + if update: + _next_notice_id_counter += 1 + return str(cur) + + +@dataclasses.dataclass(frozen=True) +class Notice(_max_posargs(1)): + """A Pebble notice.""" + + key: str + """The notice key, a string that differentiates notices of this type. + + This is in the format ``domain/path``; for example: + ``canonical.com/postgresql/backup`` or ``example.com/mycharm/notice``. + """ + + id: str = dataclasses.field(default_factory=_next_notice_id) + """Unique ID for this notice.""" + + user_id: int | None = None + """UID of the user who may view this notice (None means notice is public).""" + + type: pebble.NoticeType | str = pebble.NoticeType.CUSTOM + """Type of the notice.""" + + first_occurred: datetime.datetime = dataclasses.field(default_factory=_now_utc) + """The first time one of these notices (type and key combination) occurs.""" + + last_occurred: datetime.datetime = dataclasses.field(default_factory=_now_utc) + """The last time one of these notices occurred.""" + + last_repeated: datetime.datetime = dataclasses.field(default_factory=_now_utc) + """The time this notice was last repeated. + + See Pebble's `Notices documentation <https://canonical-pebble.readthedocs-hosted.com/en/latest/reference/notices/>`_ + for an explanation of what "repeated" means. + """ + + occurrences: int = 1 + """The number of times one of these notices has occurred.""" + + last_data: dict[str, str] = dataclasses.field(default_factory=dict) + """Additional data captured from the last occurrence of one of these notices.""" + + repeat_after: datetime.timedelta | None = None + """Minimum time after one of these was last repeated before Pebble will repeat it again.""" + + expire_after: datetime.timedelta | None = None + """How long since one of these last occurred until Pebble will drop the notice.""" + + def _to_ops(self) -> pebble.Notice: + return pebble.Notice( + id=self.id, + user_id=self.user_id, + type=self.type, + key=self.key, + first_occurred=self.first_occurred, + last_occurred=self.last_occurred, + last_repeated=self.last_repeated, + occurrences=self.occurrences, + last_data=self.last_data, + repeat_after=self.repeat_after, + expire_after=self.expire_after, + ) + + +@dataclasses.dataclass(frozen=True) +class CheckInfo(_max_posargs(1)): + """A health check for a Pebble workload container.""" + + name: str + """Name of the check.""" + + level: pebble.CheckLevel | None = None + """Level of the check.""" + + status: pebble.CheckStatus = pebble.CheckStatus.UP + """Status of the check. + + :attr:`ops.pebble.CheckStatus.UP` means the check is healthy (the number of + failures is fewer than the threshold), :attr:`ops.pebble.CheckStatus.DOWN` + means the check is unhealthy (the number of failures has reached the + threshold). + """ + + failures: int = 0 + """Number of failures since the check last succeeded.""" + + threshold: int = 3 + """Failure threshold. + + This is how many consecutive failures for the check to be considered 'down'. + """ + + def _to_ops(self) -> pebble.CheckInfo: + return pebble.CheckInfo( + name=self.name, + level=self.level, + status=self.status, + failures=self.failures, + threshold=self.threshold, + ) + + +@dataclasses.dataclass(frozen=True) +class Container(_max_posargs(1)): + """A Kubernetes container where a charm's workload runs.""" + + name: str + """Name of the container, as found in the charm metadata.""" + + can_connect: bool = False + """When False, all Pebble operations will fail.""" + + # This is the base plan. On top of it, one can add layers. + # We need to model pebble in this way because it's impossible to retrieve the layers from + # pebble or derive them from the resulting plan (which one CAN get from pebble). + # So if we are instantiating Container by fetching info from a 'live' charm, the 'layers' + # will be unknown. all that we can know is the resulting plan (the 'computed plan'). + _base_plan: dict[str, Any] = dataclasses.field(default_factory=dict) + # We expect most of the user-facing testing to be covered by this 'layers' attribute, + # as it is all that will be known when unit-testing. + layers: dict[str, pebble.Layer] = dataclasses.field(default_factory=dict) + """All :class:`ops.pebble.Layer` definitions that have already been added to the container.""" + + service_statuses: dict[str, pebble.ServiceStatus] = dataclasses.field( + default_factory=dict, + ) + """The current status of each Pebble service running in the container.""" + + # this is how you specify the contents of the filesystem: suppose you want to express that your + # container has: + # - /home/foo/bar.py + # - /bin/bash + # - /bin/baz + # + # this becomes: + # mounts = { + # 'foo': Mount(location='/home/foo/', source=Path('/path/to/local/dir/containing/bar/py/')) + # 'bin': Mount(location='/bin/', source=Path('/path/to/local/dir/containing/bash/and/baz/')) + # } + # when the charm runs `pebble.pull`, it will return .open() from one of those paths. + # when the charm pushes, it will either overwrite one of those paths (careful!) or it will + # create a tempfile and insert its path in the mock filesystem tree + mounts: dict[str, Mount] = dataclasses.field(default_factory=dict) + """Provides access to the contents of the simulated container filesystem. + + For example, suppose you want to express that your container has: + + * ``/home/foo/bar.py`` + * ``/bin/bash`` + * ``/bin/baz`` + + this becomes:: + + mounts = { + 'foo': Mount('/home/foo', pathlib.Path('/path/to/local/dir/containing/bar/py/')), + 'bin': Mount('/bin/', pathlib.Path('/path/to/local/dir/containing/bash/and/baz/')), + } + """ + + execs: Iterable[Exec] = frozenset() + """Simulate executing commands in the container. + + Specify each command the charm might run in the container and an :class:`Exec` + containing its return code and any stdout/stderr. + + For example:: + + container = Container( + name='foo', + execs={ + Exec(['whoami'], return_code=0, stdout='ubuntu'), + Exec( + ['dig', '+short', 'canonical.com'], + return_code=0, + stdout='185.125.190.20\\n185.125.190.21', + ), + } + ) + """ + + notices: list[Notice] = dataclasses.field(default_factory=list) + """Any Pebble notices that already exist in the container.""" + + check_infos: frozenset[CheckInfo] = frozenset() + """All Pebble health checks that have been added to the container.""" + + def __hash__(self) -> int: + return hash(self.name) + + def __post_init__(self): + if not isinstance(self.execs, frozenset): + # Allow passing a regular set (or other iterable) of Execs. + object.__setattr__(self, "execs", frozenset(self.execs)) + + def _render_services(self): + # copied over from ops.testing._TestingPebbleClient._render_services() + services: dict[str, pebble.Service] = {} + for key in sorted(self.layers.keys()): + layer = self.layers[key] + for name, service in layer.services.items(): + services[name] = service + return services + + @property + def plan(self) -> pebble.Plan: + """The 'computed' Pebble plan. + + This is the base plan plus the layers that have been added on top. + You should run your assertions on this plan, not so much on the layers, + as those are input data. + """ + + # copied over from ops.testing._TestingPebbleClient.get_plan(). + plan = pebble.Plan(yaml.safe_dump(self._base_plan)) + services = self._render_services() + if not services: + return plan + for name in sorted(services.keys()): + plan.services[name] = services[name] + return plan + + @property + def services(self) -> dict[str, pebble.ServiceInfo]: + """The Pebble services as rendered in the plan.""" + services = self._render_services() + infos: dict[str, pebble.ServiceInfo] = {} + names = sorted(services.keys()) + for name in names: + try: + service = services[name] + except KeyError: + # in pebble, it just returns "nothing matched" if there are 0 matches, + # but it ignores services it doesn't recognize + continue + status = self.service_statuses.get(name, pebble.ServiceStatus.INACTIVE) + if service.startup == "": + startup = pebble.ServiceStartup.DISABLED + else: + startup = pebble.ServiceStartup(service.startup) + info = pebble.ServiceInfo( + name, + startup=startup, + current=pebble.ServiceStatus(status), + ) + infos[name] = info + return infos + + def get_filesystem(self, ctx: Context) -> pathlib.Path: + """Simulated Pebble filesystem in this context. + + Returns: + A temporary filesystem containing any files or directories the + charm pushed to the container. + """ + return ctx._get_container_root(self.name) + + +_RawStatusLiteral = Literal[ + "waiting", + "blocked", + "active", + "unknown", + "error", + "maintenance", +] + + +@dataclasses.dataclass(frozen=True) +class _EntityStatus: + """This class represents StatusBase and should not be interacted with directly.""" + + # Why not use StatusBase directly? Because that can't be used with + # dataclasses.asdict to then be JSON-serializable. + + name: _RawStatusLiteral + message: str = "" + + _entity_statuses: ClassVar[dict[str, type[_EntityStatus]]] = {} + + def __eq__(self, other: Any): + if isinstance(other, (StatusBase, _EntityStatus)): + return (self.name, self.message) == (other.name, other.message) + return super().__eq__(other) + + def __repr__(self): + status_type_name = self.name.title() + "Status" + if self.name == "unknown": + return f"{status_type_name}()" + return f"{status_type_name}('{self.message}')" + + @classmethod + def from_status_name( + cls, + name: _RawStatusLiteral, + message: str = "", + ) -> _EntityStatus: + """Convert the status name, such as 'active', to the class, such as ActiveStatus.""" + # Note that this won't work for UnknownStatus. + # All subclasses have a default 'name' attribute, but the type checker can't tell that. + return cls._entity_statuses[name](message=message) # type:ignore + + @classmethod + def from_ops(cls, obj: StatusBase) -> _EntityStatus: + """Convert from the ops.StatusBase object to the matching _EntityStatus object.""" + return cls.from_status_name(obj.name, obj.message) + + +@dataclasses.dataclass(frozen=True, eq=False, repr=False) +class UnknownStatus(_EntityStatus, ops.UnknownStatus): + __doc__ = ops.UnknownStatus.__doc__ + + name: Literal["unknown"] = "unknown" + + def __init__(self): + super().__init__(name=self.name) + + +@dataclasses.dataclass(frozen=True, eq=False, repr=False) +class ErrorStatus(_EntityStatus, ops.ErrorStatus): + __doc__ = ops.ErrorStatus.__doc__ + + name: Literal["error"] = "error" + + def __init__(self, message: str = ""): + super().__init__(name="error", message=message) + + +@dataclasses.dataclass(frozen=True, eq=False, repr=False) +class ActiveStatus(_EntityStatus, ops.ActiveStatus): + __doc__ = ops.ActiveStatus.__doc__ + + name: Literal["active"] = "active" + + def __init__(self, message: str = ""): + super().__init__(name="active", message=message) + + +@dataclasses.dataclass(frozen=True, eq=False, repr=False) +class BlockedStatus(_EntityStatus, ops.BlockedStatus): + __doc__ = ops.BlockedStatus.__doc__ + + name: Literal["blocked"] = "blocked" + + def __init__(self, message: str = ""): + super().__init__(name="blocked", message=message) + + +@dataclasses.dataclass(frozen=True, eq=False, repr=False) +class MaintenanceStatus(_EntityStatus, ops.MaintenanceStatus): + __doc__ = ops.MaintenanceStatus.__doc__ + + name: Literal["maintenance"] = "maintenance" + + def __init__(self, message: str = ""): + super().__init__(name="maintenance", message=message) + + +@dataclasses.dataclass(frozen=True, eq=False, repr=False) +class WaitingStatus(_EntityStatus, ops.WaitingStatus): + __doc__ = ops.WaitingStatus.__doc__ + + name: Literal["waiting"] = "waiting" + + def __init__(self, message: str = ""): + super().__init__(name="waiting", message=message) + + +_EntityStatus._entity_statuses.update( + unknown=UnknownStatus, + error=ErrorStatus, + active=ActiveStatus, + blocked=BlockedStatus, + maintenance=MaintenanceStatus, + waiting=WaitingStatus, +) + + +@dataclasses.dataclass(frozen=True) +class StoredState(_max_posargs(1)): + """Represents unit-local state that persists across events.""" + + name: str = "_stored" + """The attribute in the parent Object where the state is stored. + + For example, ``_stored`` in this class:: + + class MyCharm(ops.CharmBase): + _stored = ops.StoredState() + + """ + + owner_path: str | None = None + """The path to the owner of this StoredState instance. + + If None, the owner is the Framework. Otherwise, /-separated object names, + for example MyCharm/MyCharmLib. + """ + + # Ideally, the type here would be only marshallable types, rather than Any. + # However, it's complex to describe those types, since it's a recursive + # definition - even in TypeShed the _Marshallable type includes containers + # like list[Any], which seems to defeat the point. + content: dict[str, Any] = dataclasses.field(default_factory=dict) + """The content of the :class:`ops.StoredState` instance.""" + + _data_type_name: str = "StoredStateData" + + @property + def _handle_path(self): + return f"{self.owner_path or ''}/{self._data_type_name}[{self.name}]" + + def __hash__(self) -> int: + return hash(self._handle_path) + + +_RawPortProtocolLiteral = Literal["tcp", "udp", "icmp"] + + +@dataclasses.dataclass(frozen=True) +class Port(_max_posargs(1)): + """Represents a port on the charm host. + + Port objects should not be instantiated directly: use :class:`TCPPort`, + :class:`UDPPort`, or :class:`ICMPPort` instead. + """ + + port: int | None = None + """The port to open. Required for TCP and UDP; not allowed for ICMP.""" + + protocol: _RawPortProtocolLiteral = "tcp" + """The protocol that data transferred over the port will use.""" + + def __post_init__(self): + if type(self) is Port: + raise RuntimeError( + "Port cannot be instantiated directly; " + "please use TCPPort, UDPPort, or ICMPPort", + ) + + def __eq__(self, other: object) -> bool: + if isinstance(other, (Port, ops.Port)): + return (self.protocol, self.port) == (other.protocol, other.port) + return False + + +@dataclasses.dataclass(frozen=True) +class TCPPort(Port): + """Represents a TCP port on the charm host.""" + + port: int # type: ignore + """The port to open.""" + protocol: _RawPortProtocolLiteral = "tcp" + """The protocol that data transferred over the port will use. + + :meta private: + """ + + def __post_init__(self): + super().__post_init__() + if not (1 <= self.port <= 65535): + raise StateValidationError( + f"`port` outside bounds [1:65535], got {self.port}", + ) + + +@dataclasses.dataclass(frozen=True) +class UDPPort(Port): + """Represents a UDP port on the charm host.""" + + port: int # type: ignore + """The port to open.""" + protocol: _RawPortProtocolLiteral = "udp" + """The protocol that data transferred over the port will use. + + :meta private: + """ + + def __post_init__(self): + super().__post_init__() + if not (1 <= self.port <= 65535): + raise StateValidationError( + f"`port` outside bounds [1:65535], got {self.port}", + ) + + +@dataclasses.dataclass(frozen=True) +class ICMPPort(Port): + """Represents an ICMP port on the charm host.""" + + protocol: _RawPortProtocolLiteral = "icmp" + """The protocol that data transferred over the port will use. + + :meta private: + """ + + _max_positional_args: Final = 0 + + def __post_init__(self): + super().__post_init__() + if self.port is not None: + raise StateValidationError("`port` cannot be set for `ICMPPort`") + + +_port_cls_by_protocol = { + "tcp": TCPPort, + "udp": UDPPort, + "icmp": ICMPPort, +} + + +_next_storage_index_counter = 0 # storage indices start at 0 + + +def _next_storage_index(*, update: bool = True): + """Get the index (used to be called ID) the next Storage to be created will get. + + Pass update=False if you're only inspecting it. + Pass update=True if you also want to bump it. + """ + global _next_storage_index_counter + cur = _next_storage_index_counter + if update: + _next_storage_index_counter += 1 + return cur + + +@dataclasses.dataclass(frozen=True) +class Storage(_max_posargs(1)): + """Represents an (attached) storage made available to the charm container.""" + + name: str + """The name of the storage, as found in the charm metadata.""" + + index: int = dataclasses.field(default_factory=_next_storage_index) + """The index of this storage instance. + + For Kubernetes charms, this will always be 1. For machine charms, each new + Storage instance gets a new index.""" + + def __eq__(self, other: object) -> bool: + if isinstance(other, (Storage, ops.Storage)): + return (self.name, self.index) == (other.name, other.index) + return False + + def get_filesystem(self, ctx: Context) -> pathlib.Path: + """Simulated filesystem root in this context.""" + return ctx._get_storage_root(self.name, self.index) + + +@dataclasses.dataclass(frozen=True) +class Resource(_max_posargs(0)): + """Represents a resource made available to the charm.""" + + name: str + """The name of the resource, as found in the charm metadata.""" + path: str | pathlib.Path + """A local path that will be provided to the charm as the content of the resource.""" + + +@dataclasses.dataclass(frozen=True) +class State(_max_posargs(0)): + """Represents the Juju-owned portion of a unit's state. + + Roughly speaking, it wraps all hook-tool- and pebble-mediated data a charm can access in its + lifecycle. For example, status-get will return data from `State.unit_status`, is-leader will + return data from `State.leader`, and so on. + """ + + config: dict[str, str | int | float | bool] = dataclasses.field( + default_factory=dict, + ) + """The present configuration of this charm.""" + relations: Iterable[RelationBase] = dataclasses.field(default_factory=frozenset) + """All relations that currently exist for this charm.""" + networks: Iterable[Network] = dataclasses.field(default_factory=frozenset) + """Manual overrides for any relation and extra bindings currently provisioned for this charm. + If a metadata-defined relation endpoint is not explicitly mapped to a Network in this field, + it will be defaulted. + + .. warning:: + `extra-bindings` is a deprecated, regretful feature in Juju/ops. For completeness we + support it, but use at your own risk. If a metadata-defined extra-binding is left empty, + it will be defaulted. + """ + containers: Iterable[Container] = dataclasses.field(default_factory=frozenset) + """All containers (whether they can connect or not) that this charm is aware of.""" + storages: Iterable[Storage] = dataclasses.field(default_factory=frozenset) + """All **attached** storage instances for this charm. + + If a storage is not attached, omit it from this listing.""" + + # we don't use sets to make json serialization easier + opened_ports: Iterable[Port] = dataclasses.field(default_factory=frozenset) + """Ports opened by Juju on this charm.""" + leader: bool = False + """Whether this charm has leadership.""" + model: Model = Model() + """The model this charm lives in.""" + secrets: Iterable[Secret] = dataclasses.field(default_factory=frozenset) + """The secrets this charm has access to (as an owner, or as a grantee). + + The presence of a secret in this list entails that the charm can read it. + Whether it can manage it or not depends on the individual secret's `owner` flag.""" + resources: Iterable[Resource] = dataclasses.field(default_factory=frozenset) + """All resources that this charm can access.""" + planned_units: int = 1 + """Number of non-dying planned units that are expected to be running this application. + + Use with caution.""" + + # Represents the OF's event queue. These events will be emitted before the event being + # dispatched, and represent the events that had been deferred during the previous run. + # If the charm defers any events during "this execution", they will be appended + # to this list. + deferred: list[DeferredEvent] = dataclasses.field(default_factory=list) + """Events that have been deferred on this charm by some previous execution.""" + stored_states: Iterable[StoredState] = dataclasses.field( + default_factory=frozenset, + ) + """Contents of a charm's stored state.""" + + # the current statuses. + app_status: _EntityStatus = UnknownStatus() + """Status of the application.""" + unit_status: _EntityStatus = UnknownStatus() + """Status of the unit.""" + workload_version: str = "" + """Workload version.""" + + def __post_init__(self): + # Let people pass in the ops classes, and convert them to the appropriate Scenario classes. + for name in ["app_status", "unit_status"]: + val = getattr(self, name) + if isinstance(val, _EntityStatus): + pass + elif isinstance(val, StatusBase): + object.__setattr__(self, name, _EntityStatus.from_ops(val)) + else: + raise TypeError(f"Invalid status.{name}: {val!r}") + normalised_ports = [ + Port(protocol=port.protocol, port=port.port) + if isinstance(port, ops.Port) + else port + for port in self.opened_ports + ] + if self.opened_ports != normalised_ports: + object.__setattr__(self, "opened_ports", normalised_ports) + normalised_storage = [ + Storage(name=storage.name, index=storage.index) + if isinstance(storage, ops.Storage) + else storage + for storage in self.storages + ] + if self.storages != normalised_storage: + object.__setattr__(self, "storages", normalised_storage) + + # ops.Container, ops.Model, ops.Relation, ops.Secret should not be instantiated by charmers. + # ops.Network does not have the relation name, so cannot be converted. + # ops.Resources does not contain the source of the resource, so cannot be converted. + # ops.StoredState is not convenient to initialise with data, so not useful here. + + # It's convenient to pass a set, but we really want the attributes to be + # frozen sets to increase the immutability of State objects. + for name in [ + "relations", + "containers", + "storages", + "networks", + "opened_ports", + "secrets", + "resources", + "stored_states", + ]: + val = getattr(self, name) + # It's ok to pass any iterable (of hashable objects), but you'll get + # a frozenset as the actual attribute. + if not isinstance(val, frozenset): + object.__setattr__(self, name, frozenset(val)) + + def _update_workload_version(self, new_workload_version: str): + """Update the current app version and record the previous one.""" + # We don't keep a full history because we don't expect the app version to change more + # than once per hook. + + # bypass frozen dataclass + object.__setattr__(self, "workload_version", new_workload_version) + + def _update_status( + self, + new_status: _EntityStatus, + is_app: bool = False, + ): + """Update the current app/unit status.""" + name = "app_status" if is_app else "unit_status" + # bypass frozen dataclass + object.__setattr__(self, name, new_status) + + def _update_opened_ports(self, new_ports: frozenset[Port]): + """Update the current opened ports.""" + # bypass frozen dataclass + object.__setattr__(self, "opened_ports", new_ports) + + def _update_secrets(self, new_secrets: frozenset[Secret]): + """Update the current secrets.""" + # bypass frozen dataclass + object.__setattr__(self, "secrets", new_secrets) + + def get_container(self, container: str, /) -> Container: + """Get container from this State, based on its name.""" + for state_container in self.containers: + if state_container.name == container: + return state_container + raise KeyError(f"container: {container} not found in the State") + + def get_network(self, binding_name: str, /) -> Network: + """Get network from this State, based on its binding name.""" + for network in self.networks: + if network.binding_name == binding_name: + return network + raise KeyError(f"network: {binding_name} not found in the State") + + def get_secret( + self, + *, + id: str | None = None, + label: str | None = None, + ) -> Secret: + """Get secret from this State, based on the secret's id or label.""" + if id is None and label is None: + raise ValueError("An id or label must be provided.") + + for secret in self.secrets: + if ( + (id and label and secret.id == id and secret.label == label) + or (id and label is None and secret.id == id) + or (id is None and label and secret.label == label) + ): + return secret + raise KeyError("secret: not found in the State") + + def get_stored_state( + self, + stored_state: str, + /, + *, + owner_path: str | None = None, + ) -> StoredState: + """Get stored state from this State, based on the stored state's name and owner_path.""" + for ss in self.stored_states: + if ss.name == stored_state and ss.owner_path == owner_path: + return ss + raise ValueError(f"stored state: {stored_state} not found in the State") + + def get_storage( + self, + storage: str, + /, + *, + index: int | None = 0, + ) -> Storage: + """Get storage from this State, based on the storage's name and index.""" + for state_storage in self.storages: + if state_storage.name == storage and storage.index == index: + return state_storage + raise ValueError( + f"storage: name={storage}, index={index} not found in the State", + ) + + def get_relation(self, relation: int, /) -> RelationBase: + """Get relation from this State, based on the relation's id.""" + for state_relation in self.relations: + if state_relation.id == relation: + return state_relation + raise KeyError(f"relation: id={relation} not found in the State") + + def get_relations(self, endpoint: str) -> tuple[RelationBase, ...]: + """Get all relations on this endpoint from the current state.""" + + # we rather normalize the endpoint than worry about cursed metadata situations such as: + # requires: + # foo-bar: ... + # foo_bar: ... + + normalized_endpoint = _normalise_name(endpoint) + return tuple( + r + for r in self.relations + if _normalise_name(r.endpoint) == normalized_endpoint + ) + + +def _is_valid_charmcraft_25_metadata(meta: dict[str, Any]): + # Check whether this dict has the expected mandatory metadata fields according to the + # charmcraft >2.5 charmcraft.yaml schema + if (config_type := meta.get("type")) != "charm": + logger.debug( + f"Not a charm: charmcraft yaml config ``.type`` is {config_type!r}.", + ) + return False + if not all(field in meta for field in {"name", "summary", "description"}): + logger.debug("Not a charm: charmcraft yaml misses some required fields") + return False + return True + + +@dataclasses.dataclass(frozen=True) +class _CharmSpec(Generic[CharmType]): + """Charm spec.""" + + charm_type: type[CharmBase] + meta: dict[str, Any] + actions: dict[str, Any] | None = None + config: dict[str, Any] | None = None + + # autoloaded means: we are running a 'real' charm class, living in some + # /src/charm.py, and the metadata files are 'real' metadata files. + is_autoloaded: bool = False + + @staticmethod + def _load_metadata_legacy(charm_root: pathlib.Path): + """Load metadata from charm projects created with Charmcraft < 2.5.""" + # back in the days, we used to have separate metadata.yaml, config.yaml and actions.yaml + # files for charm metadata. + metadata_path = charm_root / "metadata.yaml" + meta: dict[str, Any] = ( + yaml.safe_load(metadata_path.open()) if metadata_path.exists() else {} + ) + + config_path = charm_root / "config.yaml" + config = yaml.safe_load(config_path.open()) if config_path.exists() else None + + actions_path = charm_root / "actions.yaml" + actions = yaml.safe_load(actions_path.open()) if actions_path.exists() else None + return meta, config, actions + + @staticmethod + def _load_metadata(charm_root: pathlib.Path): + """Load metadata from charm projects created with Charmcraft >= 2.5.""" + metadata_path = charm_root / "charmcraft.yaml" + meta: dict[str, Any] = ( + yaml.safe_load(metadata_path.open()) if metadata_path.exists() else {} + ) + if not _is_valid_charmcraft_25_metadata(meta): + meta = {} + config = meta.pop("config", None) + actions = meta.pop("actions", None) + return meta, config, actions + + @staticmethod + def autoload(charm_type: type[CharmBase]) -> _CharmSpec[CharmType]: + """Construct a ``_CharmSpec`` object by looking up the metadata from the charm's repo root. + + Will attempt to load the metadata off the ``charmcraft.yaml`` file + """ + charm_source_path = pathlib.Path(inspect.getfile(charm_type)) + charm_root = charm_source_path.parent.parent + + # attempt to load metadata from unified charmcraft.yaml + meta, config, actions = _CharmSpec._load_metadata(charm_root) + + if not meta: + # try to load using legacy metadata.yaml/actions.yaml/config.yaml files + meta, config, actions = _CharmSpec._load_metadata_legacy(charm_root) + + if not meta: + # still no metadata? bug out + raise MetadataNotFoundError( + f"invalid charm root {charm_root!r}; " + f"expected to contain at least a `charmcraft.yaml` file " + f"(or a `metadata.yaml` file if it's an old charm).", + ) + + return _CharmSpec( + charm_type=charm_type, + meta=meta, + actions=actions, + config=config, + is_autoloaded=True, + ) + + def get_all_relations(self) -> list[tuple[str, dict[str, str]]]: + """A list of all relation endpoints defined in the metadata.""" + return list( + chain( + self.meta.get("requires", {}).items(), + self.meta.get("provides", {}).items(), + self.meta.get("peers", {}).items(), + ), + ) + + +@dataclasses.dataclass(frozen=True) +class DeferredEvent: + """An event that has been deferred to run prior to the next Juju event. + + Tests should not instantiate this class directly: use the `deferred` method + of the event instead. For example: + + ctx = Context(MyCharm) + deferred_start = ctx.on.start().deferred(handler=MyCharm._on_start) + state = State(deferred=[deferred_start]) + """ + + handle_path: str + owner: str + observer: str + + # needs to be marshal.dumps-able. + snapshot_data: dict[Any, Any] = dataclasses.field(default_factory=dict) + + # It would be nicer if people could do something like: + # `isinstance(state.deferred[0], ops.StartEvent)` + # than comparing with the string names, but there's only one `_Event` + # class in Scenario, and it also needs to be created from the context, + # which is not available here. For the ops classes, it's complex to create + # them because they need a Handle. + @property + def name(self): + """A comparable name for the event.""" + return self.handle_path.split("/")[-1].split("[")[0] + + +class _EventType(str, Enum): + FRAMEWORK = "framework" + BUILTIN = "builtin" + RELATION = "relation" + ACTION = "action" + SECRET = "secret" + STORAGE = "storage" + WORKLOAD = "workload" + CUSTOM = "custom" + + +class _EventPath(str): + if TYPE_CHECKING: # pragma: no cover + name: str + owner_path: list[str] + suffix: str + prefix: str + is_custom: bool + type: _EventType + + def __new__(cls, string: str): + string = _normalise_name(string) + instance = super().__new__(cls, string) + + instance.name = name = string.split(".")[-1] + instance.owner_path = string.split(".")[:-1] or ["on"] + + instance.suffix, instance.type = suffix, _ = _EventPath._get_suffix_and_type( + name, + ) + if suffix: + instance.prefix, _ = string.rsplit(suffix) + else: + instance.prefix = string + + instance.is_custom = suffix == "" + return instance + + @staticmethod + def _get_suffix_and_type(s: str) -> tuple[str, _EventType]: + for suffix in _RELATION_EVENTS_SUFFIX: + if s.endswith(suffix): + return suffix, _EventType.RELATION + + if s.endswith(_ACTION_EVENT_SUFFIX): + return _ACTION_EVENT_SUFFIX, _EventType.ACTION + + if s in _SECRET_EVENTS: + return s, _EventType.SECRET + + if s in _FRAMEWORK_EVENTS: + return s, _EventType.FRAMEWORK + + # Whether the event name indicates that this is a storage event. + for suffix in _STORAGE_EVENTS_SUFFIX: + if s.endswith(suffix): + return suffix, _EventType.STORAGE + + # Whether the event name indicates that this is a workload event. + if s.endswith(_PEBBLE_READY_EVENT_SUFFIX): + return _PEBBLE_READY_EVENT_SUFFIX, _EventType.WORKLOAD + if s.endswith(_PEBBLE_CUSTOM_NOTICE_EVENT_SUFFIX): + return _PEBBLE_CUSTOM_NOTICE_EVENT_SUFFIX, _EventType.WORKLOAD + if s.endswith(_PEBBLE_CHECK_FAILED_EVENT_SUFFIX): + return _PEBBLE_CHECK_FAILED_EVENT_SUFFIX, _EventType.WORKLOAD + if s.endswith(_PEBBLE_CHECK_RECOVERED_EVENT_SUFFIX): + return _PEBBLE_CHECK_RECOVERED_EVENT_SUFFIX, _EventType.WORKLOAD + + if s in _BUILTIN_EVENTS: + return "", _EventType.BUILTIN + + return "", _EventType.CUSTOM + + +@dataclasses.dataclass(frozen=True) +class _Event: # type: ignore + """A Juju, ops, or custom event that can be run against a charm. + + Typically, for simple events, the string name (e.g. ``install``) can be used, + and for more complex events, an ``event`` property will be available on the + related object (e.g. ``relation.joined_event``). + """ + + path: str + args: tuple[Any, ...] = () + kwargs: dict[str, Any] = dataclasses.field(default_factory=dict) + + storage: Storage | None = None + """If this is a storage event, the storage it refers to.""" + relation: RelationBase | None = None + """If this is a relation event, the relation it refers to.""" + relation_remote_unit_id: int | None = None + relation_departed_unit_id: int | None = None + + secret: Secret | None = None + """If this is a secret event, the secret it refers to.""" + + # if this is a secret-removed or secret-expired event, the secret revision it refers to + secret_revision: int | None = None + + container: Container | None = None + """If this is a workload (container) event, the container it refers to.""" + + notice: Notice | None = None + """If this is a Pebble notice event, the notice it refers to.""" + + check_info: CheckInfo | None = None + """If this is a Pebble check event, the check info it provides.""" + + action: _Action | None = None + """If this is an action event, the :class:`Action` it refers to.""" + + _owner_path: list[str] = dataclasses.field(default_factory=list) + + def __post_init__(self): + path = _EventPath(self.path) + # bypass frozen dataclass + object.__setattr__(self, "path", path) + + @property + def _path(self) -> _EventPath: + # we converted it in __post_init__, but the type checker doesn't know about that + return cast(_EventPath, self.path) + + @property + def name(self) -> str: + """Full event name. + + Consists of a 'prefix' and a 'suffix'. The suffix denotes the type of the event, the + prefix the name of the entity the event is about. + + "foo-relation-changed": + - "foo"=prefix (name of a relation), + - "-relation-changed"=suffix (relation event) + """ + return self._path.name + + @property + def owner_path(self) -> list[str]: + """Path to the ObjectEvents instance owning this event. + + If this event is defined on the toplevel charm class, it should be ['on']. + """ + return self._path.owner_path + + @property + def _is_relation_event(self) -> bool: + """Whether the event name indicates that this is a relation event.""" + return self._path.type is _EventType.RELATION + + @property + def _is_action_event(self) -> bool: + """Whether the event name indicates that this is a relation event.""" + return self._path.type is _EventType.ACTION + + @property + def _is_secret_event(self) -> bool: + """Whether the event name indicates that this is a secret event.""" + return self._path.type is _EventType.SECRET + + @property + def _is_storage_event(self) -> bool: + """Whether the event name indicates that this is a storage event.""" + return self._path.type is _EventType.STORAGE + + @property + def _is_workload_event(self) -> bool: + """Whether the event name indicates that this is a workload event.""" + return self._path.type is _EventType.WORKLOAD + + # this method is private because _CharmSpec is not quite user-facing; also, + # the user should know. + def _is_builtin_event(self, charm_spec: _CharmSpec[CharmType]) -> bool: + """Determine whether the event is a custom-defined one or a builtin one.""" + event_name = self.name + + # simple case: this is an event type owned by our charm base.on + if hasattr(charm_spec.charm_type.on, event_name): + return hasattr(CharmEvents, event_name) + + # this could be an event defined on some other Object, e.g. a charm lib. + # We don't support (yet) directly emitting those, but they COULD have names that conflict + # with events owned by the base charm. E.g. if the charm has a `foo` relation, the charm + # will get a charm.on.foo_relation_created. Your charm lib is free to define its own + # `foo_relation_created` custom event, because its handle will be + # `charm.lib.on.foo_relation_created` and therefore be unique and the Framework is happy. + # However, our Event data structure ATM has no knowledge of which Object/Handle it is + # owned by. So the only thing we can do right now is: check whether the event name, + # assuming it is owned by the charm, LOOKS LIKE that of a builtin event or not. + return self._path.type is not _EventType.CUSTOM + + def deferred(self, handler: Callable[..., Any], event_id: int = 1) -> DeferredEvent: + """Construct a DeferredEvent from this Event.""" + handler_repr = repr(handler) + handler_re = re.compile(r"<function (.*) at .*>") + match = handler_re.match(handler_repr) + if not match: + raise ValueError( + f"cannot construct DeferredEvent from {handler}; please create one manually.", + ) + owner_name, handler_name = match.groups()[0].split(".")[-2:] + handle_path = f"{owner_name}/on/{self.name}[{event_id}]" + + # Many events have no snapshot data: install, start, stop, remove, config-changed, + # upgrade-charm, pre-series-upgrade, post-series-upgrade, leader-elected, + # leader-settings-changed, collect-metrics + snapshot_data: dict[str, Any] = {} + + # fixme: at this stage we can't determine if the event is a builtin one or not; if it is + # not, then the coming checks are meaningless: the custom event could be named like a + # relation event but not *be* one. + if self._is_workload_event: + # Enforced by the consistency checker, but for type checkers: + assert self.container is not None + snapshot_data["container_name"] = self.container.name + if self.notice: + if hasattr(self.notice.type, "value"): + notice_type = cast(pebble.NoticeType, self.notice.type).value + else: + notice_type = str(self.notice.type) + snapshot_data.update( + { + "notice_id": self.notice.id, + "notice_key": self.notice.key, + "notice_type": notice_type, + }, + ) + elif self.check_info: + snapshot_data["check_name"] = self.check_info.name + + elif self._is_relation_event: + # Enforced by the consistency checker, but for type checkers: + assert self.relation is not None + relation = self.relation + if isinstance(relation, PeerRelation): + # FIXME: relation.unit for peers should point to <this unit>, but we + # don't have access to the local app name in this context. + remote_app = "local" + elif isinstance(relation, (Relation, SubordinateRelation)): + remote_app = relation.remote_app_name + else: + raise RuntimeError(f"unexpected relation type: {relation!r}") + + snapshot_data.update( + { + "relation_name": relation.endpoint, + "relation_id": relation.id, + "app_name": remote_app, + }, + ) + if not self.name.endswith(("_created", "_broken")): + snapshot_data["unit_name"] = ( + f"{remote_app}/{self.relation_remote_unit_id}" + ) + if self.name.endswith("_departed"): + snapshot_data["departing_unit"] = ( + f"{remote_app}/{self.relation_departed_unit_id}" + ) + + elif self._is_storage_event: + # Enforced by the consistency checker, but for type checkers: + assert self.storage is not None + snapshot_data.update( + { + "storage_name": self.storage.name, + "storage_index": self.storage.index, + # "storage_location": str(self.storage.get_filesystem(self._context)), + }, + ) + + elif self._is_secret_event: + # Enforced by the consistency checker, but for type checkers: + assert self.secret is not None + snapshot_data.update( + {"secret_id": self.secret.id, "secret_label": self.secret.label}, + ) + if self.name.endswith(("_remove", "_expired")): + snapshot_data["secret_revision"] = self.secret_revision + + elif self._is_action_event: + # Enforced by the consistency checker, but for type checkers: + assert self.action is not None + snapshot_data["id"] = self.action.id + + return DeferredEvent( + handle_path, + owner_name, + handler_name, + snapshot_data=snapshot_data, + ) + + +_next_action_id_counter = 1 + + +def _next_action_id(*, update: bool = True): + """Get the ID the next action to be created will get. + + Pass update=False if you're only inspecting it. + Pass update=True if you also want to bump it. + """ + global _next_action_id_counter + cur = _next_action_id_counter + if update: + _next_action_id_counter += 1 + # Juju currently uses numbers for the ID, but in the past used UUIDs, so + # we need these to be strings. + return str(cur) + + +@dataclasses.dataclass(frozen=True) +class _Action(_max_posargs(1)): + """A ``juju run`` command. + + Used to simulate ``juju run``, passing in any parameters. For example:: + + def test_backup_action(): + ctx = Context(MyCharm) + state = ctx.run( + ctx.on.action('do_backup', params={'filename': 'foo'}), + State(), + ) + assert ctx.action_results == ... + """ + + name: str + """Juju action name, as found in the charm metadata.""" + + params: Mapping[str, AnyJson] = dataclasses.field(default_factory=dict) + """Parameter values passed to the action.""" + + id: str = dataclasses.field(default_factory=_next_action_id) + """Juju action ID. + + Every action invocation is automatically assigned a new one. Override in + the rare cases where a specific ID is required.""" diff --git a/testing/dist/ops_scenario-7.0.5-py3-none-any.whl b/testing/dist/ops_scenario-7.0.5-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..d4bf9fd42ebe49d073366bcb7dd217fc7f4b1301 GIT binary patch literal 70372 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 X&~t>r4**aT9t&pxfHi6&!n=O~cp!yb literal 0 HcmV?d00001 diff --git a/testing/src/ops_scenario.egg-info/PKG-INFO b/testing/src/ops_scenario.egg-info/PKG-INFO new file mode 100644 index 000000000..288ae8623 --- /dev/null +++ b/testing/src/ops_scenario.egg-info/PKG-INFO @@ -0,0 +1,1208 @@ +Metadata-Version: 2.1 +Name: ops-scenario +Version: 7.0.6.dev0 +Summary: Python library providing a state-transition testing API for Operator Framework charms. +Author-email: Pietro Pasotti <pietro.pasotti@canonical.com> +Maintainer-email: "The Charm Tech team at Canonical Ltd." <charm-tech@lists.launchpad.net> +License: Apache-2.0 +Project-URL: Homepage, https://github.com/canonical/operator +Project-URL: Bug Tracker, https://github.com/canonical/operator/issues +Keywords: juju,test +Classifier: Development Status :: 5 - Production/Stable +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Framework :: Pytest +Classifier: Intended Audience :: Developers +Classifier: Topic :: Software Development :: Quality Assurance +Classifier: Topic :: Software Development :: Testing +Classifier: Topic :: Utilities +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +Requires-Dist: ops~=2.17 +Requires-Dist: PyYAML>=6.0.1 + +# Scenario + +[![Build](https://github.com/canonical/ops-scenario/actions/workflows/build_wheels.yaml/badge.svg)](https://github.com/canonical/ops-scenario/actions/workflows/build_wheels.yaml) +[![QC](https://github.com/canonical/ops-scenario/actions/workflows/quality_checks.yaml/badge.svg?event=pull_request)](https://github.com/canonical/ops-scenario/actions/workflows/quality_checks.yaml?event=pull_request) +[![Discourse Status](https://img.shields.io/discourse/status?server=https%3A%2F%2Fdiscourse.charmhub.io&style=flat&label=CharmHub%20Discourse)](https://discourse.charmhub.io) +[![foo](https://img.shields.io/badge/everything-charming-blueviolet)](https://github.com/PietroPasotti/jhack) +[![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://discourse.charmhub.io/t/rethinking-charm-testing-with-ops-scenario/8649) +[![Python >= 3.8](https://img.shields.io/badge/python-3.8-blue.svg)](https://www.python.org/downloads/release/python-380/) + +Scenario is a state-transition testing SDK for Operator Framework charms. + +Where the Harness enables you to procedurally mock pieces of the state the charm needs to function, Scenario tests allow +you to declaratively define the state all at once, and use it as a sort of context against which you can fire a single +event on the charm and execute its logic. + +This puts scenario tests somewhere in between unit and integration tests: some say 'functional', some say 'contract', I prefer 'state-transition'. + +Scenario tests nudge you into thinking of a charm as an 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 relation data? what is my +config?...) and the charm's execution `Context` (what relations 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 initial state back. + +![state transition model depiction](https://raw.githubusercontent.com/canonical/ops-scenario/main/resources/state-transition-model.png) + +For example: a charm currently in `unknown` status is executed with a `start` event, and based on whether it has leadership or not (according to its input state), it will decide to set `active` or `blocked` status (which will be reflected in the output state). + +Scenario-testing a charm, then, means verifying that: + +- the charm does not raise uncaught exceptions while handling the event +- the output state (or the diff with the input state) is as expected. + +## Core concepts as a metaphor + +I like metaphors, so here we go: + +- There is a theatre stage. +- You pick an actor (a Charm) to put on the stage. Not just any actor: an improv one. +- You arrange the stage with content that the actor will have to interact with. This consists of selecting: + - An initial situation (`State`) in which the actor is, e.g. is the actor the main role or an NPC (`is_leader`), or what + other actors are there around it, what is written in those pebble-shaped books on the table? + - Something that has just happened (an `Event`) and to which the actor has to react (e.g. one of the NPCs leaves the + stage (`relation-departed`), or the content of one of the books changes). +- How the actor will react to the event will have an impact on the context: e.g. the actor might knock over a table (a + container), or write something down into one of the books. + +## Core concepts not as a metaphor + +Scenario tests are about running assertions on atomic state transitions treating the charm being tested like a black +box. An initial state goes in, an event occurs (say, `'start'`) and a new state comes out. Scenario tests are about +validating the transition, that is, consistency-checking the delta between the two states, and verifying the charm +author's expectations. + +Comparing scenario tests with `Harness` tests: + +- Harness exposes an imperative API: the user is expected to call methods on the Harness driving it to the desired + state, then verify its validity by calling charm methods or inspecting the raw data. In contrast, Scenario is declarative. You fully specify an initial state, an execution context and an event, then you run the charm and inspect the results. +- Harness instantiates the charm once, then allows you to fire multiple events on the charm, which is breeding ground + for subtle bugs. Scenario tests are centered around testing single state transitions, that is, one event at a time. + This ensures that the execution environment is as clean as possible (for a unit test). +- Harness maintains a model of the Juju Model, which is a maintenance burden and adds complexity. Scenario mocks at the + level of hook tools and stores all mocking data in a monolithic data structure (the State), which makes it more + lightweight and portable. + +# Writing scenario tests + +A scenario test consists of three broad steps: + +- **Arrange**: + - declare the context + - declare the input state + - select an event to fire +- **Act**: + - run the context (i.e. obtain the output state, given the input state and the event) +- **Assert**: + - verify that the output state (or the delta 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 internal APIs and the internal state of the charm and operator framework. + +The most basic scenario is one in which all is defaulted and barely any data is +available. The charm has no config, no relations, no leadership, and its status is `unknown`. + +With that, we can write the simplest possible scenario test: + +```python +def test_scenario_base(): + ctx = scenario.Context(MyCharm, meta={"name": "foo"}) + out = ctx.run(ctx.on.start(), scenario.State()) + assert out.unit_status == scenario.UnknownStatus() +``` + +Note that you should always compare the app and unit status using `==`, not `is`. You can compare +them to either the `scenario` objects, or the `ops` ones. + +Now let's start making it more complicated. Our charm sets a special state if it has leadership on 'start': + +```python +import pytest + + +class MyCharm(ops.CharmBase): + def __init__(self, framework): + super().__init__(framework) + framework.observe(self.on.start, self._on_start) + + def _on_start(self, _): + if self.unit.is_leader(): + self.unit.status = ops.ActiveStatus('I rule') + else: + self.unit.status = ops.ActiveStatus('I am ruled') + + +@pytest.mark.parametrize('leader', (True, False)) +def test_status_leader(leader): + ctx = scenario.Context(MyCharm, meta={"name": "foo"}) + out = ctx.run(ctx.on.start(), scenario.State(leader=leader)) + assert out.unit_status == scenario.ActiveStatus('I rule' if leader else 'I am ruled') +``` + +By defining the right state we can programmatically define what answers will the charm get to all the questions it can +ask the Juju model: am I leader? What are my relations? What is the remote unit I'm talking to? etc... + +## Statuses + +One of the simplest types of black-box testing available to charmers is to execute the charm and verify that the charm +sets the expected unit/application status. We have seen a simple example above including leadership. But what if the +charm transitions through a sequence of statuses? + +```python +# charm code: +def _on_event(self, _event): + self.unit.status = ops.MaintenanceStatus('determining who the ruler is...') + try: + if self._call_that_takes_a_few_seconds_and_only_passes_on_leadership(): + self.unit.status = ops.ActiveStatus('I rule') + else: + self.unit.status = ops.WaitingStatus('checking this is right...') + self._check_that_takes_some_more_time() + self.unit.status = ops.ActiveStatus('I am ruled') + except: + self.unit.status = ops.BlockedStatus('something went wrong') +``` + +More broadly, often we want to test 'side effects' of executing a charm, such as what events have been emitted, what +statuses it went through, etc... Before we get there, we have to explain what the `Context` represents, and its +relationship with the `State`. + +## Context and State + +Consider the following tests. Suppose we want to verify that while handling a given top-level Juju event: + +- a specific chain of (custom) events was emitted on the charm +- the charm `juju-log`ged these specific strings +- the charm went through this sequence of app/unit statuses (e.g. `maintenance`, then `waiting`, then `active`) + +These types of test have a place in Scenario, but that is not State: the contents of the Juju log or the status history +are side effects of executing a charm, but are not persisted in a charm-accessible "state" in any meaningful way. +In other words: those data streams are, from the charm's perspective, write-only. + +As such, they do not belong in `scenario.State` but in `scenario.Context`: the object representing the charm's execution +context. + +## Status history + +You can verify that the charm has followed the expected path by checking the unit/app status history like so: + +```python +def test_statuses(): + ctx = scenario.Context(MyCharm, meta={"name": "foo"}) + out = ctx.run(ctx.on.start(), scenario.State(leader=False)) + assert ctx.unit_status_history == [ + scenario.UnknownStatus(), + scenario.MaintenanceStatus('determining who the ruler is...'), + scenario.WaitingStatus('checking this is right...'), + ] + assert out.unit_status == scenario.ActiveStatus("I am ruled") + + # similarly you can check the app status history: + assert ctx.app_status_history == [ + scenario.UnknownStatus(), + ... + ] +``` + +Note that the *current* status is **not** in the unit status history. + +Also note that, unless you initialize the State with a preexisting status, the first status in the history will always +be `unknown`. That is because, so far as Scenario is concerned, each event is "the first event this charm has ever +seen". + +If you want to simulate a situation in which the charm already has seen some event, and is in a status other than +Unknown (the default status every charm is born with), you will have to pass the 'initial status' to State. + +```python +class MyCharm(ops.CharmBase): + def __init__(self, framework): + super().__init__(framework) + framework.observe(self.on.start, self._on_start) + + def _on_start(self, event): + self.model.unit.status = ops.ActiveStatus("foo") + +# ... +ctx = scenario.Context(MyCharm, meta={"name": "foo"}) +ctx.run(ctx.on.start(), scenario.State(unit_status=scenario.ActiveStatus('foo'))) +assert ctx.unit_status_history == [ + scenario.ActiveStatus('foo'), # now the first status is active: 'foo'! + # ... +] +``` + +## Workload version history + +Using a similar api to `*_status_history`, you can assert that the charm has set one or more workload versions during a +hook execution: + +```python +# ... +ctx = scenario.Context(HistoryCharm, meta={"name": "foo"}) +ctx.run(ctx.on.start(), scenario.State()) +assert ctx.workload_version_history == ['1', '1.2', '1.5'] +# ... +``` + +Note that the *current* version is not in the version history, as with the status history. + +## Emitted events + +If your charm deals with deferred events, custom events, and charm libs that in turn emit their own custom events, it +can be hard to examine the resulting control flow. In these situations it can be useful to verify that, as a result of a +given Juju event triggering (say, 'start'), a specific chain of events is emitted on the charm. The +resulting state, black-box as it is, gives little insight into how exactly it was obtained. + +```python +def test_foo(): + ctx = scenario.Context(...) + ctx.run(ctx.on.start(), ...) + + assert len(ctx.emitted_events) == 1 + assert isinstance(ctx.emitted_events[0], ops.StartEvent) +``` + +You can configure what events will be captured by passing the following arguments to `Context`: +- `capture_deferred_events`: If you want to include re-emitted deferred events. +- `capture_framework_events`: If you want to include framework events (`pre-commit`, `commit`, and `collect-status`). + +For example: +```python +def test_emitted_full(): + ctx = scenario.Context( + MyCharm, + capture_deferred_events=True, + capture_framework_events=True, + ) + ctx.run(ctx.on.start(), scenario.State(deferred=[ctx.on.update_status().deferred(MyCharm._foo)])) + + assert len(ctx.emitted_events) == 5 + assert [e.handle.kind for e in ctx.emitted_events] == [ + "update_status", + "start", + "collect_unit_status", + "pre_commit", + "commit", + ] +``` + +## Relations + +You can write scenario tests to verify the shape of relation data: + +```python +# This charm copies over remote app data to local unit data +class MyCharm(ops.CharmBase): + ... + + def _on_event(self, event): + rel = event.relation + assert rel.app.name == 'remote' + assert rel.data[self.unit]['abc'] == 'foo' + rel.data[self.unit]['abc'] = rel.data[event.app]['cde'] + + +def test_relation_data(): + rel = scenario.Relation( + endpoint="foo", + interface="bar", + remote_app_name="remote", + local_unit_data={"abc": "foo"}, + remote_app_data={"cde": "baz!"}, + ) + state_in = scenario.State(relations={rel}) + ctx = scenario.Context(MyCharm, meta={"name": "foo"}) + + state_out = ctx.run(ctx.on.start(), state_in) + + assert state_out.get_relation(rel.id).local_unit_data == {"abc": "baz!"} + # You can do this to check that there are no other differences: + assert state_out.relations == { + scenario.Relation( + endpoint="foo", + interface="bar", + remote_app_name="remote", + local_unit_data={"abc": "baz!"}, + remote_app_data={"cde": "baz!"}, + ), + } + +# which is very idiomatic and superbly explicit. +``` + +The only mandatory argument to `Relation` (and other relation types, see below) is `endpoint`. The `interface` will be +derived from the charm's metadata. When fully defaulted, a relation is 'empty'. There are no remote units, the +remote application is called `'remote'` and only has a single unit `remote/0`, and nobody has written any data to the +databags yet. + +That is typically the state of a relation when the first unit joins it. + +When you use `Relation`, you are specifying a regular (conventional) relation. But that is not the only type of +relation. There are also peer relations and subordinate relations. While in the background the data model is the same, +the data access rules and the consistency constraints on them are very different. For example, it does not make sense +for a peer relation to have a different 'remote app' than its 'local app', because it's the same application. + +### PeerRelation + +To declare a peer relation, you should use `scenario.PeerRelation`. The core difference with regular relations is +that peer relations do not have a "remote app" (it's this app, in fact). So unlike `Relation`, a `PeerRelation` does not +have `remote_app_name` or `remote_app_data` arguments. Also, it talks in terms of `peers`: + +- `Relation.remote_units_data` maps to `PeerRelation.peers_data` + +```python +relation = scenario.PeerRelation( + endpoint="peers", + peers_data={1: {}, 2: {}, 42: {'foo': 'bar'}}, +) +``` + +be mindful when using `PeerRelation` not to include **"this unit"**'s ID in `peers_data` or `peers_ids`, as that would +be flagged by the Consistency Checker: + +```python +state_in = scenario.State(relations={ + scenario.PeerRelation( + endpoint="peers", + peers_data={1: {}, 2: {}, 42: {'foo': 'bar'}}, + )}) + +meta = { + "name": "invalid", + "peers": { + "peers": { + "interface": "foo", + } + } +} +ctx = scenario.Context(ops.CharmBase, meta=meta, unit_id=1) +ctx.run(ctx.on.start(), state_in) # invalid: this unit's id cannot be the ID of a peer. +``` + +### SubordinateRelation + +To declare a subordinate relation, you should use `scenario.SubordinateRelation`. The core difference with regular +relations is that subordinate relations always have exactly one remote unit (there is always exactly one remote unit +that this unit can see). +Because of that, `SubordinateRelation`, compared to `Relation`, always talks in terms of `remote`: + +- `Relation.remote_units_data` becomes `SubordinateRelation.remote_unit_data` taking a single `Dict[str:str]`. The remote unit ID can be provided as a separate argument. +- `Relation.remote_unit_ids` becomes `SubordinateRelation.remote_unit_id` (a single ID instead of a list of IDs) +- `Relation.remote_units_data` becomes `SubordinateRelation.remote_unit_data` (a single databag instead of a mapping from unit IDs to databags) + +```python +relation = scenario.SubordinateRelation( + endpoint="peers", + remote_unit_data={"foo": "bar"}, + remote_app_name="zookeeper", + remote_unit_id=42 +) +relation.remote_unit_name # "zookeeper/42" +``` + +### Triggering Relation Events + +If you want to trigger relation events, use `ctx.on.relation_changed` (and so +on for the other relation events) and pass the relation object: + +```python +ctx = scenario.Context(MyCharm, meta=MyCharm.META) + +relation = scenario.Relation(endpoint="foo", interface="bar") +changed_event = ctx.on.relation_changed(relation=relation) +joined_event = ctx.on.relation_joined(relation=relation) +# ... +``` + +The reason for this construction is that the event is associated with some relation-specific metadata, that Scenario +needs to set up the process that will run `ops.main` with the right environment variables. + +### Additional event parameters + +All relation events have some additional metadata that does not belong in the Relation object, such as, for a +relation-joined event, the name of the (remote) unit that is joining the relation. That is what determines what +`ops.model.Unit` you get when you get `RelationJoinedEvent().unit` in an event handler. + +In order to supply this parameter, as well as the relation object, pass as `remote_unit` the id of the +remote unit that the event is about. The reason that this parameter is not supplied to `Relation` but to relation +events, is that the relation already ties 'this app' to some 'remote app' (cfr. the `Relation.remote_app_name` attr), +but not to a specific unit. What remote unit this event is about is not a `State` concern but an `Event` one. + +```python +ctx = scenario.Context(MyCharm, meta=MyCharm.META) + +relation = scenario.Relation(endpoint="foo", interface="bar") +remote_unit_2_is_joining_event = ctx.on.relation_joined(relation, remote_unit=2) +``` + +## Networks + +Simplifying a bit the Juju "spaces" model, each integration endpoint a charm defines in its metadata is associated with a network. Regardless of whether there is a living relation over that endpoint, that is. + +If your charm has a relation `"foo"` (defined in its metadata), then the charm will be able at runtime to do `self.model.get_binding("foo").network`. +The network you'll get by doing so is heavily defaulted (see `state.Network`) and good for most use-cases because the charm should typically not be concerned about what IP it gets. + +On top of the relation-provided network bindings, a charm can also define some `extra-bindings` in its metadata and access them at runtime. Note that this is a deprecated feature that should not be relied upon. For completeness, we support it in Scenario. + +If you want to, you can override any of these relation or extra-binding associated networks with a custom one by passing it to `State.networks`. + +```python +state = scenario.State(networks={ + scenario.Network("foo", [scenario.BindAddress([scenario.Address('192.0.2.1')])]) +}) +``` + +Where `foo` can either be the name of an `extra-bindings`-defined binding, or a relation endpoint. + +## Containers + +When testing a Kubernetes charm, you can mock container interactions. When using the null state (`State()`), there will +be no containers. So if the charm were to `self.unit.containers`, it would get back an empty dict. + +To give the charm access to some containers, you need to pass them to the input state, like so: +`State(containers={...})` + +An example of a state including some containers: + +```python +state = scenario.State(containers={ + scenario.Container(name="foo", can_connect=True), + scenario.Container(name="bar", can_connect=False) +}) +``` + +In this case, `self.unit.get_container('foo').can_connect()` would return `True`, while for 'bar' it would give `False`. + +### Container filesystem setup + +You can configure a container to have some files in it: + +```python +import pathlib + +local_file = pathlib.Path('/path/to/local/real/file.txt') + +container = scenario.Container( + name="foo", + can_connect=True, + mounts={'local': scenario.Mount(location='/local/share/config.yaml', source=local_file)} + ) +state = scenario.State(containers={container}) +``` + +In this case, if the charm were to: + +```python +def _on_start(self, _): + foo = self.unit.get_container('foo') + content = foo.pull('/local/share/config.yaml').read() +``` + +then `content` would be the contents of our locally-supplied `file.txt`. You can use `tempfile` for nicely wrapping +data and passing it to the charm via the container. + +`container.push` works similarly, so you can write a test like: + +```python +import tempfile + + +class MyCharm(ops.CharmBase): + def __init__(self, framework): + super().__init__(framework) + framework.observe(self.on.foo_pebble_ready, self._on_pebble_ready) + + def _on_pebble_ready(self, _): + foo = self.unit.get_container('foo') + foo.push('/local/share/config.yaml', "TEST", make_dirs=True) + + +def test_pebble_push(): + with tempfile.NamedTemporaryFile() as local_file: + container = scenario.Container( + name='foo', + can_connect=True, + mounts={'local': Mount(location='/local/share/config.yaml', source=local_file.name)} + ) + state_in = State(containers={container}) + ctx = Context( + MyCharm, + meta={"name": "foo", "containers": {"foo": {}}} + ) + ctx.run( + ctx.on.pebble_ready(container), + state_in, + ) + assert local_file.read().decode() == "TEST" +``` + +`container.pebble_ready_event` is syntactic sugar for: `Event("foo-pebble-ready", container=container)`. The reason we +need to associate the container with the event is that the Framework uses an envvar to determine which container the +pebble-ready event is about (it does not use the event name). Scenario needs that information, similarly, for injecting +that envvar into the charm's runtime. + +### Container filesystem post-mortem + +If the charm writes files to a container (to a location you didn't Mount as a temporary folder you have access to), you will be able to inspect them using the `get_filesystem` api. + +```python +class MyCharm(ops.CharmBase): + def __init__(self, framework): + super().__init__(framework) + framework.observe(self.on.foo_pebble_ready, self._on_pebble_ready) + + def _on_pebble_ready(self, _): + foo = self.unit.get_container('foo') + foo.push('/local/share/config.yaml', "TEST", make_dirs=True) + + +def test_pebble_push(): + container = scenario.Container(name='foo', can_connect=True) + state_in = scenario.State(containers={container}) + ctx = scenario.Context( + MyCharm, + meta={"name": "foo", "containers": {"foo": {}}} + ) + + ctx.run(ctx.on.start(), state_in) + + # This is the root of the simulated container filesystem. Any mounts will be symlinks in it. + container_root_fs = container.get_filesystem(ctx) + cfg_file = container_root_fs / 'local' / 'share' / 'config.yaml' + assert cfg_file.read_text() == "TEST" +``` + +### `Container.exec` mocks + +`container.exec` is a tad more complicated, but if you get to this low a level of simulation, you probably will have far +worse issues to deal with. You need to specify, for each possible command the charm might run on the container, what the +result of that would be: its return code, what will be written to stdout/stderr. + +```python +LS_LL = """ +.rw-rw-r-- 228 ubuntu ubuntu 18 jan 12:05 -- charmcraft.yaml +.rw-rw-r-- 497 ubuntu ubuntu 18 jan 12:05 -- config.yaml +.rw-rw-r-- 900 ubuntu ubuntu 18 jan 12:05 -- CONTRIBUTING.md +drwxrwxr-x - ubuntu ubuntu 18 jan 12:06 -- lib +""" + + +class MyCharm(ops.CharmBase): + def _on_start(self, _): + foo = self.unit.get_container('foo') + proc = foo.exec(['ls', '-ll']) + proc.stdin.write("...") + stdout, _ = proc.wait_output() + assert stdout == LS_LL + + +def test_pebble_exec(): + container = scenario.Container( + name='foo', + execs={ + scenario.Exec( + command_prefix=['ls'], + return_code=0, + stdout=LS_LL, + ), + } + ) + state_in = scenario.State(containers={container}) + ctx = scenario.Context( + MyCharm, + meta={"name": "foo", "containers": {"foo": {}}}, + ) + state_out = ctx.run( + ctx.on.pebble_ready(container), + state_in, + ) + assert ctx.exec_history[container.name][0].command == ['ls', '-ll'] + assert ctx.exec_history[container.name][0].stdin == "..." +``` + +Scenario will attempt to find the right `Exec` object by matching the provided +command prefix against the command used in the ops `container.exec()` call. For +example if the command is `['ls', '-ll']` then the searching will be: + + 1. an `Exec` with exactly the same as command prefix, `('ls', '-ll')` + 2. an `Exec` with the command prefix `('ls', )` + 3. an `Exec` with the command prefix `()` + +If none of these are found Scenario will raise an `ExecError`. + +### Pebble Notices + +Pebble can generate notices, which Juju will detect, and wake up the charm to +let it know that something has happened in the container. The most common +use-case is Pebble custom notices, which is a mechanism for the workload +application to trigger a charm event. +- +When the charm is notified, there might be a queue of existing notices, or just +the one that has triggered the event: + +```python +class MyCharm(ops.CharmBase): + def __init__(self, framework): + super().__init__(framework) + framework.observe(self.on["my-container"].pebble_custom_notice, self._on_notice) + + def _on_notice(self, event): + event.notice.key # == "example.com/c" + for notice in self.unit.get_container("my-container").get_notices(): + ... + +ctx = scenario.Context(MyCharm, meta={"name": "foo", "containers": {"my-container": {}}}) +notices = [ + scenario.Notice(key="example.com/a", occurrences=10), + scenario.Notice(key="example.com/b", last_data={"bar": "baz"}), + scenario.Notice(key="example.com/c"), +] +container = scenario.Container("my-container", notices=notices) +state = scenario.State(containers={container}) +ctx.run(ctx.on.pebble_custom_notice(container=container, notice=notices[-1]), state) +``` + +### Pebble Checks + +A Pebble plan can contain checks, and when those checks exceed the configured +failure threshold, or start succeeding again after, Juju will emit a +pebble-check-failed or pebble-check-recovered event. In order to simulate these +events, you need to add a `CheckInfo` to the container. Note that the status of the +check doesn't have to match the event being generated: by the time that Juju +sends a pebble-check-failed event the check might have started passing again. + +```python +ctx = scenario.Context(MyCharm, meta={"name": "foo", "containers": {"my_container": {}}}) +check_info = scenario.CheckInfo("http-check", failures=7, status=ops.pebble.CheckStatus.DOWN) +container = scenario.Container("my_container", check_infos={check_info}) +state = scenario.State(containers={container}) +ctx.run(ctx.on.pebble_check_failed(info=check_info, container=container), state=state) +``` + +## Storage + +If your charm defines `storage` in its metadata, you can use `scenario.Storage` to instruct Scenario to make (mocked) filesystem storage available to the charm at runtime. + +Using the same `get_filesystem` API as `Container`, you can access the temporary directory used by Scenario to mock the filesystem root before and after the scenario runs. + +```python +# Some charm with a 'foo' filesystem-type storage defined in its metadata: +ctx = scenario.Context(MyCharm, meta=MyCharm.META) +storage = scenario.Storage("foo") + +# Setup storage with some content: +(storage.get_filesystem(ctx) / "myfile.txt").write_text("helloworld") + +with ctx(ctx.on.update_status(), scenario.State(storages={storage})) as manager: + foo = manager.charm.model.storages["foo"][0] + loc = foo.location + path = loc / "myfile.txt" + assert path.exists() + assert path.read_text() == "helloworld" + + myfile = loc / "path.py" + myfile.write_text("helloworlds") + +# post-mortem: inspect fs contents. +assert ( + storage.get_filesystem(ctx) / "path.py" +).read_text() == "helloworlds" +``` + +Note that State only wants to know about **attached** storages. A storage which is not attached to the charm can simply be omitted from State and the charm will be none the wiser. + +### Storage-add + +If a charm requests adding more storage instances while handling some event, you can inspect that from the `Context.requested_storage` API. + +```python notest +# In MyCharm._on_foo: +# The charm requests two new "foo" storage instances to be provisioned: +self.model.storages.request("foo", 2) +``` + +From test code, you can inspect that: + +```python notest +ctx = scenario.Context(MyCharm, meta=MyCharm.META) +ctx.run(ctx.on.some_event_that_will_cause_on_foo_to_be_called(), scenario.State()) + +# the charm has requested two 'foo' storages to be provisioned: +assert ctx.requested_storages['foo'] == 2 +``` + +Requesting storages has no other consequence in Scenario. In real life, this request will trigger Juju to provision the storage and execute the charm again with `foo-storage-attached`. +So a natural follow-up Scenario test suite for this case would be: + +```python +ctx = scenario.Context(MyCharm, meta=MyCharm.META) +foo_0 = scenario.Storage('foo') +# The charm is notified that one of the storages it has requested is ready: +ctx.run(ctx.on.storage_attached(foo_0), scenario.State(storages={foo_0})) + +foo_1 = scenario.Storage('foo') +# The charm is notified that the other storage is also ready: +ctx.run(ctx.on.storage_attached(foo_1), scenario.State(storages={foo_0, foo_1})) +``` + +## Ports + +Since `ops 2.6.0`, charms can invoke the `open-port`, `close-port`, and `opened-ports` hook tools to manage the ports opened on the host VM/container. Using the `State.opened_ports` API, you can: + +- simulate a charm run with a port opened by some previous execution +ctx = scenario.Context(MyCharm, meta=MyCharm.META) +ctx.run(ctx.on.start(), scenario.State(opened_ports={scenario.TCPPort(42)})) +``` +- assert that a charm has called `open-port` or `close-port`: +```python +ctx = scenario.Context(PortCharm, meta=MyCharm.META) +state1 = ctx.run(ctx.on.start(), scenario.State()) +assert state1.opened_ports == [scenario.TCPPort(42)] + +state2 = ctx.run(ctx.on.stop(), state1) +assert state2.opened_ports == {} +``` + +## Secrets + +Scenario has secrets. Here's how you use them. + +```python +state = scenario.State( + secrets={ + scenario.Secret( + tracked_content={'key': 'public'}, + latest_content={'key': 'public', 'cert': 'private'}, + ) + } +) +``` + +The only mandatory arguments to Secret is the `tracked_content` dict: a `str:str` +mapping representing the content of the revision. If there is a newer revision +of the content than the one the unit that's handling the event is tracking, then +`latest_content` should also be provided - if it's not, then Scenario assumes +that `latest_content` is the `tracked_content`. If there are other revisions of +the content, simply don't include them: the unit has no way of knowing about +these. + +There are three cases: +- the secret is owned by this app but not this unit, in which case this charm can only manage it if we are the leader +- the secret is owned by this unit, in which case this charm can always manage it (leader or not) +- (default) the secret is not owned by this app nor unit, which means we can't manage it but only view it (this includes user secrets) + +Thus by default, the secret is not owned by **this charm**, but, implicitly, by some unknown 'other charm' (or a user), and that other has granted us view rights. + +The presence of the secret in `State.secrets` entails that we have access to it, either as owners or as grantees. Therefore, if we're not owners, we must be grantees. Absence of a Secret from the known secrets list means we are not entitled to obtaining it in any way. The charm, indeed, shouldn't even know it exists. + +[note] +If this charm does not own the secret, but also it was not granted view rights by the (remote) owner, you model this in Scenario by _not adding it to State.secrets_! The presence of a `Secret` in `State.secrets` means, in other words, that the charm has view rights (otherwise, why would we put it there?). If the charm owns the secret, or is leader, it will _also_ have manage rights on top of view ones. +[/note] + +To specify a secret owned by this unit (or app): + +```python +rel = scenario.Relation("web") +state = scenario.State( + secrets={ + scenario.Secret( + {'key': 'private'}, + owner='unit', # or 'app' + # The secret owner has granted access to the "remote" app over some relation: + remote_grants={rel.id: {"remote"}} + ) + } +) +``` + +To specify a secret owned by some other application, or a user secret, and give this unit (or app) access to it: + +```python +state = scenario.State( + secrets={ + scenario.Secret( + {'key': 'public'}, + # owner=None, which is the default + ) + } +) +``` + +When handling the `secret-expired` and `secret-remove` events, the charm must remove the specified revision of the secret. For `secret-remove`, the revision will no longer be in the `State`, because it's no longer in use (which is why the `secret-remove` event was triggered). To ensure that the charm is removing the secret, check the context for the history of secret removal: + +```python +class SecretCharm(ops.CharmBase): + def __init__(self, framework): + super().__init__(framework) + self.framework.observe(self.on.secret_remove, self._on_secret_remove) + + def _on_secret_remove(self, event): + event.secret.remove_revision(event.revision) + + +ctx = scenario.Context(SecretCharm, meta={"name": "foo"}) +secret = scenario.Secret({"password": "xxxxxxxx"}, owner="app") +old_revision = 42 +state = ctx.run( + ctx.on.secret_remove(secret, revision=old_revision), + scenario.State(leader=True, secrets={secret}) +) +assert ctx.removed_secret_revisions == [old_revision] +``` + +## StoredState + +Scenario can simulate StoredState. You can define it on the input side as: + +```python +class MyCharmType(ops.CharmBase): + my_stored_state = ops.StoredState() + + def __init__(self, framework): + super().__init__(framework) + assert self.my_stored_state.foo == 'bar' # this will pass! + + +state = scenario.State(stored_states={ + scenario.StoredState( + owner_path="MyCharmType", + name="my_stored_state", + content={ + 'foo': 'bar', + 'baz': {42: 42}, + }), + }, +) +``` + +And the charm's runtime will see `self.my_stored_state.foo` and `.baz` as expected. Also, you can run assertions on it on +the output side the same as any other bit of state. + +## Resources + +If your charm requires access to resources, you can make them available to it through `State.resources`. +From the perspective of a 'real' deployed charm, if your charm _has_ resources defined in its metadata, they _must_ be made available to the charm. That is a Juju-enforced constraint: you can't deploy a charm without attaching all resources it needs to it. +However, when testing, this constraint is unnecessarily strict (and it would also mean the great majority of all existing tests would break) since a charm will only notice that a resource is not available when it explicitly asks for it, which not many charms do. + +So, the only consistency-level check we enforce in Scenario when it comes to resource is that if a resource is provided in State, it needs to have been declared in the metadata. + +```python +import pathlib + +ctx = scenario.Context(MyCharm, meta={'name': 'juliette', "resources": {"foo": {"type": "oci-image"}}}) +resource = scenario.Resource(name='foo', path='/path/to/resource.tar') +with ctx(ctx.on.start(), scenario.State(resources={resource})) as manager: + # If the charm, at runtime, were to call self.model.resources.fetch("foo"), it would get '/path/to/resource.tar' back. + path = manager.charm.model.resources.fetch('foo') + assert path == pathlib.Path('/path/to/resource.tar') +``` + +## Model + +Charms don't usually need to be aware of the model in which they are deployed, +but if you need to set the model name or UUID, you can provide a `scenario.Model` +to the state: + +```python +ctx = scenario.Context(MyCharm, meta={"name": "foo"}) +state_in = scenario.State(model=scenario.Model(name="my-model")) +out = ctx.run(ctx.on.start(), state_in) +assert out.model.name == "my-model" +assert out.model.uuid == state_in.model.uuid +``` + +### CloudSpec + +You can set CloudSpec information in the model (only `type` and `name` are required). + +Example: + +```python +import scenario + +cloud_spec=scenario.CloudSpec( + type="lxd", + endpoint="https://127.0.0.1:8443", + credential=scenario.CloudCredential( + auth_type="clientcertificate", + attributes={ + "client-cert": "foo", + "client-key": "bar", + "server-cert": "baz", + }, + ), +) +state = scenario.State( + model=scenario.Model(name="my-vm-model", type="lxd", cloud_spec=cloud_spec), +) +``` + +Then you can access it by `Model.get_cloud_spec()`: + +```python +# charm.py +class MyVMCharm(ops.CharmBase): + def __init__(self, framework: ops.Framework): + super().__init__(framework) + framework.observe(self.on.start, self._on_start) + + def _on_start(self, event: ops.StartEvent): + self.cloud_spec = self.model.get_cloud_spec() +``` + +# Actions + +An action is a special sort of event, even though `ops` handles them almost identically. +In most cases, you'll want to inspect the 'results' of an action, or whether it has failed or +logged something while executing. Many actions don't have a direct effect on the output state. + +How to test actions with scenario: + +## Actions without parameters + +```python +def test_backup_action(): + ctx = scenario.Context(MyCharm) + + # If you didn't declare do_backup in the charm's metadata, + # the `ConsistencyChecker` will slap you on the wrist and refuse to proceed. + state = ctx.run(ctx.on.action("do_backup"), scenario.State()) + + # You can assert on action results and logs using the context: + assert ctx.action_logs == ['baz', 'qux'] + assert ctx.action_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 +success checks in every test where the action is successful. + +```python +def test_backup_action_failed(): + ctx = scenario.Context(MyCharm) + + with pytest.raises(ActionFailed) as exc_info: + ctx.run(ctx.on.action("do_backup"), scenario.State()) + assert exc_info.value.message == "sorry, couldn't do the backup" + # The state is also available if that's required: + assert exc_info.value.state.get_container(...) + + # You can still assert action results and logs that occurred as well as the failure: + assert ctx.action_logs == ['baz', 'qux'] + assert ctx.action_results == {'foo': 'bar'} +``` + +## Parametrized Actions + +If the action takes parameters, you can pass those in the call. + +```python +def test_backup_action(): + ctx = scenario.Context(MyCharm) + + # If the parameters (or their type) don't match what is declared in the metadata, + # the `ConsistencyChecker` will slap you on the other wrist. + state = ctx.run( + ctx.on.action("do_backup", params={'a': 'b'}), + scenario.State() + ) + + # ... +``` + +# Deferred events + +Scenario allows you to accurately simulate the Operator Framework's event queue. The event queue is responsible for +keeping track of the deferred events. On the input side, you can verify that if the charm triggers with this and that +event in its queue (they would be there because they had been deferred in the previous run), then the output state is +valid. You generate the deferred data structure using the event's `deferred()` method: + +```python +class MyCharm(ops.CharmBase): + ... + + def _on_update_status(self, event): + event.defer() + + def _on_start(self, event): + event.defer() + + +def test_start_on_deferred_update_status(): + """Test charm execution if a 'start' is dispatched when in the previous run an update-status had been deferred.""" + ctx = scenario.Context(MyCharm) + state_in = scenario.State( + deferred=[ + ctx.on.update_status().deferred(handler=MyCharm._on_update_status) + ] + ) + state_out = ctx.run(ctx.on.start(), state_in) + assert len(state_out.deferred) == 1 + assert state_out.deferred[0].name == 'start' +``` + +On the output side, you can verify that an event that you expect to have been deferred during this trigger, has indeed +been deferred. + +```python +class MyCharm(ops.CharmBase): + ... + + def _on_start(self, event): + event.defer() + + +def test_defer(MyCharm): + out = scenario.Context(MyCharm).run(ctx.on.start(), scenario.State()) + assert len(out.deferred) == 1 + assert out.deferred[0].name == 'start' +``` + +# Live charm introspection + +Scenario is a black-box, state-transition testing framework. It makes it trivial to assert that a status went from A to +B, but not to assert that, in the context of this charm execution, with this state, a certain charm-internal method was called and returned a +given piece of data, or would return this and that _if_ it had been called. + +The Scenario `Context` object can be used as a context manager for this use case specifically: + +```python notest +from charms.bar.lib_name.v1.charm_lib import CharmLib + + +class MyCharm(ops.CharmBase): + META = {"name": "mycharm"} + _stored = ops.StoredState() + + def __init__(self, framework): + super().__init__(framework) + self._stored.set_default(a="a") + self.my_charm_lib = CharmLib() + framework.observe(self.on.start, self._on_start) + + def _on_start(self, event): + self._stored.a = "b" + + +def test_live_charm_introspection(mycharm): + ctx = scenario.Context(mycharm, meta=mycharm.META) + with ctx(ctx.on.start(), scenario.State()) as manager: + # This is your charm instance, after ops has set it up: + charm: MyCharm = manager.charm + + # We can check attributes on nested Objects or the charm itself: + assert charm.my_charm_lib.foo == "foo" + # such as stored state: + assert charm._stored.a == "a" + + # This will tell ops.main to proceed with normal execution and emit the "start" event on the charm: + state_out = manager.run() + + # After that is done, we are handed back control, and we can again do some introspection: + assert charm.my_charm_lib.foo == "bar" + # and check that the charm's internal state is as we expect: + assert charm._stored.a == "b" + + # state_out is, as in regular scenario tests, a State object you can assert on: + assert state_out.unit_status == ... +``` + +Note that you can't call `manager.run()` multiple times: the object is a context that ensures that `ops.main` 'pauses' right +before emitting the event to hand you some introspection hooks, but for the rest this is a regular Scenario test: you +can't emit multiple events in a single charm execution. + +# The virtual charm root + +Before executing the charm, Scenario copies the charm's `/src`, any libs, the metadata, config, and actions `yaml`s to a temporary directory. The +charm will see that temporary directory as its 'root'. This allows us to keep things simple when dealing with metadata that can be +either inferred from the charm type being passed to `Context` or be passed to it as an argument, thereby overriding +the inferred one. This also allows you to test charms defined on the fly, as in: + +```python +class MyCharmType(ops.CharmBase): + pass + + +ctx = scenario.Context(charm_type=MyCharmType, meta={'name': 'my-charm-name'}) +ctx.run(ctx.on.start(), scenario.State()) +``` + +A consequence of this fact is that you have no direct control over the temporary directory that we are creating to put the metadata +you are passing to `.run()` (because `ops` expects it to be a file...). That is, unless you pass your own: + +```python +import tempfile + + +class MyCharmType(ops.CharmBase): + pass + + +td = tempfile.TemporaryDirectory() +ctx = scenario.Context( + charm_type=MyCharmType, + meta={'name': 'my-charm-name'}, + charm_root=td.name +) +state = ctx.run(ctx.on.start(), scenario.State()) +``` + +Do this, and you will be able to set up said directory as you like before the charm is run, as well as verify its +contents after the charm has run. Do keep in mind that any metadata files you create in it will be overwritten by Scenario, and therefore +ignored, if you pass any metadata keys to `Context`. Omit `meta` in the call +above, and Scenario will instead attempt to read metadata from the +temporary directory. + +# Immutability + +All of the data structures in `state`, e.g. `State, Relation, Container`, etc... are implemented as frozen dataclasses. + +This means that all components of the state that goes into a `context.run()` call are not mutated by the call, and the +state that you obtain in return is a different instance, and all parts of it have been (deep)copied. +This ensures that you can do delta-based comparison of states without worrying about them being mutated by Scenario. + +If you want to modify any of these data structures, you will need to either reinstantiate it from scratch, or use +the dataclasses `replace` api. + +```python +import dataclasses + +relation = scenario.Relation('foo', remote_app_data={"1": "2"}) +# make a copy of relation, but with remote_app_data set to {"3": "4"} +relation2 = dataclasses.replace(relation, remote_app_data={"3": "4"}) +``` + +# Consistency checks + +A Scenario, that is, the combination of an event, a state, and a charm, is consistent if it's plausible in JujuLand. For +example, Juju can't emit a `foo-relation-changed` event on your charm unless your charm has declared a `foo` relation +endpoint in its metadata. If that happens, that's a Juju bug. Scenario however assumes that Juju is bug-free, +therefore, so far as we're concerned, that can't happen, and therefore we help you verify that the scenarios you create +are consistent and raise an exception if that isn't so. + +That happens automatically behind the scenes whenever you trigger an event; +`scenario._consistency_checker.check_consistency` is called and verifies that the scenario makes sense. + +## Caveats: + +- False positives: not all checks are implemented yet; more will come. +- False negatives: it is possible that a scenario you know to be consistent is seen as inconsistent. That is probably a + bug in the consistency checker itself; please report it. +- Inherent limitations: if you have a custom event whose name conflicts with a builtin one, the consistency constraints + of the builtin one will apply. For example: if you decide to name your custom event `bar-pebble-ready`, but you are + working on a machine charm or don't have either way a `bar` container in your `metadata.yaml`, Scenario will flag that + as inconsistent. + +## Bypassing the checker + +If you have a clear false negative, are explicitly testing 'edge', inconsistent situations, or for whatever reason the +checker is in your way, you can set the `SCENARIO_SKIP_CONSISTENCY_CHECKS` envvar and skip it altogether. Hopefully you +don't need that. + +# Jhack integrations + +Up until `v5.6.0`, Scenario shipped with a cli tool called `snapshot`, used to interact with a live charm's state. +The functionality [has been moved over to `jhack`](https://github.com/PietroPasotti/jhack/pull/111), +to allow us to keep working on it independently, and to streamline +the profile of Scenario itself as it becomes more broadly adopted and ready for widespread usage. diff --git a/testing/src/ops_scenario.egg-info/SOURCES.txt b/testing/src/ops_scenario.egg-info/SOURCES.txt new file mode 100644 index 000000000..7f1426b79 --- /dev/null +++ b/testing/src/ops_scenario.egg-info/SOURCES.txt @@ -0,0 +1,52 @@ +CHANGES.md +CONTRIBUTING.md +README.md +UPGRADING.md +pyproject.toml +tox.ini +src/ops_scenario.egg-info/PKG-INFO +src/ops_scenario.egg-info/SOURCES.txt +src/ops_scenario.egg-info/dependency_links.txt +src/ops_scenario.egg-info/requires.txt +src/ops_scenario.egg-info/top_level.txt +src/scenario/__init__.py +src/scenario/_consistency_checker.py +src/scenario/_ops_main_mock.py +src/scenario/_runtime.py +src/scenario/context.py +src/scenario/errors.py +src/scenario/logger.py +src/scenario/mocking.py +src/scenario/py.typed +src/scenario/state.py +tests/__init__.py +tests/helpers.py +tests/readme-conftest.py +tests/test_charm_spec_autoload.py +tests/test_consistency_checker.py +tests/test_context.py +tests/test_context_on.py +tests/test_emitted_events_util.py +tests/test_plugin.py +tests/test_runtime.py +tests/test_e2e/__init__.py +tests/test_e2e/test_actions.py +tests/test_e2e/test_cloud_spec.py +tests/test_e2e/test_config.py +tests/test_e2e/test_deferred.py +tests/test_e2e/test_event.py +tests/test_e2e/test_juju_log.py +tests/test_e2e/test_manager.py +tests/test_e2e/test_network.py +tests/test_e2e/test_pebble.py +tests/test_e2e/test_play_assertions.py +tests/test_e2e/test_ports.py +tests/test_e2e/test_relations.py +tests/test_e2e/test_resource.py +tests/test_e2e/test_rubbish_events.py +tests/test_e2e/test_secrets.py +tests/test_e2e/test_state.py +tests/test_e2e/test_status.py +tests/test_e2e/test_storage.py +tests/test_e2e/test_stored_state.py +tests/test_e2e/test_vroot.py \ No newline at end of file diff --git a/testing/src/ops_scenario.egg-info/dependency_links.txt b/testing/src/ops_scenario.egg-info/dependency_links.txt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/testing/src/ops_scenario.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/testing/src/ops_scenario.egg-info/requires.txt b/testing/src/ops_scenario.egg-info/requires.txt new file mode 100644 index 000000000..99af883cb --- /dev/null +++ b/testing/src/ops_scenario.egg-info/requires.txt @@ -0,0 +1,2 @@ +ops~=2.17 +PyYAML>=6.0.1 diff --git a/testing/src/ops_scenario.egg-info/top_level.txt b/testing/src/ops_scenario.egg-info/top_level.txt new file mode 100644 index 000000000..de43fb96d --- /dev/null +++ b/testing/src/ops_scenario.egg-info/top_level.txt @@ -0,0 +1 @@ +scenario