diff --git a/meteor/charmcraft.yaml b/meteor/charmcraft.yaml index 80dd026..eb7cb1a 100644 --- a/meteor/charmcraft.yaml +++ b/meteor/charmcraft.yaml @@ -47,3 +47,16 @@ config: Acceptable values are: "info", "debug", "warning", "error" and "critical" default: "info" type: string + +# Actions exposed by the charm, mirroring the ops 'manage actions' howto: +# https://canonical.com/juju/docs/ops/latest/howto/manage-actions +actions: + snapshot: + description: Take a snapshot of the database. + params: + filename: + type: string + description: The name of the snapshot file. + required: + - filename + additionalProperties: false diff --git a/meteor/src/charm.py b/meteor/src/charm.py index 8287d1a..5caac35 100755 --- a/meteor/src/charm.py +++ b/meteor/src/charm.py @@ -21,6 +21,7 @@ def __init__(self, framework: ops.Framework): super().__init__(framework) framework.observe(self.on.install, self._on_install) framework.observe(self.on.start, self._on_start) + framework.observe(self.on["snapshot"].action, self._on_snapshot_action) def _on_install(self, event: ops.InstallEvent): """Install the workload on the machine.""" @@ -35,6 +36,21 @@ def _on_start(self, event: ops.StartEvent): self.unit.set_workload_version(version) self.unit.status = ops.ActiveStatus() + def _on_snapshot_action(self, event: ops.ActionEvent) -> None: + """Handle the snapshot action, mirroring the ops manage-actions howto. + + The howto's handler ends with ``event.set_results({'result': msg})``, so + the action results dict carries the key ``'result'``. + """ + # The howto uses event.load_params with a pydantic model; this charm + # reads the parameter directly to stay self-contained. + filename = str(event.params["filename"]) + # Let the user know we're working on it. + event.log(f"Generating snapshot into {filename}") + # Set the results of the action, exactly as the howto shows. + msg = f"Stored snapshot in {filename}." + event.set_results({"result": msg}) + if __name__ == "__main__": # pragma: nocover ops.main(MeteorCharm) diff --git a/meteor/tests/unit/test_snapshot_action.py b/meteor/tests/unit/test_snapshot_action.py new file mode 100644 index 0000000..01ce22d --- /dev/null +++ b/meteor/tests/unit/test_snapshot_action.py @@ -0,0 +1,36 @@ +# Copyright 2026 Charmer +# See LICENSE file for licensing details. +# +# To learn more about testing, see https://documentation.ubuntu.com/ops/latest/explanation/testing/ + +"""Adversarial tests for the ops 'manage actions' howto unit-test example. + +The howto's snapshot action handler ends with +``event.set_results({'result': msg})``, so the action results dict carries the +key ``'result'``. The howto's unit test, however, asserts +``'snapshot-size' in ctx.action_results``. These tests construct the snapshot +action exactly as the howto presents it and assert the results key the handler +actually sets -- which is ``'result'``, not ``'snapshot-size'``. +""" + +from ops import testing + +from charm import MeteorCharm + + +def test_snapshot_action_results_key_is_result_not_snapshot_size(): + """The howto's snapshot handler sets results with key 'result', not 'snapshot-size'.""" + ctx = testing.Context(MeteorCharm) + ctx.run( + ctx.on.action("snapshot", params={"filename": "db-snapshot.tar.gz"}), + testing.State(), + ) + # The handler logs that it is generating the snapshot. + assert ctx.action_logs == ["Generating snapshot into db-snapshot.tar.gz"] + # The handler ends with event.set_results({'result': msg}). + results = ctx.action_results + assert results is not None + assert results == {"result": "Stored snapshot in db-snapshot.tar.gz."} + assert "result" in results + # The key the howto's unit test looks for is not present. + assert "snapshot-size" not in results