Harness (legacy unit testing)¶
Deprecated since version 2.17: The Harness framework is deprecated and will be moved out of the base
install in a future ops release. Charm authors that don’t want to upgrade
will still be able to use it with pip install ops[harness]
.
The Harness API includes:
ops.testing.Harness
, a class to set up the simulated environment, that provides:add_relation()
method, to declare a relation (integration) with another app.begin()
andcleanup()
methods to start and end the testing lifecycle.evaluate_status()
method, which aggregates the status of the charm after test interactions.model
attribute, which exposes e.g. theunit
attribute for detailed assertions on the unit’s state.
Warning
The Harness API has flaws with resetting the charm state between Juju events. Care must be taken when emitting multiple events with the same Harness object.
Note
Unit testing is only one aspect of a comprehensive testing strategy. For more on testing charms, see Charm SDK | Testing.
- class ops.testing.ActionFailed(message: str, output: ActionOutput | None = None, *, state: State | None = None)[source]
Bases:
Exception
Raised when
event.fail()
is called during an action handler.- message: str
Optional details of the failure, as provided by
ops.ActionEvent.fail()
.
- output: ActionOutput
Any logs and results set by the Charm.
When using Context.run, both logs and results will be empty - these can be found in Context.action_logs and Context.action_results.
- class ops.testing.ActionOutput(logs: List[str], results: Dict[str, Any])[source]¶
Bases:
object
Contains the logs and results from a
Harness.run_action()
call.- logs: List[str]¶
Messages generated by the Charm using
ops.ActionEvent.log()
.
- results: Dict[str, Any]¶
The action’s results, as set or updated by
ops.ActionEvent.set_results()
.
- class ops.testing.ExecArgs(command: List[str], environment: Dict[str, str], working_dir: str | None, timeout: float | None, user_id: int | None, user: str | None, group_id: int | None, group: str | None, stdin: str | bytes | None, encoding: str | None, combine_stderr: bool)[source]¶
Bases:
object
Represent arguments captured from the
ops.Container.exec()
method call.These arguments will be passed to the
Harness.handle_exec()
handler function. Seeops.pebble.Client.exec()
for documentation of properties.
- class ops.testing.ExecResult(exit_code: int = 0, stdout: str | bytes = b'', stderr: str | bytes = b'')[source]¶
Bases:
object
Represents the result of a simulated process execution.
This class is typically used to return the output and exit code from the
Harness.handle_exec()
result or handler function.
- class ops.testing.Harness(charm_cls: Type[CharmType], *, meta: str | TextIO | None = None, actions: str | TextIO | None = None, config: str | TextIO | None = None)[source]¶
Bases:
Generic
[CharmType
]This class represents a way to build up the model that will drive a test suite.
The model created is from the viewpoint of the charm that is being tested.
Always call
harness.cleanup()
after creating aHarness
:@pytest.fixture() def harness(): harness = Harness(MyCharm) yield harness harness.cleanup()
Below is an example test using
begin_with_initial_hooks()
that ensures the charm responds correctly to config changes (the parameterharness
in the test function is a pytest fixture that does setup/teardown, seeHarness
):def test_foo(harness): # Instantiate the charm and trigger events that Juju would on startup harness.begin_with_initial_hooks() # Update charm config and trigger config-changed harness.update_config({'log_level': 'warn'}) # Check that charm properly handled config-changed, for example, # the charm added the correct Pebble layer plan = harness.get_container_pebble_plan('prometheus') assert '--log.level=warn' in plan.services['prometheus'].command
To set up the model without triggering events (or calling charm code), perform the harness actions before calling
begin()
. Below is an example that adds a relation before callingbegin
, and then updates config to trigger theconfig-changed
event in the charm (the parameterharness
in the test function is a pytest fixture that does setup/teardown, seeHarness
):def test_bar(harness): # Set up model before "begin" (no events triggered) harness.set_leader(True) harness.add_relation('db', 'postgresql', unit_data={'key': 'val'}) # Now instantiate the charm to start triggering events as the model changes harness.begin() harness.update_config({'some': 'config'}) # Check that charm has properly handled config-changed, for example, # has written the app's config file root = harness.get_filesystem_root('container') assert (root / 'etc' / 'app.conf').exists()
- Parameters:
charm_cls – The Charm class to test.
meta – A string or file-like object containing the contents of
metadata.yaml
. If not supplied, we will look for ametadata.yaml
file in the parent directory of the Charm, and if not found fall back to a trivialname: test-charm
metadata.actions – A string or file-like object containing the contents of
actions.yaml
. If not supplied, we will look for anactions.yaml
file in the parent directory of the Charm.config – A string or file-like object containing the contents of
config.yaml
. If not supplied, we will look for aconfig.yaml
file in the parent directory of the Charm.
- add_model_secret(owner: str | Application | Unit, content: Dict[str, str]) str [source]¶
Add a secret owned by the remote application or unit specified.
This is named
add_model_secret
instead ofadd_secret
to avoid confusion with theops.Application.add_secret()
andops.Unit.add_secret()
methods used by secret owner charms.- Parameters:
owner – The name of the remote application (or specific remote unit) that will own the secret.
content – A key-value mapping containing the payload of the secret, for example
{"password": "foo123"}
.
- Returns:
The ID of the newly-secret added.
- add_network(address: str, *, endpoint: str | None = None, relation_id: int | None = None, cidr: str | None = None, interface: str = 'eth0', ingress_addresses: Iterable[str] | None = None, egress_subnets: Iterable[str] | None = None)[source]¶
Add simulated network data for the given relation endpoint (binding).
Calling this multiple times with the same (binding, relation_id) combination will replace the associated network data.
Example:
# Set network info for default binding harness.add_network('10.0.0.10') # Or set network info for specific endpoint harness.add_network('10.0.0.10', endpoint='db')
After either of those calls, the following will be true (in the first case, the simulated network-get will fall back to the default binding):
binding = harness.model.get_binding('db') assert binding.network.bind_address == ipaddress.IPv4Address('10.0.0.10'))
- Parameters:
address – Binding’s IPv4 or IPv6 address.
endpoint – Name of relation endpoint (binding) to add network data for. If not provided, add info for the default binding.
relation_id – Relation ID for the binding. If provided, the endpoint argument must be provided and correspond. If not provided, add network data for the endpoint’s default binding.
cidr – Binding’s CIDR. Defaults to “<address>/24” if address is an IPv4 address, or “<address>/64” if address is IPv6 (the host bits are cleared).
interface – Name of network interface.
ingress_addresses – List of ingress addresses. Defaults to [address].
egress_subnets – List of egress subnets. Defaults to [cidr].
- Raises:
ModelError – If the endpoint is not a known relation name, or the relation_id is incorrect or doesn’t match the endpoint.
ValueError – If address is not an IPv4 or IPv6 address.
- add_oci_resource(resource_name: str, contents: Mapping[str, str] | None = None) None [source]¶
Add OCI resources to the backend.
This will register an OCI resource and create a temporary file for processing metadata about the resource. A default set of values will be used for all the file contents unless a specific contents dict is provided.
- Parameters:
resource_name – Name of the resource to add custom contents to.
contents – Optional custom dict to write for the named resource.
- add_relation(relation_name: str, remote_app: str, *, app_data: Mapping[str, str] | None = None, unit_data: Mapping[str, str] | None = None) int [source]¶
Declare that there is a new relation between this application and remote_app.
This function creates a relation with an application and triggers a
RelationCreatedEvent
. To match Juju’s behaviour, it also creates a default network binding on this endpoint. If you want to associate a custom network to this binding (or a global default network), provide one usingadd_network()
before calling this function.If app_data or unit_data are provided, also add a new unit (
<remote_app>/0
) to the relation and triggerRelationJoinedEvent
. Then update the application data if app_data is provided and the unit data if unit_data is provided, triggeringRelationChangedEvent
after each update. Alternatively, charm tests can calladd_relation_unit()
andupdate_relation_data()
explicitly.For peer relations defined in the charm’s metadata,
begin_with_initial_hooks()
will create them automatically, so the caller doesn’t need to calladd_relation()
. If the caller chooses to add a peer relation by themselves, make sure to calladd_relation()
beforebegin_with_initial_hooks()
so that Harness won’t create it again.Example usage:
secret_id = harness.add_model_secret('mysql', {'password': 'SECRET'}) harness.add_relation('db', 'mysql', unit_data={ 'host': 'mysql.localhost, 'username': 'appuser', 'secret-id': secret_id, })
- Parameters:
relation_name – The relation on the charm that is being integrated with.
remote_app – The name of the application that is being integrated with. To add a peer relation, set to the name of this application.
app_data – If provided, also add a new unit to the relation (triggering relation-joined) and set the application relation data (triggering relation-changed).
unit_data – If provided, also add a new unit to the relation (triggering relation-joined) and set the unit relation data (triggering relation-changed).
- Returns:
The ID of the relation created.
- add_relation_unit(relation_id: int, remote_unit_name: str) None [source]¶
Add a new unit to a relation.
This will trigger a relation_joined event. This would naturally be followed by a relation_changed event, which can be triggered with
update_relation_data()
. This separation is artificial in the sense that Juju will always fire the two, but is intended to make testing relations and their data bags slightly more natural.Unless finer-grained control is needed, most charm tests can call
add_relation()
with the app_data or unit_data argument instead of using this function.Example:
rel_id = harness.add_relation('db', 'postgresql') harness.add_relation_unit(rel_id, 'postgresql/0')
- Parameters:
relation_id – The integer relation identifier (as returned by
add_relation()
).remote_unit_name – A string representing the remote unit that is being added.
- add_resource(resource_name: str, content: AnyStr) None [source]¶
Add content for a resource to the backend.
This will register the content, so that a call to
model.resources.fetch(resource_name)
will return a path to a file containing that content.- Parameters:
resource_name – The name of the resource being added
content – Either string or bytes content, which will be the content of the filename returned by resource-get. If contents is a string, it will be encoded in utf-8
- add_storage(storage_name: str, count: int = 1, *, attach: bool = False) List[str] [source]¶
Create a new storage device and attach it to this unit.
To have repeatable tests, each device will be initialized with location set to /[tmpdir]/<storage_name>N, where N is the counter and will be a number from [0,total_num_disks-1].
The test harness uses symbolic links to imitate storage mounts, which may lead to some inconsistencies compared to the actual charm.
- Parameters:
storage_name – The storage backend name on the Charm
count – Number of disks being added
attach – True to also attach the storage mount; if
begin()
has been called a True value will also emit storage-attached
- Returns:
A list of storage IDs, e.g. [“my-storage/1”, “my-storage/2”].
- add_user_secret(content: Dict[str, str]) str [source]¶
Add a secret owned by the user, simulating the
juju add-secret
command.- Parameters:
content – A key-value mapping containing the payload of the secret, for example
{"password": "foo123"}
.- Returns:
The ID of the newly-added secret.
Example usage (the parameter
harness
in the test function is a pytest fixture that does setup/teardown, seeHarness
):# charmcraft.yaml config: options: mysec: type: secret description: "tell me your secrets" # charm.py class MyVMCharm(ops.CharmBase): def __init__(self, framework: ops.Framework): super().__init__(framework) framework.observe(self.on.config_changed, self._on_config_changed) def _on_config_changed(self, event: ops.ConfigChangedEvent): mysec = self.config.get('mysec') if mysec: sec = self.model.get_secret(id=mysec, label="mysec") self.config_from_secret = sec.get_content() # test_charm.py def test_config_changed(harness): secret_content = {'password': 'foo'} secret_id = harness.add_user_secret(secret_content) harness.grant_secret(secret_id, 'test-charm') harness.begin() harness.update_config({'mysec': secret_id}) secret = harness.model.get_secret(id=secret_id).get_content() assert harness.charm.config_from_secret == secret.get_content()
- attach_storage(storage_id: str) None [source]¶
Attach a storage device.
The intent of this function is to simulate a
juju attach-storage
call. If called afterbegin()
and hooks are not disabled, it will trigger a storage-attached hook if the storage unit in question exists and is presently marked as detached.The test harness uses symbolic links to imitate storage mounts, which may lead to some inconsistencies compared to the actual charm.
- Parameters:
storage_id – The full storage ID of the storage unit being attached, including the storage key, e.g. my-storage/0.
- begin() None [source]¶
Instantiate the Charm and start handling events.
Before calling
begin()
, there is no Charm instance, so changes to the Model won’t emit events. Callbegin()
forcharm
to be valid.Should only be called once.
- begin_with_initial_hooks() None [source]¶
Fire the same hooks that Juju would fire at startup.
This triggers install, relation-created, config-changed, start, pebble-ready (for any containers), and any relation-joined hooks based on what relations have been added before begin was called. Note that all of these are fired before returning control to the test suite, so to introspect what happens at each step, fire them directly (for example,
Charm.on.install.emit()
).To use this with all the normal hooks, instantiate the harness, setup any relations that should be active when the charm starts, and then call this method. This method will automatically create and add peer relations that are specified in metadata.yaml.
If the charm metadata specifies containers, this sets can_connect to True for all containers (in addition to triggering pebble-ready for each).
Example:
harness = Harness(MyCharm) # Do initial setup here # Add storage if needed before begin_with_initial_hooks() is called storage_ids = harness.add_storage('data', count=1)[0] storage_id = storage_id[0] # we only added one storage instance harness.add_relation('db', 'postgresql', unit_data={'key': 'val'}) harness.set_leader(True) harness.update_config({'initial': 'config'}) harness.begin_with_initial_hooks() # This will cause # install, db-relation-created('postgresql'), leader-elected, config-changed, start # db-relation-joined('postgresql/0'), db-relation-changed('postgresql/0') # To be fired.
- property charm: CharmType¶
Return the instance of the charm class that was passed to
__init__
.Note that the Charm is not instantiated until
begin()
is called. Until then, attempting to access this property will raise an exception.
- cleanup() None [source]¶
Called by the test infrastructure to clean up any temporary directories/files/etc.
Always call
self.addCleanup(harness.cleanup)
after creating aHarness
.
- container_pebble_ready(container_name: str)[source]¶
Fire the pebble_ready hook for the associated container.
This will switch the given container’s
can_connect
state to True before the hook function is called.It will do nothing if
begin()
has not been called.
- detach_storage(storage_id: str) None [source]¶
Detach a storage device.
The intent of this function is to simulate a
juju detach-storage
call. It will trigger a storage-detaching hook if the storage unit in question exists and is presently marked as attached.Note that the Charm is not instantiated until
begin()
is called. Until then, attempting to use this method will raise an exception.- Parameters:
storage_id – The full storage ID of the storage unit being detached, including the storage key, e.g. my-storage/0.
- disable_hooks() None [source]¶
Stop emitting hook events when the model changes.
This can be used by developers to stop changes to the model from emitting events that the charm will react to. Call
enable_hooks()
to re-enable them.
- enable_hooks() None [source]¶
Re-enable hook events from charm.on when the model is changed.
By default, hook events are enabled once
begin()
is called, but ifdisable_hooks()
is used, this method will enable them again.
- evaluate_status() None [source]¶
Trigger the collect-status events and set application and/or unit status.
This will always trigger
collect_unit_status
, and set the unit status if any statuses were added.If running on the leader unit (
set_leader()
has been called withTrue
), this will triggercollect_app_status
, and set the application status if any statuses were added.Tests should normally call this and then assert that
self.model.app.status
orself.model.unit.status
is the value expected.Evaluation is not “additive”; this method resets the added statuses before triggering each collect-status event.
- get_container_pebble_plan(container_name: str) Plan [source]¶
Return the current plan that Pebble is executing for the given container.
- Parameters:
container_name – The simple name of the associated container
- Returns:
The Pebble plan for this container. Use
Plan.to_yaml
to get a string form for the content.- Raises:
KeyError – if no Pebble client exists for that container name (should only happen if container is not present in
metadata.yaml
).
- get_filesystem_root(container: str | Container) Path [source]¶
Return the temp directory path harness will use to simulate the container filesystem.
In a real container runtime, each container has an isolated root filesystem. To simulate this behaviour, the testing harness manages a temporary directory for each container. Any Pebble filesystem API calls will be translated and mapped to this directory, as if the directory was the container’s filesystem root.
This process is quite similar to the
chroot
command. Charm tests should treat the returned directory as the container’s root directory (/
). The testing harness will not create any files or directories inside the simulated container’s root directory; it’s up to the test to populate the container’s root directory with any files or directories the charm needs.Regarding the file ownership: unprivileged users are unable to create files with distinct ownership. To circumvent this limitation, the testing harness maps all user and group options related to file operations to match the current user and group.
Example usage (the parameter
harness
in the test function is a pytest fixture that does setup/teardown, seeHarness
):# charm.py class ExampleCharm(ops.CharmBase): def __init__(self, *args): super().__init__(*args) self.framework.observe(self.on["mycontainer"].pebble_ready, self._on_pebble_ready) def _on_pebble_ready(self, event: ops.PebbleReadyEvent): self.hostname = event.workload.pull("/etc/hostname").read() # test_charm.py def test_hostname(harness): root = harness.get_filesystem_root("mycontainer") (root / "etc").mkdir() (root / "etc" / "hostname").write_text("hostname.example.com") harness.begin_with_initial_hooks() assert harness.charm.hostname == "hostname.example.com"
- Parameters:
container – The name of the container or the container instance.
- Returns:
The path of the temporary directory associated with the specified container.
- get_pod_spec() Tuple[Mapping[Any, Any], Mapping[Any, Any]] [source]¶
Return the content of the pod spec as last set by the charm.
This returns both the pod spec and any k8s_resources that were supplied. See the signature of
Pod.set_spec
.
- get_relation_data(relation_id: int, app_or_unit: str | Application | Unit) Mapping[str, str] [source]¶
Get the relation data bucket for a single app or unit in a given relation.
This ignores all of the safety checks of who can and can’t see data in relations (eg, non-leaders can’t read their own application’s relation data because there are no events that keep that data up-to-date for the unit).
- Parameters:
relation_id – The relation whose content we want to look at.
app_or_unit – An
Application
orUnit
instance, or its name, whose data we want to read.
- Returns:
A dict containing the relation data for
app_or_unit
or None.- Raises:
KeyError – if
relation_id
doesn’t exist
- get_secret_grants(secret_id: str, relation_id: int) Set[str] [source]¶
Return the set of app and unit names granted to secret for this relation.
- Parameters:
secret_id – The ID of the secret to get grants for.
relation_id – The ID of the relation granted access.
- get_secret_revisions(secret_id: str) List[int] [source]¶
Return the list of revision IDs for the given secret, oldest first.
- Parameters:
secret_id – The ID of the secret to get revisions for.
- grant_secret(secret_id: str, observer: str | Application | Unit)[source]¶
Grant read access to this secret for the given observer application or unit.
For user secrets, grant access to the application, simulating the
juju grant-secret
command.If the given application or unit has already been granted access to this secret, do nothing.
- Parameters:
secret_id – The ID of the secret to grant access to. This should normally be the return value of
add_model_secret()
.observer – The name of the application (or specific unit) to grant access to. A relation between this application and the charm under test must already have been created.
- handle_exec(container: str | Container, command_prefix: Sequence[str], *, handler: Callable[[ExecArgs], None | ExecResult] | None = None, result: int | str | bytes | ExecResult | None = None)[source]¶
Register a handler to simulate the Pebble command execution.
This allows a test harness to simulate the behavior of running commands in a container. When
ops.Container.exec()
is triggered, the registered handler is used to generate stdout and stderr for the simulated execution.A
handler
or aresult
may be provided, but not both:A
handler
is a function acceptingops.testing.ExecArgs
and returningops.testing.ExecResult
as the simulated process outcome. For cases that have side effects but don’t return output, the handler can returnNone
, which is equivalent to returningExecResult()
.A
result
is for simulations that don’t need to inspect theexec
arguments; the output or exit code is provided directly. Settingresult
to str or bytes means use that string as stdout (with exit code 0); settingresult
to int means return that exit code (and no stdout).
If
handle_exec
is called more than once with overlapping command prefixes, the longest match takes precedence. The registration of an execution handler can be updated by re-registering with the same command prefix.The execution handler receives the timeout value in the
ExecArgs
. If needed, it can raise aTimeoutError
to inform the harness that a timeout occurred.If
ops.Container.exec()
is called withcombine_stderr=True
, the execution handler should, if required, weave the simulated standard error into the standard output. The harness checks the result and will raise an exception if stderr is non-empty.- Parameters:
container – The specified container or its name.
command_prefix – The command prefix to register against.
handler – A handler function that simulates the command’s execution.
result – A simplified form to specify the command’s simulated result.
Example usage:
# produce no output and return 0 for every command harness.handle_exec('container', [], result=0) # simple example that just produces output (exit code 0) harness.handle_exec('webserver', ['ls', '/etc'], result='passwd\nprofile\n') # slightly more complex (use stdin) harness.handle_exec( 'c1', ['sha1sum'], handler=lambda args: ExecResult(stdout=hashlib.sha1(args.stdin).hexdigest())) # more complex example using args.command def docker_handler(args: testing.ExecArgs) -> testing.ExecResult: match args.command: case ['docker', 'run', image]: return testing.ExecResult(stdout=f'running {image}') case ['docker', 'ps']: return testing.ExecResult(stdout='CONTAINER ID IMAGE ...') case _: return testing.ExecResult(exit_code=1, stderr='unknown command') harness.handle_exec('database', ['docker'], handler=docker_handler) # handle timeout def handle_timeout(args: testing.ExecArgs) -> int: if args.timeout is not None and args.timeout < 10: raise TimeoutError return 0 harness.handle_exec('database', ['foo'], handler=handle_timeout)
- hooks_disabled()[source]¶
A context manager to run code with hooks disabled.
Example:
with harness.hooks_disabled(): # things in here don't fire events harness.set_leader(True) harness.update_config(unset=['foo', 'bar']) # things here will again fire events
- pebble_notify(container_name: str, key: str, *, data: Dict[str, str] | None = None, repeat_after: timedelta | None = None, type: NoticeType = NoticeType.CUSTOM) str [source]¶
Record a Pebble notice with the specified key and data.
If
begin()
has been called and the notice is new or was repeated, this will trigger a notice event of the appropriate type, for exampleops.PebbleCustomNoticeEvent
.- Parameters:
container_name – Name of workload container.
key – Notice key; must be in “example.com/path” format.
data – Data fields for this notice.
repeat_after – Only allow this notice to repeat after this duration has elapsed (the default is to always repeat).
type – Notice type (currently only “custom” notices are supported).
- Returns:
The notice’s ID.
- property reboot_count: int¶
Number of times the charm has called
ops.Unit.reboot()
.
- remove_relation(relation_id: int) None [source]¶
Remove a relation.
- Parameters:
relation_id – The relation ID for the relation to be removed.
- Raises:
RelationNotFoundError – if relation id is not valid
- remove_relation_unit(relation_id: int, remote_unit_name: str) None [source]¶
Remove a unit from a relation.
Example:
rel_id = harness.add_relation('db', 'postgresql') harness.add_relation_unit(rel_id, 'postgresql/0') ... harness.remove_relation_unit(rel_id, 'postgresql/0')
This will trigger a relation_departed event. This would normally be followed by a relation_changed event triggered by Juju. However, when using the test harness, a relation_changed event must be triggered using
update_relation_data()
. This deviation from normal Juju behaviour facilitates testing by making each step in the charm life cycle explicit.- Parameters:
relation_id – The integer relation identifier (as returned by
add_relation()
).remote_unit_name – A string representing the remote unit that is being removed.
- remove_storage(storage_id: str) None [source]¶
Detach a storage device.
The intent of this function is to simulate a
juju remove-storage
call. It will trigger a storage-detaching hook if the storage unit in question exists and is presently marked as attached. Then it will remove the storage unit from the testing backend.- Parameters:
storage_id – The full storage ID of the storage unit being removed, including the storage key, e.g. my-storage/0.
- Raises:
RuntimeError – if the storage is not in the metadata.
- reset_planned_units() None [source]¶
Reset the planned units override.
This allows the harness to fall through to the built in methods that will try to guess at a value for planned units, based on the number of peer relations that have been setup in the testing harness.
- revoke_secret(secret_id: str, observer: str | Application | Unit)[source]¶
Revoke read access to this secret for the given observer application or unit.
If the given application or unit does not have access to this secret, do nothing.
- Parameters:
secret_id – The ID of the secret to revoke access for. This should normally be the return value of
add_model_secret()
.observer – The name of the application (or specific unit) to revoke access to. A relation between this application and the charm under test must have already been created.
- run_action(action_name: str, params: Dict[str, Any] | None = None) ActionOutput [source]¶
Simulates running a charm action, as with
juju run
.Use this only after calling
begin()
.Validates that no required parameters are missing, and that additional parameters are not provided if that is not permitted. Does not validate the types of the parameters - you can use the jsonschema package to do this in your tests; for example:
schema = harness.charm.meta.actions["action-name"].parameters try: jsonschema.validate(instance=params, schema=schema) except jsonschema.ValidationError: # Do something about the invalid params. ... harness.run_action("action-name", params)
- Parameters:
action_name – the name of the action to run, as found in
actions.yaml
.params – override the default parameter values found in
actions.yaml
. If a parameter is not inparams
, orparams
isNone
, then the default value fromactions.yaml
will be used.
- Raises:
ActionFailed – if
ops.ActionEvent.fail()
is called. Note that this will be raised at the end of therun_action
call, not immediately whenfail()
is called, to match the run-time behaviour.
- set_can_connect(container: str | Container, val: bool)[source]¶
Change the simulated connection status of a container’s underlying Pebble client.
After calling this,
ops.Container.can_connect()
will return val.
- set_cloud_spec(spec: CloudSpec)[source]¶
Set cloud specification (metadata) including credentials.
Call this method before the charm calls
ops.Model.get_cloud_spec()
.Example usage (the parameter
harness
in the test function is a pytest fixture that does setup/teardown, seeHarness
):# 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() # test_charm.py def test_start(harness): cloud_spec = ops.model.CloudSpec.from_dict({ 'name': 'localhost', 'type': 'lxd', 'endpoint': 'https://127.0.0.1:8443', 'credential': { 'auth-type': 'certificate', 'attrs': { 'client-cert': 'foo', 'client-key': 'bar', 'server-cert': 'baz' }, }, }) harness.set_cloud_spec(cloud_spec) harness.begin() harness.charm.on.start.emit() assert harness.charm.cloud_spec == cloud_spec
- set_leader(is_leader: bool = True) None [source]¶
Set whether this unit is the leader or not.
If this charm becomes a leader then leader_elected will be triggered. If
begin()
has already been called, then the charm’s peer relation should usually be added prior to calling this method (withadd_relation()
) to properly initialise and make available relation data that leader elected hooks may want to access.- Parameters:
is_leader – Whether this unit is the leader.
- set_model_info(name: str | None = None, uuid: str | None = None) None [source]¶
Set the name and UUID of the model that this is representing.
Cannot be called once
begin()
has been called. Use it to set the value that will be returned byModel.name
andModel.uuid
.This is a convenience method to invoke both
set_model_name()
andset_model_uuid()
at once.
- set_model_name(name: str) None [source]¶
Set the name of the Model that this is representing.
Cannot be called once
begin()
has been called. Use it to set the value that will be returned byModel.name
.
- set_model_uuid(uuid: str) None [source]¶
Set the uuid of the Model that this is representing.
Cannot be called once
begin()
has been called. Use it to set the value that will be returned byModel.uuid
.
- set_planned_units(num_units: int) None [source]¶
Set the number of “planned” units.
This is the value that
Application.planned_units
should return.In real world circumstances, this number will be the number of units in the application. That is, this number will be the number of peers this unit has, plus one, as we count our own unit in the total.
A change to the return from
planned_units
will not generate an event. Typically, a charm author would check planned units during a config or install hook, or after receiving a peer relation joined event.
- set_secret_content(secret_id: str, content: Dict[str, str])[source]¶
Update a secret’s content, add a new revision, and fire secret-changed.
- Parameters:
secret_id – The ID of the secret to update. This should normally be the return value of
add_model_secret()
.content – A key-value mapping containing the new payload.
- trigger_secret_expiration(secret_id: str, revision: int, *, label: str | None = None)[source]¶
Trigger a secret-expired event for the given secret.
This event is fired by Juju when a secret’s expiration time elapses, however, time-based events cannot be simulated appropriately in the harness, so this fires it manually.
- Parameters:
secret_id – The ID of the secret associated with the event.
revision – Revision number to provide to the event. This should be an item from the list returned by
get_secret_revisions()
.label – Label value to send to the event. If None, the secret’s label is used.
- trigger_secret_removal(secret_id: str, revision: int, *, label: str | None = None)[source]¶
Trigger a secret-remove event for the given secret and revision.
This event is fired by Juju for a specific revision when all the secret’s observers have refreshed to a later revision, however, in the harness call this method to fire the event manually.
- Parameters:
secret_id – The ID of the secret associated with the event.
revision – Revision number to provide to the event. This should be an item from the list returned by
get_secret_revisions()
.label – Label value to send to the event. If None, the secret’s label is used.
- trigger_secret_rotation(secret_id: str, *, label: str | None = None)[source]¶
Trigger a secret-rotate event for the given secret.
This event is fired by Juju when a secret’s rotation time elapses, however, time-based events cannot be simulated appropriately in the harness, so this fires it manually.
- Parameters:
secret_id – The ID of the secret associated with the event.
label – Label value to send to the event. If None, the secret’s label is used.
- update_config(key_values: Mapping[str, str | int | float | bool] | None = None, unset: Iterable[str] = ()) None [source]¶
Update the config as seen by the charm.
This will trigger a config_changed event.
Note that the
key_values
mapping will only add or update configuration items. To remove existing ones, see theunset
parameter.- Parameters:
key_values – A Mapping of key:value pairs to update in config.
unset – An iterable of keys to remove from config. This sets the value to the default if defined, otherwise removes the key altogether.
- Raises:
ValueError – if the key is not present in the config.
- update_relation_data(relation_id: int, app_or_unit: str, key_values: Mapping[str, str]) None [source]¶
Update the relation data for a given unit or application in a given relation.
This also triggers the relation_changed event for the given
relation_id
.Unless finer-grained control is needed, most charm tests can call
add_relation()
with the app_data or unit_data argument instead of using this function.- Parameters:
relation_id – The integer relation ID representing this relation.
app_or_unit – The unit or application name that is being updated. This can be the local or remote application.
key_values – Each key/value will be updated in the relation data.