diff --git a/ops/model.py b/ops/model.py index 029537751..86bb7d152 100644 --- a/ops/model.py +++ b/ops/model.py @@ -1790,6 +1790,14 @@ class UnknownStatus(StatusBase): A unit-agent has finished calling install, config-changed and start, but the charm has not called status-set yet. + This status is for READ ONLY and should not be used to set a unit status. The following + charm code would be an invalid charm code. + + ``` + # Raises ops.model.ModelError : ERROR invalid status "unknown", expected one of + # [maintenance blocked waiting active] + self.unit.status = UnknownStatus() + ``` """ name = 'unknown' @@ -1807,6 +1815,15 @@ class ErrorStatus(StatusBase): The unit-agent has encountered an error (the application or unit requires human intervention in order to operate correctly). + + This status is for READ ONLY and should not be used to set a unit status. The following + charm code would be an invalid charm code. + + ``` + # Raises ops.model.ModelError : ERROR invalid status "error", expected one of + # [maintenance blocked waiting active] + self.unit.status = ErrorStatus() + ``` """ name = 'error' diff --git a/ops/testing.py b/ops/testing.py index b41a646e0..fced2e310 100644 --- a/ops/testing.py +++ b/ops/testing.py @@ -446,7 +446,7 @@ def begin_with_initial_hooks(self) -> None: # the unit status from "Maintenance" to "Unknown". See gh#726 post_setup_sts = self._backend.status_get() if post_setup_sts.get("status") == "maintenance" and not post_setup_sts.get("message"): - self._backend.status_set("unknown", "", is_app=False) + self._backend.status_set("unknown", "", is_app=False, by_testing_backend=True) all_ids = list(self._backend._relation_names.items()) random.shuffle(all_ids) for rel_id, rel_name in all_ids: @@ -2222,7 +2222,17 @@ def status_get(self, *, is_app: bool = False): else: return self._unit_status - def status_set(self, status: '_StatusName', message: str = '', *, is_app: bool = False): + def status_set( + self, + status: "_StatusName", + message: str = "", + *, + is_app: bool = False, + by_testing_backend: bool = False, + ): + if not by_testing_backend and status in {model.ErrorStatus.name, model.UnknownStatus.name}: + raise model.ModelError(f'ERROR invalid status "{status}", expected one of' + ' [maintenance blocked waiting active]') if is_app: self._app_status = {'status': status, 'message': message} else: diff --git a/test/test_testing.py b/test/test_testing.py index 8ccc8e24d..1fc624d6c 100644 --- a/test/test_testing.py +++ b/test/test_testing.py @@ -2851,6 +2851,29 @@ def _on_collect_unit_status(self, event: ops.CollectStatusEvent): self.assertEqual(harness.model.app.status, ops.ActiveStatus('active app')) self.assertEqual(harness.model.unit.status, ops.ActiveStatus('active unit')) + def test_invalid_status_set(self): + + class TestCharm(ops.CharmBase): + def __init__(self, framework: ops.Framework): + super().__init__(framework) + self.framework.observe(self.on.collect_app_status, self._on_collect_app_status) + self.framework.observe(self.on.collect_unit_status, self._on_collect_unit_status) + self.app_status_to_add = ops.ErrorStatus('errored app') + self.unit_status_to_add = ops.ErrorStatus('errored unit') + + def _on_collect_app_status(self, event: ops.CollectStatusEvent): + event.add_status(self.app_status_to_add) + + def _on_collect_unit_status(self, event: ops.CollectStatusEvent): + event.add_status(self.unit_status_to_add) + + harness = ops.testing.Harness(TestCharm) + harness.set_leader(True) + harness.begin() + + with self.assertRaises(ops.model.ModelError): + harness.evaluate_status() + class TestNetwork(unittest.TestCase): def setUp(self):