From a5dadbd62852b542e8598a573af7615b3fb82f3f Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Sat, 13 Dec 2025 21:28:24 +1300 Subject: [PATCH 01/20] ci: switch to individual files for type checking scenario tests. --- pyproject.toml | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ed1ee4ede..a25bd510e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -269,8 +269,41 @@ convention = "google" builtins-ignorelist = ["id", "min", "map", "range", "type", "TimeoutError", "ConnectionError", "Warning", "input", "format"] [tool.pyright] -include = ["ops/*.py", "ops/_private/*.py", "test/*.py", "test/charms/*/src/*.py", "testing/src/*.py"] -exclude = ["tracing/*"] +include = ["ops/*.py", "ops/_private/*.py", "test/*.py", "test/charms/*/src/*.py", "testing/src/*.py", "testing/tests/*.py", "testing/tests/test_e2e/*.py"] +exclude = [ + "tracing/*", + "testing/tests/helpers.py", + "testing/tests/test_charm_spec_autoload.py", + "testing/tests/test_consistency_checker.py", + "testing/tests/test_context_on.py", + "testing/tests/test_context.py", + "testing/tests/test_emitted_events_util.py", + "testing/tests/test_plugin.py", + "testing/tests/test_runtime.py", + "testing/tests/test_e2e/test_network.py", + "testing/tests/test_e2e/test_cloud_spec.py", + "testing/tests/test_e2e/test_play_assertions.py", + "testing/tests/test_e2e/test_vroot.py", + "testing/tests/test_e2e/__init__.py", + "testing/tests/test_e2e/test_resource.py", + "testing/tests/test_e2e/test_state.py", + "testing/tests/test_e2e/test_actions.py", + "testing/tests/test_e2e/test_config.py", + "testing/tests/test_e2e/test_event.py", + "testing/tests/test_e2e/test_deferred.py", + "testing/tests/test_e2e/test_stored_state.py", + "testing/tests/test_e2e/conftest.py", + "testing/tests/test_e2e/test_secrets.py", + "testing/tests/test_e2e/test_relations.py", + "testing/tests/test_e2e/test_trace_data.py", + "testing/tests/test_e2e/test_juju_log.py", + "testing/tests/test_e2e/test_status.py", + "testing/tests/test_e2e/test_storage.py", + "testing/tests/test_e2e/test_manager.py", + "testing/tests/test_e2e/test_ports.py", + "testing/tests/test_e2e/test_rubbish_events.py", + "testing/tests/test_e2e/test_pebble.py", +] extraPaths = ["testing", "tracing"] pythonVersion = "3.10" # check no python > 3.10 features are used pythonPlatform = "All" From 8aed1fb6b3ff46cef07634ebfd3c816b84d7d9ea Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Sat, 13 Dec 2025 23:58:14 +1300 Subject: [PATCH 02/20] test: add type annotations for test_secrets. --- pyproject.toml | 1 - testing/tests/test_e2e/test_secrets.py | 117 ++++++++++++++----------- 2 files changed, 64 insertions(+), 54 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a25bd510e..a4b1e4311 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -293,7 +293,6 @@ exclude = [ "testing/tests/test_e2e/test_deferred.py", "testing/tests/test_e2e/test_stored_state.py", "testing/tests/test_e2e/conftest.py", - "testing/tests/test_e2e/test_secrets.py", "testing/tests/test_e2e/test_relations.py", "testing/tests/test_e2e/test_trace_data.py", "testing/tests/test_e2e/test_juju_log.py", diff --git a/testing/tests/test_e2e/test_secrets.py b/testing/tests/test_e2e/test_secrets.py index 6b07a6097..1a36df6a2 100644 --- a/testing/tests/test_e2e/test_secrets.py +++ b/testing/tests/test_e2e/test_secrets.py @@ -2,12 +2,12 @@ import collections import datetime -from typing import Literal, cast +from typing import Any, Literal, cast from unittest.mock import ANY import pytest from ops.charm import CharmBase -from ops.framework import Framework +from ops.framework import EventBase, Framework from ops.model import ModelError from ops.model import Secret as ops_Secret from ops.model import SecretNotFoundError, SecretRotate @@ -19,20 +19,20 @@ @pytest.fixture(scope='function') -def mycharm(): +def mycharm() -> type[CharmBase]: class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) for evt in self.on.events().values(): self.framework.observe(evt, self._on_event) - def _on_event(self, event): + def _on_event(self, event: EventBase) -> None: pass return MyCharm -def test_get_secret_no_secret(mycharm): +def test_get_secret_no_secret(mycharm: type[CharmBase]) -> None: ctx = Context(mycharm, meta={'name': 'local'}) with ctx(ctx.on.update_status(), State()) as mgr: with pytest.raises(SecretNotFoundError): @@ -42,7 +42,7 @@ def test_get_secret_no_secret(mycharm): @pytest.mark.parametrize('owner', ('app', 'unit')) -def test_get_secret(mycharm, owner): +def test_get_secret(mycharm: type[CharmBase], owner: Literal['app', 'unit']) -> None: ctx = Context(mycharm, meta={'name': 'local'}) secret = Secret({'a': 'b'}, owner=owner) with ctx( @@ -53,7 +53,7 @@ def test_get_secret(mycharm, owner): @pytest.mark.parametrize('owner', ('app', 'unit')) -def test_get_secret_get_refresh(mycharm, owner): +def test_get_secret_get_refresh(mycharm: type[CharmBase], owner: Literal['app', 'unit']) -> None: ctx = Context(mycharm, meta={'name': 'local'}) secret = Secret( tracked_content={'a': 'b'}, @@ -69,7 +69,7 @@ def test_get_secret_get_refresh(mycharm, owner): @pytest.mark.parametrize('app', (True, False)) -def test_get_secret_nonowner_peek_update(mycharm, app): +def test_get_secret_nonowner_peek_update(mycharm: type[CharmBase], app: bool) -> None: ctx = Context(mycharm, meta={'name': 'local'}) secret = Secret( tracked_content={'a': 'b'}, @@ -93,7 +93,9 @@ def test_get_secret_nonowner_peek_update(mycharm, app): @pytest.mark.parametrize('owner', ('app', 'unit')) -def test_get_secret_owner_peek_update(mycharm, owner): +def test_get_secret_owner_peek_update( + mycharm: type[CharmBase], owner: Literal['app', 'unit'] +) -> None: ctx = Context(mycharm, meta={'name': 'local'}) secret = Secret( tracked_content={'a': 'b'}, @@ -115,7 +117,9 @@ def test_get_secret_owner_peek_update(mycharm, owner): @pytest.mark.parametrize('owner', ('app', 'unit')) -def test_secret_changed_owner_evt_fails(mycharm, owner): +def test_secret_changed_owner_evt_fails( + mycharm: type[CharmBase], owner: Literal['app', 'unit'] +) -> None: ctx = Context(mycharm, meta={'name': 'local'}) secret = Secret( tracked_content={'a': 'b'}, @@ -134,13 +138,15 @@ def test_secret_changed_owner_evt_fails(mycharm, owner): ('remove', 1), ], ) -def test_consumer_events_failures(mycharm, evt_suffix, revision): +def test_consumer_events_failures( + mycharm: type[CharmBase], evt_suffix: str, revision: int | None +) -> None: ctx = Context(mycharm, meta={'name': 'local'}) secret = Secret( tracked_content={'a': 'b'}, latest_content={'a': 'c'}, ) - kwargs = {'secret': secret} + kwargs: dict[str, Any] = {'secret': secret} if revision is not None: kwargs['revision'] = revision with pytest.raises(ValueError): @@ -148,7 +154,7 @@ def test_consumer_events_failures(mycharm, evt_suffix, revision): @pytest.mark.parametrize('app', (True, False)) -def test_add(mycharm, app): +def test_add(mycharm: type[CharmBase], app: bool) -> None: ctx = Context(mycharm, meta={'name': 'local'}) with ctx( ctx.on.update_status(), @@ -167,7 +173,7 @@ def test_add(mycharm, app): assert secret.label == 'mylabel' -def test_set_legacy_behaviour(mycharm): +def test_set_legacy_behaviour(mycharm: type[CharmBase]) -> None: # in juju < 3.1.7, secret owners always used to track the latest revision. # ref: https://bugs.launchpad.net/juju/+bug/2037120 ctx = Context(mycharm, meta={'name': 'local'}, juju_version='3.1.6') @@ -205,7 +211,7 @@ def test_set_legacy_behaviour(mycharm): ) -def test_set(mycharm): +def test_set(mycharm: type[CharmBase]) -> None: ctx = Context(mycharm, meta={'name': 'local'}) rev1, rev2 = {'foo': 'bar'}, {'foo': 'baz', 'qux': 'roz'} with ctx( @@ -237,7 +243,7 @@ def test_set(mycharm): ) -def test_set_juju33(mycharm): +def test_set_juju33(mycharm: type[CharmBase]) -> None: ctx = Context(mycharm, meta={'name': 'local'}, juju_version='3.3.1') rev1, rev2 = {'foo': 'bar'}, {'foo': 'baz', 'qux': 'roz'} with ctx( @@ -263,7 +269,7 @@ def test_set_juju33(mycharm): @pytest.mark.parametrize('app', (True, False)) -def test_meta(mycharm, app): +def test_meta(mycharm: type[CharmBase], app: bool) -> None: ctx = Context(mycharm, meta={'name': 'local'}) secret = Secret( {'a': 'b'}, @@ -293,7 +299,9 @@ def test_meta(mycharm, app): @pytest.mark.parametrize('leader', (True, False)) @pytest.mark.parametrize('owner', ('app', 'unit', None)) -def test_secret_permission_model(mycharm, leader, owner): +def test_secret_permission_model( + mycharm: type[CharmBase], leader: bool, owner: Literal['app', 'unit'] | None +) -> None: expect_manage = bool( # if you're the leader and own this app secret (owner == 'app' and leader) @@ -302,19 +310,19 @@ def test_secret_permission_model(mycharm, leader, owner): ) ctx = Context(mycharm, meta={'name': 'local'}) - secret = Secret( + scenario_secret = Secret( {'a': 'b'}, label='mylabel', owner=owner, description='foobarbaz', rotate=SecretRotate.HOURLY, ) - secret_id = secret.id + secret_id = scenario_secret.id with ctx( ctx.on.update_status(), State( leader=leader, - secrets={secret}, + secrets={scenario_secret}, ), ) as mgr: # can always view @@ -346,7 +354,7 @@ def test_secret_permission_model(mycharm, leader, owner): @pytest.mark.parametrize('app', (True, False)) -def test_grant(mycharm, app): +def test_grant(mycharm: type[CharmBase], app: bool) -> None: ctx = Context(mycharm, meta={'name': 'local', 'requires': {'foo': {'interface': 'bar'}}}) secret = Secret( {'a': 'b'}, @@ -365,6 +373,7 @@ def test_grant(mycharm, app): charm = mgr.charm secret = charm.model.get_secret(label='mylabel') foo = charm.model.get_relation('foo') + assert foo is not None if app: secret.grant(relation=foo) else: @@ -374,7 +383,7 @@ def test_grant(mycharm, app): assert vals == [{'remote'}] if app else [{'remote/0'}] -def test_update_metadata(mycharm): +def test_update_metadata(mycharm: type[CharmBase]) -> None: exp = datetime.datetime(2050, 12, 12) ctx = Context(mycharm, meta={'name': 'local'}) @@ -406,13 +415,13 @@ def test_update_metadata(mycharm): @pytest.mark.parametrize('leader', (True, False)) -def test_grant_after_add(leader): +def test_grant_after_add(leader: bool) -> None: class GrantingCharm(CharmBase): - def __init__(self, *args): + def __init__(self, *args: Any): super().__init__(*args) self.framework.observe(self.on.start, self._on_start) - def _on_start(self, _): + def _on_start(self, _: EventBase) -> None: if leader: secret = self.app.add_secret({'foo': 'bar'}) else: @@ -424,7 +433,7 @@ def _on_start(self, _): ctx.run(ctx.on.start(), state) -def test_grant_nonowner(mycharm): +def test_grant_nonowner(mycharm: type[CharmBase]) -> None: secret = Secret( {'a': 'b'}, label='mylabel', @@ -433,7 +442,7 @@ def test_grant_nonowner(mycharm): ) secret_id = secret.id - def post_event(charm: CharmBase): + def post_event(charm: CharmBase) -> None: secret = charm.model.get_secret(id=secret_id) secret = charm.model.get_secret(label='mylabel') foo = charm.model.get_relation('foo') @@ -454,7 +463,7 @@ def post_event(charm: CharmBase): ) -def test_add_grant_revoke_remove(): +def test_add_grant_revoke_remove() -> None: class GrantingCharm(CharmBase): pass @@ -498,13 +507,13 @@ class GrantingCharm(CharmBase): output.get_secret(label='mylabel') -def test_secret_removed_event(): +def test_secret_removed_event() -> None: class SecretCharm(CharmBase): - def __init__(self, framework): + def __init__(self, framework: Framework): super().__init__(framework) self.framework.observe(self.on.secret_remove, self._on_secret_remove) - def _on_secret_remove(self, event): + def _on_secret_remove(self, event: Any) -> None: event.secret.remove_revision(event.revision) ctx = Context(SecretCharm, meta={'name': 'foo'}) @@ -518,13 +527,13 @@ def _on_secret_remove(self, event): assert ctx.removed_secret_revisions == [old_revision] -def test_secret_expired_event(): +def test_secret_expired_event() -> None: class SecretCharm(CharmBase): - def __init__(self, framework): + def __init__(self, framework: Framework): super().__init__(framework) self.framework.observe(self.on.secret_expired, self._on_secret_expired) - def _on_secret_expired(self, event): + def _on_secret_expired(self, event: Any) -> None: event.secret.set_content({'password': 'newpass'}) event.secret.remove_revision(event.revision) @@ -539,13 +548,13 @@ def _on_secret_expired(self, event): assert ctx.removed_secret_revisions == [old_revision] -def test_remove_bad_revision(): +def test_remove_bad_revision() -> None: class SecretCharm(CharmBase): - def __init__(self, framework): + def __init__(self, framework: Framework): super().__init__(framework) self.framework.observe(self.on.secret_remove, self._on_secret_remove) - def _on_secret_remove(self, event): + def _on_secret_remove(self, event: Any) -> None: with pytest.raises(ValueError): event.secret.remove_revision(event.revision) @@ -561,13 +570,13 @@ def _on_secret_remove(self, event): ) -def test_set_label_on_get(): +def test_set_label_on_get() -> None: class SecretCharm(CharmBase): - def __init__(self, framework): + def __init__(self, framework: Framework): super().__init__(framework) self.framework.observe(self.on.start, self._on_start) - def _on_start(self, _): + def _on_start(self, _: EventBase) -> None: id = self.unit.add_secret({'foo': 'bar'}).id secret = self.model.get_secret(id=id, label='label1') assert secret.label == 'label1' @@ -579,12 +588,12 @@ def _on_start(self, _): assert state.get_secret(label='label2').tracked_content == {'foo': 'bar'} -def test_no_additional_positional_arguments(): +def test_no_additional_positional_arguments() -> None: with pytest.raises(TypeError): - Secret({}, {}) + Secret({}, {}) # type: ignore[misc] -def test_default_values(): +def test_default_values() -> None: contents = {'foo': 'bar'} secret = Secret(contents) assert secret.latest_content == secret.tracked_content == contents @@ -597,13 +606,13 @@ def test_default_values(): assert secret.remote_grants == {} -def test_add_secret(secrets_context: Context[SecretsCharm]): +def test_add_secret(secrets_context: Context[SecretsCharm]) -> None: state = State(leader=True) state = secrets_context.run(secrets_context.on.action('add-secret'), state) result = cast('Result', secrets_context.action_results) assert result is not None - assert result['secretid'] + assert result.get('secretid') scenario_secret = next(iter(state.secrets)) @@ -634,7 +643,7 @@ def test_add_secret(secrets_context: Context[SecretsCharm]): 'label,description,expire,rotate', ], ) -def test_add_secret_with_metadata(secrets_context: Context[SecretsCharm], fields: str): +def test_add_secret_with_metadata(secrets_context: Context[SecretsCharm], fields: str) -> None: state = State(leader=True) state = secrets_context.run( secrets_context.on.action('add-with-meta', params={'fields': fields}), state @@ -678,7 +687,7 @@ def test_add_secret_with_metadata(secrets_context: Context[SecretsCharm], fields ) def test_set_secret( secrets_context: Context[SecretsCharm], flow: str, lookup_by: Literal['id', 'label'] -): +) -> None: secret = Secret({'some': 'content'}, owner='app', id='theid', label='thelabel') state = State(leader=True, secrets={secret}) params = {'flow': flow, f'secret{lookup_by}': f'the{lookup_by}'} @@ -706,13 +715,15 @@ def test_set_secret( assert info['rotation'] == rotation_values[counts['rotate']] -def common_assertions(scenario_secret: Secret | None, result: Result): +def common_assertions(scenario_secret: Secret | None, result: Result) -> None: if scenario_secret: assert scenario_secret.owner == 'app' assert not scenario_secret.remote_grants - assert result.get('after') - info = result['after']['info'] + after = result.get('after') + assert after is not None + info = after['info'] + assert info is not None # Verify that the unit and the scaffolding see the same data # # Scenario presents a secret with a full secret URI to the charm @@ -726,5 +737,5 @@ def common_assertions(scenario_secret: Secret | None, result: Result): # https://github.com/canonical/operator/issues/2104 assert info['rotates'] is None - assert scenario_secret.tracked_content == result['after']['tracked'] - assert scenario_secret.latest_content == result['after']['latest'] + assert scenario_secret.tracked_content == after['tracked'] + assert scenario_secret.latest_content == after['latest'] From 4a150634a7fbbfe748d809a6a9f9a57d8ced6686 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Sun, 14 Dec 2025 12:13:18 +1300 Subject: [PATCH 03/20] test: add type annotations to test_relations. --- pyproject.toml | 1 - testing/tests/test_e2e/test_relations.py | 113 +++++++++++++---------- 2 files changed, 66 insertions(+), 48 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a4b1e4311..6e6977423 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -293,7 +293,6 @@ exclude = [ "testing/tests/test_e2e/test_deferred.py", "testing/tests/test_e2e/test_stored_state.py", "testing/tests/test_e2e/conftest.py", - "testing/tests/test_e2e/test_relations.py", "testing/tests/test_e2e/test_trace_data.py", "testing/tests/test_e2e/test_juju_log.py", "testing/tests/test_e2e/test_status.py", diff --git a/testing/tests/test_e2e/test_relations.py b/testing/tests/test_e2e/test_relations.py index 6cf20c511..3a7a40b1d 100644 --- a/testing/tests/test_e2e/test_relations.py +++ b/testing/tests/test_e2e/test_relations.py @@ -1,6 +1,7 @@ from __future__ import annotations from collections.abc import Callable +from typing import Any, cast import ops import pytest @@ -17,7 +18,7 @@ import scenario from scenario import Context -from scenario.errors import UncaughtCharmError +from scenario.errors import StateValidationError, UncaughtCharmError from scenario.state import ( _DEFAULT_JUJU_DATABAG, _Event, @@ -25,7 +26,6 @@ Relation, RelationBase, State, - StateValidationError, SubordinateRelation, _next_relation_id, ) @@ -44,22 +44,22 @@ def define_event(cls, event_kind: str, event_type: 'type[EventBase]'): class MyCharm(CharmBase): _call: Callable[[MyCharm, _Event], None] | None = None called = False - on = MyCharmEvents() + on: CharmEvents = MyCharmEvents() def __init__(self, framework: Framework): super().__init__(framework) for evt in self.on.events().values(): self.framework.observe(evt, self._on_event) - def _on_event(self, event): + def _on_event(self, event: EventBase): if self._call: MyCharm.called = True - self._call(event) + self._call(self, cast('Any', event)) return MyCharm -def test_get_relation(mycharm): +def test_get_relation(mycharm: Any): def pre_event(charm: CharmBase): assert charm.model.get_relation('foo') assert charm.model.get_relation('bar') is None @@ -336,10 +336,10 @@ def _update_status(self, event: EventBase): 'remote_app_name', ('remote', 'prometheus', 'aodeok123'), ) -def test_relation_events(mycharm, evt_name, remote_app_name): +def test_relation_events(mycharm: Any, evt_name: str, remote_app_name: str): relation = Relation(endpoint='foo', interface='foo', remote_app_name=remote_app_name) - def callback(charm: CharmBase, e): + def callback(charm: CharmBase, e: EventBase): if not isinstance(e, RelationEvent): return # filter out collect status events @@ -347,8 +347,10 @@ def callback(charm: CharmBase, e): assert charm.model.get_relation('foo') is None assert e.relation.app.name == remote_app_name else: - assert charm.model.get_relation('foo').app is not None - assert charm.model.get_relation('foo').app.name == remote_app_name + rel = charm.model.get_relation('foo') + assert rel is not None + assert rel.app is not None + assert rel.app.name == remote_app_name mycharm._call = callback @@ -387,18 +389,25 @@ def callback(charm: CharmBase, e): 'remote_unit_id', (0, 1), ) -def test_relation_events_attrs(mycharm, evt_name, has_unit, remote_app_name, remote_unit_id): +def test_relation_events_attrs( + mycharm: Any, + evt_name: str, + has_unit: bool, + remote_app_name: str, + remote_unit_id: int, +): relation = Relation(endpoint='foo', interface='foo', remote_app_name=remote_app_name) - def callback(charm: CharmBase, event): + def callback(charm: CharmBase, event: EventBase): if isinstance(event, CollectStatusEvent): return - assert event.app - if not isinstance(event, (RelationCreatedEvent, RelationBrokenEvent)): - assert event.unit - if isinstance(event, RelationDepartedEvent): - assert event.departing_unit + if isinstance(event, RelationEvent): + assert event.app + if not isinstance(event, RelationCreatedEvent | RelationBrokenEvent): + assert event.unit + if isinstance(event, RelationDepartedEvent): + assert event.departing_unit mycharm._call = callback @@ -427,7 +436,9 @@ def callback(charm: CharmBase, event): 'remote_app_name', ('remote', 'prometheus', 'aodeok123'), ) -def test_relation_events_no_attrs(mycharm, evt_name, remote_app_name, caplog): +def test_relation_events_no_attrs( + mycharm: Any, evt_name: str, remote_app_name: str, caplog: pytest.LogCaptureFixture +): relation = Relation( endpoint='foo', interface='foo', @@ -435,17 +446,18 @@ def test_relation_events_no_attrs(mycharm, evt_name, remote_app_name, caplog): remote_units_data={0: {}, 1: {}}, # 2 units ) - def callback(charm: CharmBase, event): + def callback(charm: CharmBase, event: EventBase): if isinstance(event, CollectStatusEvent): return - assert event.app # that's always present - # .unit is always None for created and broken. - if isinstance(event, (RelationCreatedEvent, RelationBrokenEvent)): - assert event.unit is None - else: - assert event.unit - assert (evt_name == 'departed') is bool(getattr(event, 'departing_unit', False)) + if isinstance(event, RelationEvent): + assert event.app # that's always present + # .unit is always None for created and broken. + if isinstance(event, RelationCreatedEvent | RelationBrokenEvent): + assert event.unit is None + else: + assert event.unit + assert (evt_name == 'departed') is bool(getattr(event, 'departing_unit', False)) mycharm._call = callback @@ -487,19 +499,22 @@ def test_relation_default_unit_data_peer(): @pytest.mark.parametrize('evt_name', ('broken', 'created')) -def test_relation_events_no_remote_units(mycharm, evt_name, caplog): +def test_relation_events_no_remote_units( + mycharm: Any, evt_name: str, caplog: pytest.LogCaptureFixture +): relation = Relation( endpoint='foo', interface='foo', remote_units_data={}, # no units ) - def callback(charm: CharmBase, event): + def callback(charm: CharmBase, event: EventBase): if isinstance(event, CollectStatusEvent): return - assert event.app # that's always present - assert not event.unit + if isinstance(event, RelationEvent): + assert event.app # that's always present + assert not event.unit mycharm._call = callback @@ -523,16 +538,16 @@ def callback(charm: CharmBase, event): assert 'remote unit ID unset; no remote unit data present' in caplog.text -@pytest.mark.parametrize('data', (set(), {}, [], (), 1, 1.0, None, b'')) -def test_relation_unit_data_bad_types(mycharm, data): +@pytest.mark.parametrize('data', (set(), {}, [], (), 1, 1.0, None, b'')) # type: ignore[reportUnknownArgumentType] +def test_relation_unit_data_bad_types(mycharm: Any, data: object): with pytest.raises(StateValidationError): - Relation(endpoint='foo', interface='foo', remote_units_data={0: {'a': data}}) + Relation(endpoint='foo', interface='foo', remote_units_data={0: {'a': cast('Any', data)}}) -@pytest.mark.parametrize('data', (set(), {}, [], (), 1, 1.0, None, b'')) -def test_relation_app_data_bad_types(mycharm, data): +@pytest.mark.parametrize('data', (set(), {}, [], (), 1, 1.0, None, b'')) # type: ignore[reportUnknownArgumentType] +def test_relation_app_data_bad_types(mycharm: Any, data: object): with pytest.raises(StateValidationError): - Relation(endpoint='foo', interface='foo', local_app_data={'a': data}) + Relation(endpoint='foo', interface='foo', local_app_data={'a': cast('Any', data)}) @pytest.mark.parametrize( @@ -547,7 +562,7 @@ def test_relation_app_data_bad_types(mycharm, data): SubordinateRelation('c'), ), ) -def test_relation_event_trigger(relation, evt_name, mycharm): +def test_relation_event_trigger(relation: RelationBase, evt_name: str, mycharm: Any): meta = { 'name': 'mycharm', 'requires': {'a': {'interface': 'i1'}}, @@ -568,7 +583,7 @@ def test_relation_event_trigger(relation, evt_name, mycharm): ) -def test_trigger_sub_relation(mycharm): +def test_trigger_sub_relation(mycharm: Any): meta = { 'name': 'mycharm', 'provides': { @@ -612,7 +627,7 @@ def test_relation_ids(): assert rel.id == initial_id + i -def test_broken_relation_not_in_model_relations(mycharm): +def test_broken_relation_not_in_model_relations(mycharm: Any): rel = Relation('foo') ctx = Context(mycharm, meta={'name': 'local', 'requires': {'foo': {'interface': 'foo'}}}) @@ -625,17 +640,19 @@ def test_broken_relation_not_in_model_relations(mycharm): def test_get_relation_when_missing(): class MyCharm(CharmBase): - def __init__(self, framework): + def __init__(self, framework: Framework): super().__init__(framework) self.framework.observe(self.on.update_status, self._on_update_status) self.framework.observe(self.on.config_changed, self._on_config_changed) self.relation = None - def _on_update_status(self, _): + def _on_update_status(self, _: EventBase): self.relation = self.model.get_relation('foo') - def _on_config_changed(self, _): - self.relation = self.model.get_relation('foo', self.config['relation-id']) + def _on_config_changed(self, _: EventBase): + relation_id = self.config['relation-id'] + assert isinstance(relation_id, int) + self.relation = self.model.get_relation('foo', relation_id) ctx = Context( MyCharm, @@ -652,6 +669,7 @@ def _on_config_changed(self, _): rel = Relation('foo') with ctx(ctx.on.update_status(), State(relations={rel})) as mgr: mgr.run() + assert mgr.charm.relation is not None assert mgr.charm.relation.id == rel.id # If a relation that doesn't exist is requested, that should also not raise @@ -659,6 +677,7 @@ def _on_config_changed(self, _): with ctx(ctx.on.config_changed(), State(config={'relation-id': 42})) as mgr: mgr.run() rel = mgr.charm.relation + assert rel is not None assert rel.id == 42 assert not rel.active @@ -670,9 +689,9 @@ def _on_config_changed(self, _): @pytest.mark.parametrize('klass', (Relation, PeerRelation, SubordinateRelation)) -def test_relation_positional_arguments(klass): +def test_relation_positional_arguments(klass: type[RelationBase]): with pytest.raises(TypeError): - klass('foo', 'bar', None) + cast('Any', klass)('foo', 'bar', None) def test_relation_default_values(): @@ -722,11 +741,11 @@ def test_peer_relation_default_values(): def test_relation_remote_model(): class MyCharm(CharmBase): - def __init__(self, framework): + def __init__(self, framework: Framework): super().__init__(framework) self.framework.observe(self.on.start, self._on_start) - def _on_start(self, event): + def _on_start(self, event: EventBase): relation = self.model.get_relation('foo') assert relation is not None self.remote_model_uuid = relation.remote_model.uuid From 56c1b9820ba6bbb4f743b6270729ebf056fcff38 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Sun, 14 Dec 2025 12:14:42 +1300 Subject: [PATCH 04/20] ci: remove unnecessary exclude. --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6e6977423..095bcb59c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -293,7 +293,6 @@ exclude = [ "testing/tests/test_e2e/test_deferred.py", "testing/tests/test_e2e/test_stored_state.py", "testing/tests/test_e2e/conftest.py", - "testing/tests/test_e2e/test_trace_data.py", "testing/tests/test_e2e/test_juju_log.py", "testing/tests/test_e2e/test_status.py", "testing/tests/test_e2e/test_storage.py", From 796720be5e79086b2852b86b0fcb9e20581fc250 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Sun, 14 Dec 2025 12:20:40 +1300 Subject: [PATCH 05/20] test: add type annotations to test_juju_log The class variable gets adjusted in the ruff fixes PR, but we can handle the merge conflict later. --- pyproject.toml | 1 - testing/tests/test_e2e/test_juju_log.py | 10 ++++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 095bcb59c..387c7cad9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -293,7 +293,6 @@ exclude = [ "testing/tests/test_e2e/test_deferred.py", "testing/tests/test_e2e/test_stored_state.py", "testing/tests/test_e2e/conftest.py", - "testing/tests/test_e2e/test_juju_log.py", "testing/tests/test_e2e/test_status.py", "testing/tests/test_e2e/test_storage.py", "testing/tests/test_e2e/test_manager.py", diff --git a/testing/tests/test_e2e/test_juju_log.py b/testing/tests/test_e2e/test_juju_log.py index cbbbc1494..e41120f42 100644 --- a/testing/tests/test_e2e/test_juju_log.py +++ b/testing/tests/test_e2e/test_juju_log.py @@ -1,9 +1,11 @@ from __future__ import annotations import logging +from typing import Any import pytest from ops.charm import CharmBase, CollectStatusEvent +from ops.framework import EventBase, Framework from scenario import Context from scenario.state import JujuLogLine, State @@ -14,14 +16,14 @@ @pytest.fixture(scope='function') def mycharm(): class MyCharm(CharmBase): - META = {'name': 'mycharm'} + META: dict[str, Any] = {'name': 'mycharm'} - def __init__(self, framework): + def __init__(self, framework: Framework): super().__init__(framework) for evt in self.on.events().values(): self.framework.observe(evt, self._on_event) - def _on_event(self, event): + def _on_event(self, event: EventBase): if isinstance(event, CollectStatusEvent): return print('foo!') @@ -30,7 +32,7 @@ def _on_event(self, event): return MyCharm -def test_juju_log(mycharm): +def test_juju_log(mycharm: Any): ctx = Context(mycharm, meta=mycharm.META) ctx.run(ctx.on.start(), State()) assert JujuLogLine(level='DEBUG', message='Emitting Juju event start.') in ctx.juju_log From 5d255f0750368d4bff272dbc8f1d67945c776162 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Sun, 14 Dec 2025 12:31:24 +1300 Subject: [PATCH 06/20] test: add type annotations for test_status. --- pyproject.toml | 1 - testing/tests/test_e2e/test_status.py | 42 +++++++++++++-------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 387c7cad9..cdf6459f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -293,7 +293,6 @@ exclude = [ "testing/tests/test_e2e/test_deferred.py", "testing/tests/test_e2e/test_stored_state.py", "testing/tests/test_e2e/conftest.py", - "testing/tests/test_e2e/test_status.py", "testing/tests/test_e2e/test_storage.py", "testing/tests/test_e2e/test_manager.py", "testing/tests/test_e2e/test_ports.py", diff --git a/testing/tests/test_e2e/test_status.py b/testing/tests/test_e2e/test_status.py index 872c8a4ad..04409319b 100644 --- a/testing/tests/test_e2e/test_status.py +++ b/testing/tests/test_e2e/test_status.py @@ -3,7 +3,7 @@ import ops import pytest from ops.charm import CharmBase -from ops.framework import Framework +from ops.framework import EventBase, Framework from scenario import Context from scenario.state import ( @@ -20,21 +20,21 @@ @pytest.fixture(scope='function') -def mycharm(): +def mycharm() -> type[CharmBase]: class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) for evt in self.on.events().values(): self.framework.observe(evt, self._on_event) - def _on_event(self, event): + def _on_event(self, event: EventBase) -> None: pass return MyCharm -def test_initial_status(mycharm): - def post_event(charm: CharmBase): +def test_initial_status(mycharm: type[CharmBase]) -> None: + def post_event(charm: CharmBase) -> None: assert charm.unit.status == UnknownStatus() out = trigger( @@ -48,13 +48,13 @@ def post_event(charm: CharmBase): assert out.unit_status == UnknownStatus() -def test_status_history(mycharm): +def test_status_history(mycharm: type[CharmBase]) -> None: class StatusCharm(mycharm): - def __init__(self, framework): + def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.update_status, self._on_update_status) - def _on_update_status(self, _): + def _on_update_status(self, _: EventBase) -> None: for obj in (self.unit, self.app): obj.status = ops.ActiveStatus('1') obj.status = ops.BlockedStatus('2') @@ -82,13 +82,13 @@ def _on_update_status(self, _): ] -def test_status_history_preservation(mycharm): +def test_status_history_preservation(mycharm: type[CharmBase]) -> None: class StatusCharm(mycharm): - def __init__(self, framework): + def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.update_status, self._on_update_status) - def _on_update_status(self, _): + def _on_update_status(self, _: EventBase) -> None: for obj in (self.unit, self.app): obj.status = WaitingStatus('3') @@ -113,21 +113,21 @@ def _on_update_status(self, _): assert ctx.app_status_history == [ActiveStatus('bar')] -def test_workload_history(mycharm): +def test_workload_history(mycharm: type[CharmBase]) -> None: class WorkloadCharm(mycharm): - def __init__(self, framework): + def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.install, self._on_install) framework.observe(self.on.start, self._on_start) framework.observe(self.on.update_status, self._on_update_status) - def _on_install(self, _): + def _on_install(self, _: EventBase) -> None: self.unit.set_workload_version('1') - def _on_start(self, _): + def _on_start(self, _: EventBase) -> None: self.unit.set_workload_version('1.1') - def _on_update_status(self, _): + def _on_update_status(self, _: EventBase) -> None: self.unit.set_workload_version('1.2') ctx = Context( @@ -154,7 +154,7 @@ def _on_update_status(self, _): UnknownStatus(), ), ) -def test_status_comparison(status): +def test_status_comparison(status: ops.StatusBase) -> None: if isinstance(status, UnknownStatus): ops_status = ops.UnknownStatus() else: @@ -183,13 +183,13 @@ def test_status_comparison(status): MaintenanceStatus('qux'), ), ) -def test_status_success(status: ops.StatusBase): +def test_status_success(status: ops.StatusBase) -> None: class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.update_status, self._on_update_status) - def _on_update_status(self, _): + def _on_update_status(self, _: EventBase) -> None: self.unit.status = status ctx = Context(MyCharm, meta={'name': 'foo'}) @@ -203,13 +203,13 @@ def _on_update_status(self, _): UnknownStatus(), ), ) -def test_status_error(status: ops.StatusBase, monkeypatch: pytest.MonkeyPatch): +def test_status_error(status: ops.StatusBase, monkeypatch: pytest.MonkeyPatch) -> None: class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.update_status, self._on_update_status) - def _on_update_status(self, _): + def _on_update_status(self, _: EventBase) -> None: self.unit.status = status monkeypatch.setenv('SCENARIO_BARE_CHARM_ERRORS', 'false') From 0f5ca37bfb84bcf14101fbf2fbffa32d17c573ef Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Sun, 14 Dec 2025 12:42:50 +1300 Subject: [PATCH 07/20] test: add type annotations to test_storage The type: ignore should be able to go away when the storage class gets annotations, which is in another PR in this series. We can handle the merge then. --- pyproject.toml | 1 - testing/tests/test_e2e/test_storage.py | 26 ++++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cdf6459f8..977a60074 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -293,7 +293,6 @@ exclude = [ "testing/tests/test_e2e/test_deferred.py", "testing/tests/test_e2e/test_stored_state.py", "testing/tests/test_e2e/conftest.py", - "testing/tests/test_e2e/test_storage.py", "testing/tests/test_e2e/test_manager.py", "testing/tests/test_e2e/test_ports.py", "testing/tests/test_e2e/test_rubbish_events.py", diff --git a/testing/tests/test_e2e/test_storage.py b/testing/tests/test_e2e/test_storage.py index 3ac6c2075..cf2b6722e 100644 --- a/testing/tests/test_e2e/test_storage.py +++ b/testing/tests/test_e2e/test_storage.py @@ -15,22 +15,22 @@ class MyCharmWithoutStorage(CharmBase): @pytest.fixture -def storage_ctx(): +def storage_ctx() -> Context[MyCharmWithStorage]: return Context(MyCharmWithStorage, meta=MyCharmWithStorage.META) @pytest.fixture -def no_storage_ctx(): +def no_storage_ctx() -> Context[MyCharmWithoutStorage]: return Context(MyCharmWithoutStorage, meta=MyCharmWithoutStorage.META) -def test_storage_get_null(no_storage_ctx): +def test_storage_get_null(no_storage_ctx: Context[MyCharmWithoutStorage]) -> None: with no_storage_ctx(no_storage_ctx.on.update_status(), State()) as mgr: storages = mgr.charm.model.storages assert not len(storages) -def test_storage_get_unknown_name(storage_ctx): +def test_storage_get_unknown_name(storage_ctx: Context[MyCharmWithStorage]) -> None: with storage_ctx(storage_ctx.on.update_status(), State()) as mgr: storages = mgr.charm.model.storages # not in metadata @@ -38,7 +38,7 @@ def test_storage_get_unknown_name(storage_ctx): storages['bar'] -def test_storage_request_unknown_name(storage_ctx): +def test_storage_request_unknown_name(storage_ctx: Context[MyCharmWithStorage]) -> None: with storage_ctx(storage_ctx.on.update_status(), State()) as mgr: storages = mgr.charm.model.storages # not in metadata @@ -46,7 +46,7 @@ def test_storage_request_unknown_name(storage_ctx): storages.request('bar') -def test_storage_get_some(storage_ctx): +def test_storage_get_some(storage_ctx: Context[MyCharmWithStorage]) -> None: with storage_ctx(storage_ctx.on.update_status(), State()) as mgr: storages = mgr.charm.model.storages # known but none attached @@ -54,7 +54,7 @@ def test_storage_get_some(storage_ctx): @pytest.mark.parametrize('n', (1, 3, 5)) -def test_storage_add(storage_ctx, n): +def test_storage_add(storage_ctx: Context[MyCharmWithStorage], n: int) -> None: with storage_ctx(storage_ctx.on.update_status(), State()) as mgr: storages = mgr.charm.model.storages storages.request('foo', n) @@ -62,10 +62,10 @@ def test_storage_add(storage_ctx, n): assert storage_ctx.requested_storages['foo'] == n -def test_storage_usage(storage_ctx): +def test_storage_usage(storage_ctx: Context[MyCharmWithStorage]) -> None: storage = Storage('foo') # setup storage with some content - (storage.get_filesystem(storage_ctx) / 'myfile.txt').write_text('helloworld') + (storage.get_filesystem(storage_ctx) / 'myfile.txt').write_text('helloworld') # type: ignore[reportUnknownMemberType] with storage_ctx(storage_ctx.on.update_status(), State(storages={storage})) as mgr: foo = mgr.charm.model.storages['foo'][0] @@ -78,14 +78,16 @@ def test_storage_usage(storage_ctx): myfile.write_text('helloworlds') # post-mortem: inspect fs contents. - assert (storage.get_filesystem(storage_ctx) / 'path.py').read_text() == 'helloworlds' + assert ( + storage.get_filesystem(storage_ctx) / 'path.py' # type: ignore[reportUnknownMemberType] + ).read_text() == 'helloworlds' -def test_storage_attached_event(storage_ctx): +def test_storage_attached_event(storage_ctx: Context[MyCharmWithStorage]) -> None: storage = Storage('foo') storage_ctx.run(storage_ctx.on.storage_attached(storage), State(storages={storage})) -def test_storage_detaching_event(storage_ctx): +def test_storage_detaching_event(storage_ctx: Context[MyCharmWithStorage]) -> None: storage = Storage('foo') storage_ctx.run(storage_ctx.on.storage_detaching(storage), State(storages={storage})) From ffa4e13c42f405f7477b8e9fa6319ba1108d3266 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Sun, 14 Dec 2025 12:48:11 +1300 Subject: [PATCH 08/20] test: add type annotations to test_manager. The class vars are being adjusted in another PR. We can handle the merge later. --- pyproject.toml | 1 - testing/tests/test_e2e/test_manager.py | 26 +++++++++++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 977a60074..137286db1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -293,7 +293,6 @@ exclude = [ "testing/tests/test_e2e/test_deferred.py", "testing/tests/test_e2e/test_stored_state.py", "testing/tests/test_e2e/conftest.py", - "testing/tests/test_e2e/test_manager.py", "testing/tests/test_e2e/test_ports.py", "testing/tests/test_e2e/test_rubbish_events.py", "testing/tests/test_e2e/test_pebble.py", diff --git a/testing/tests/test_e2e/test_manager.py b/testing/tests/test_e2e/test_manager.py index 84c5c5d5a..5bf666f89 100644 --- a/testing/tests/test_e2e/test_manager.py +++ b/testing/tests/test_e2e/test_manager.py @@ -1,25 +1,29 @@ from __future__ import annotations +from typing import Any + import pytest -from ops import ActiveStatus +from ops import ActiveStatus, EventBase from ops.charm import CharmBase, CollectStatusEvent +from ops.framework import Framework from scenario import Context, State -from scenario.context import AlreadyEmittedError, Manager +from scenario.context import Manager +from scenario.errors import AlreadyEmittedError @pytest.fixture(scope='function') def mycharm(): class MyCharm(CharmBase): - META = {'name': 'mycharm'} - ACTIONS = {'do-x': {}} + META: dict[str, Any] = {'name': 'mycharm'} + ACTIONS: dict[str, Any] = {'do-x': {}} - def __init__(self, framework): + def __init__(self, framework: Framework): super().__init__(framework) for evt in self.on.events().values(): self.framework.observe(evt, self._on_event) - def _on_event(self, e): + def _on_event(self, e: EventBase) -> None: if isinstance(e, CollectStatusEvent): return @@ -28,7 +32,7 @@ def _on_event(self, e): return MyCharm -def test_manager(mycharm): +def test_manager(mycharm: Any) -> None: ctx = Context(mycharm, meta=mycharm.META) with Manager(ctx, ctx.on.start(), State()) as manager: assert isinstance(manager.charm, mycharm) @@ -37,7 +41,7 @@ def test_manager(mycharm): assert isinstance(state_out, State) -def test_manager_implicit(mycharm): +def test_manager_implicit(mycharm: Any) -> None: ctx = Context(mycharm, meta=mycharm.META) with Manager(ctx, ctx.on.start(), State()) as manager: assert isinstance(manager.charm, mycharm) @@ -47,7 +51,7 @@ def test_manager_implicit(mycharm): assert manager._emitted -def test_manager_reemit_fails(mycharm): +def test_manager_reemit_fails(mycharm: Any) -> None: ctx = Context(mycharm, meta=mycharm.META) with Manager(ctx, ctx.on.start(), State()) as manager: manager.run() @@ -55,7 +59,7 @@ def test_manager_reemit_fails(mycharm): manager.run() -def test_context_manager(mycharm): +def test_context_manager(mycharm: Any) -> None: ctx = Context(mycharm, meta=mycharm.META) with ctx(ctx.on.start(), State()) as manager: state_out = manager.run() @@ -63,7 +67,7 @@ def test_context_manager(mycharm): assert ctx.emitted_events[0].handle.kind == 'start' -def test_context_action_manager(mycharm): +def test_context_action_manager(mycharm: Any) -> None: ctx = Context(mycharm, meta=mycharm.META, actions=mycharm.ACTIONS) with ctx(ctx.on.action('do-x'), State()) as manager: state_out = manager.run() From 362feb7c43f768efecc275d1e9f90b7cfcee0c44 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Sun, 14 Dec 2025 12:54:24 +1300 Subject: [PATCH 09/20] test: add type annotations to test_ports. --- pyproject.toml | 1 - testing/tests/test_e2e/test_ports.py | 18 ++++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 137286db1..4ebfae81a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -293,7 +293,6 @@ exclude = [ "testing/tests/test_e2e/test_deferred.py", "testing/tests/test_e2e/test_stored_state.py", "testing/tests/test_e2e/conftest.py", - "testing/tests/test_e2e/test_ports.py", "testing/tests/test_e2e/test_rubbish_events.py", "testing/tests/test_e2e/test_pebble.py", ] diff --git a/testing/tests/test_e2e/test_ports.py b/testing/tests/test_e2e/test_ports.py index 9d122e778..5e8c03fb4 100644 --- a/testing/tests/test_e2e/test_ports.py +++ b/testing/tests/test_e2e/test_ports.py @@ -4,7 +4,8 @@ from ops import CharmBase, Framework, StartEvent, StopEvent from scenario import Context, State -from scenario.state import Port, StateValidationError, TCPPort, UDPPort +from scenario.errors import StateValidationError +from scenario.state import Port, TCPPort, UDPPort class MyCharm(CharmBase): @@ -24,31 +25,32 @@ def _close_port(self, _: StopEvent): @pytest.fixture -def ctx(): +def ctx() -> Context[MyCharm]: return Context(MyCharm, meta=MyCharm.META) -def test_open_port(ctx): +def test_open_port(ctx: Context[MyCharm]) -> None: out = ctx.run(ctx.on.start(), State()) - assert len(out.opened_ports) == 1 - port = tuple(out.opened_ports)[0] + ports = tuple(out.opened_ports) + assert len(ports) == 1 + port = ports[0] assert port.protocol == 'tcp' assert port.port == 12 -def test_close_port(ctx): +def test_close_port(ctx: Context[MyCharm]) -> None: out = ctx.run(ctx.on.stop(), State(opened_ports={TCPPort(42)})) assert not out.opened_ports -def test_port_no_arguments(): +def test_port_no_arguments() -> None: with pytest.raises(RuntimeError): Port() @pytest.mark.parametrize('klass', (TCPPort, UDPPort)) -def test_port_port(klass): +def test_port_port(klass: type[Port]) -> None: with pytest.raises(StateValidationError): klass(port=0) with pytest.raises(StateValidationError): From dbce9c0eaa3eb9b43f0535be8c0712017e45ba25 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Sun, 14 Dec 2025 13:03:27 +1300 Subject: [PATCH 10/20] test: add type annotations to test_rubbish_events. --- pyproject.toml | 1 - testing/tests/test_e2e/test_rubbish_events.py | 26 ++++++++++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4ebfae81a..1ebd7b4ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -293,7 +293,6 @@ exclude = [ "testing/tests/test_e2e/test_deferred.py", "testing/tests/test_e2e/test_stored_state.py", "testing/tests/test_e2e/conftest.py", - "testing/tests/test_e2e/test_rubbish_events.py", "testing/tests/test_e2e/test_pebble.py", ] extraPaths = ["testing", "tracing"] diff --git a/testing/tests/test_e2e/test_rubbish_events.py b/testing/tests/test_e2e/test_rubbish_events.py index 9240facd3..8d95a0267 100644 --- a/testing/tests/test_e2e/test_rubbish_events.py +++ b/testing/tests/test_e2e/test_rubbish_events.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import ClassVar + import pytest from ops.charm import CharmBase, CharmEvents from ops.framework import EventBase, EventSource, Framework, Object @@ -25,11 +27,11 @@ class MySubEvents(CharmEvents): sub = EventSource(SubEvent) class Sub(Object): - on = MySubEvents() + on: ClassVar[MySubEvents] = MySubEvents() class MyCharm(CharmBase): - on = MyCharmEvents() - evts = [] + on: ClassVar[MyCharmEvents] = MyCharmEvents() + evts: ClassVar[list[EventBase]] = [] def __init__(self, framework: Framework): super().__init__(framework) @@ -37,19 +39,21 @@ def __init__(self, framework: Framework): self.framework.observe(self.sub.on.sub, self._on_event) self.framework.observe(self.on.qux, self._on_event) - def _on_event(self, e): + def _on_event(self, e: EventBase) -> None: MyCharm.evts.append(e) return MyCharm @pytest.mark.parametrize('evt_name', ('rubbish', 'foo', 'bar')) -def test_rubbish_event_raises(mycharm: CharmBase, evt_name: str): +def test_rubbish_event_raises(mycharm: type[CharmBase], evt_name: str) -> None: with pytest.raises(AttributeError): trigger(State(), evt_name, mycharm, meta={'name': 'foo'}) -def test_rubbish_pebble_ready_event_raises(mycharm: CharmBase, monkeypatch: pytest.MonkeyPatch): +def test_rubbish_pebble_ready_event_raises( + mycharm: type[CharmBase], monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.setenv('SCENARIO_SKIP_CONSISTENCY_CHECKS', '1') # else it will whine about the container not being in state and meta; # but if we put the container in meta, it will actually register an event! @@ -58,14 +62,14 @@ def test_rubbish_pebble_ready_event_raises(mycharm: CharmBase, monkeypatch: pyte @pytest.mark.parametrize('evt_name', ('qux',)) -def test_custom_events_fail(mycharm, evt_name): +def test_custom_events_fail(mycharm: type[CharmBase], evt_name: str) -> None: with pytest.raises(AttributeError): trigger(State(), evt_name, mycharm, meta={'name': 'foo'}) # cfr: https://github.com/PietroPasotti/ops-scenario/pull/11#discussion_r1101694961 @pytest.mark.parametrize('evt_name', ('sub',)) -def test_custom_events_sub_raise(mycharm, evt_name): +def test_custom_events_sub_raise(mycharm: type[CharmBase], evt_name: str) -> None: with pytest.raises(AttributeError): trigger(State(), evt_name, mycharm, meta={'name': 'foo'}) @@ -82,6 +86,8 @@ def test_custom_events_sub_raise(mycharm, evt_name): ('bar-relation-changed', True), ), ) -def test_is_custom_event(mycharm, evt_name, expected): - spec = _CharmSpec(charm_type=mycharm, meta={'name': 'mycharm', 'requires': {'foo': {}}}) +def test_is_custom_event(mycharm: type[CharmBase], evt_name: str, expected: bool) -> None: + spec: _CharmSpec[CharmBase] = _CharmSpec( + charm_type=mycharm, meta={'name': 'mycharm', 'requires': {'foo': {}}} + ) assert _Event(evt_name)._is_builtin_event(spec) is expected From 0f97dd7fb729ab701a55fd05e26d4bfeabab746d Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Sun, 14 Dec 2025 17:45:30 +1300 Subject: [PATCH 11/20] test: add type annotaitons for test_pebble. The storage type ignore we should be able to remove after the other PRs are merged. --- pyproject.toml | 1 - testing/tests/test_e2e/test_pebble.py | 182 ++++++++++++++------------ 2 files changed, 100 insertions(+), 83 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1ebd7b4ed..3e8fe3577 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -293,7 +293,6 @@ exclude = [ "testing/tests/test_e2e/test_deferred.py", "testing/tests/test_e2e/test_stored_state.py", "testing/tests/test_e2e/conftest.py", - "testing/tests/test_e2e/test_pebble.py", ] extraPaths = ["testing", "tracing"] pythonVersion = "3.10" # check no python > 3.10 features are used diff --git a/testing/tests/test_e2e/test_pebble.py b/testing/tests/test_e2e/test_pebble.py index ce82d3cab..cc4925d0a 100644 --- a/testing/tests/test_e2e/test_pebble.py +++ b/testing/tests/test_e2e/test_pebble.py @@ -4,36 +4,48 @@ import datetime import io import tempfile +from collections.abc import Iterator from pathlib import Path +from typing import TYPE_CHECKING import pytest -from ops import PebbleCustomNoticeEvent, PebbleReadyEvent, pebble +from ops import ( + LazyCheckInfo, + PebbleCheckFailedEvent, + PebbleCheckRecoveredEvent, + PebbleCustomNoticeEvent, + PebbleReadyEvent, + pebble, +) from ops.charm import CharmBase -from ops.framework import Framework +from ops.framework import EventBase, Framework from ops.log import _get_juju_log_and_app_id from ops.pebble import ExecError, Layer, ServiceStartup, ServiceStatus from scenario import Context from scenario.state import CheckInfo, Container, Exec, Mount, Notice, State -from ..helpers import jsonpatch_delta, trigger +from ..helpers import jsonpatch_delta, trigger # type: ignore[reportUnknownVariableType] + +if TYPE_CHECKING: + from ops.pebble import LayerDict, ServiceDict @pytest.fixture(scope='function') -def charm_cls(): +def charm_cls() -> type[CharmBase]: class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) for evt in self.on.events().values(): self.framework.observe(evt, self._on_event) - def _on_event(self, event): + def _on_event(self, event: EventBase) -> None: pass return MyCharm -def test_no_containers(charm_cls): - def callback(self: CharmBase): +def test_no_containers(charm_cls: type[CharmBase]) -> None: + def callback(self: CharmBase) -> None: assert not self.unit.containers trigger( @@ -45,8 +57,8 @@ def callback(self: CharmBase): ) -def test_containers_from_meta(charm_cls): - def callback(self: CharmBase): +def test_containers_from_meta(charm_cls: type[CharmBase]) -> None: + def callback(self: CharmBase) -> None: assert self.unit.containers assert self.unit.get_container('foo') @@ -60,8 +72,8 @@ def callback(self: CharmBase): @pytest.mark.parametrize('can_connect', (True, False)) -def test_connectivity(charm_cls, can_connect): - def callback(self: CharmBase): +def test_connectivity(charm_cls: type[CharmBase], can_connect: bool) -> None: + def callback(self: CharmBase) -> None: assert can_connect == self.unit.get_container('foo').can_connect() trigger( @@ -73,13 +85,13 @@ def callback(self: CharmBase): ) -def test_fs_push(charm_cls): +def test_fs_push(charm_cls: type[CharmBase]) -> None: text = 'lorem ipsum/n alles amat gloriae foo' file = tempfile.NamedTemporaryFile() pth = Path(file.name) pth.write_text(text) - def callback(self: CharmBase): + def callback(self: CharmBase) -> None: container = self.unit.get_container('foo') baz = container.pull('/bar/baz.txt') assert baz.read() == text @@ -102,10 +114,10 @@ def callback(self: CharmBase): @pytest.mark.parametrize('make_dirs', (True, False)) -def test_fs_pull(charm_cls, make_dirs): +def test_fs_pull(charm_cls: type[CharmBase], make_dirs: bool) -> None: text = 'lorem ipsum/n alles amat gloriae foo' - def callback(self: CharmBase): + def callback(self: CharmBase) -> None: container = self.unit.get_container('foo') if make_dirs: container.push('/foo/bar/baz.txt', text, make_dirs=make_dirs) @@ -152,7 +164,7 @@ def callback(self: CharmBase): assert file.read_text() == text # shortcut for API niceness purposes: - file = container.get_filesystem(ctx) / 'foo' / 'bar' / 'baz.txt' + file = container.get_filesystem(ctx) / 'foo' / 'bar' / 'baz.txt' # type: ignore[reportUnknownMemberType] assert file.read_text() == text else: @@ -189,11 +201,12 @@ def callback(self: CharmBase): ('ps', PS), ), ) -def test_exec(charm_cls, cmd, out): - def callback(self: CharmBase): +def test_exec(charm_cls: type[CharmBase], cmd: str, out: str) -> None: + def callback(self: CharmBase) -> None: container = self.unit.get_container('foo') proc = container.exec([cmd]) proc.wait() + assert proc.stdout is not None assert proc.stdout.read() == out trigger( @@ -221,15 +234,16 @@ def callback(self: CharmBase): [io.StringIO('hello world!'), None], ), ) -def test_exec_history_stdin(stdin, write): +def test_exec_history_stdin(stdin: str | io.StringIO | None, write: str | None) -> None: class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) self.framework.observe(self.on.foo_pebble_ready, self._on_ready) - def _on_ready(self, _): + def _on_ready(self, _: EventBase) -> None: proc = self.unit.get_container('foo').exec(['ls'], stdin=stdin) if write: + assert proc.stdin is not None proc.stdin.write(write) proc.wait() @@ -239,8 +253,8 @@ def _on_ready(self, _): assert ctx.exec_history[container.name][0].stdin == 'hello world!' -def test_pebble_ready(charm_cls): - def callback(self: CharmBase): +def test_pebble_ready(charm_cls: type[CharmBase]) -> None: + def callback(self: CharmBase) -> None: foo = self.unit.get_container('foo') assert foo.can_connect() @@ -256,13 +270,13 @@ def callback(self: CharmBase): @pytest.mark.parametrize('starting_service_status', pebble.ServiceStatus) -def test_pebble_plan(charm_cls, starting_service_status): +def test_pebble_plan(charm_cls: type[CharmBase], starting_service_status: ServiceStatus) -> None: class PlanCharm(charm_cls): - def __init__(self, framework): + def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.foo_pebble_ready, self._on_ready) - def _on_ready(self, event): + def _on_ready(self, event: PebbleReadyEvent) -> None: foo = event.workload assert foo.get_plan().to_dict() == {'services': {'fooserv': {'startup': 'enabled'}}} @@ -316,7 +330,7 @@ def _on_ready(self, event): event='pebble_ready', ) - def serv(name, obj): + def serv(name: str, obj: ServiceDict) -> pebble.Service: return pebble.Service(name, raw=obj) container = out.get_container(container.name) @@ -331,7 +345,7 @@ def serv(name, obj): assert container.services['barserv'].startup == pebble.ServiceStartup.DISABLED -def test_exec_wait_error(charm_cls): +def test_exec_wait_error(charm_cls: type[CharmBase]) -> None: state = State( containers={ Container( @@ -346,13 +360,13 @@ def test_exec_wait_error(charm_cls): with ctx(ctx.on.start(), state) as mgr: container = mgr.charm.unit.get_container('foo') proc = container.exec(['foo']) - with pytest.raises(ExecError) as exc_info: + with pytest.raises(ExecError) as exc_info: # type: ignore[reportUnknownVariableType] proc.wait_output() - assert exc_info.value.stdout == 'hello pebble' + assert exc_info.value.stdout == 'hello pebble' # type: ignore[reportUnknownMemberType] @pytest.mark.parametrize('command', (['foo'], ['foo', 'bar'], ['foo', 'bar', 'baz'])) -def test_exec_wait_output(charm_cls, command): +def test_exec_wait_output(charm_cls: type[CharmBase], command: list[str]) -> None: state = State( containers={ Container( @@ -373,7 +387,7 @@ def test_exec_wait_output(charm_cls, command): assert ctx.exec_history[container.name][0].command == command -def test_exec_wait_output_error(charm_cls): +def test_exec_wait_output_error(charm_cls: type[CharmBase]) -> None: state = State( containers={ Container( @@ -392,7 +406,7 @@ def test_exec_wait_output_error(charm_cls): proc.wait_output() -def test_pebble_custom_notice(charm_cls): +def test_pebble_custom_notice(charm_cls: type[CharmBase]) -> None: notices = [ Notice(key='example.com/foo'), Notice(key='example.com/bar', last_data={'a': 'b'}), @@ -411,7 +425,7 @@ def test_pebble_custom_notice(charm_cls): assert container.get_notices() == [n._to_ops() for n in notices] -def test_pebble_custom_notice_in_charm(): +def test_pebble_custom_notice_in_charm() -> None: key = 'example.com/test/charm' data = {'foo': 'bar'} user_id = 100 @@ -423,11 +437,11 @@ def test_pebble_custom_notice_in_charm(): expire_after = datetime.timedelta(days=365) class MyCharm(CharmBase): - def __init__(self, framework): + def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.foo_pebble_custom_notice, self._on_custom_notice) - def _on_custom_notice(self, event: PebbleCustomNoticeEvent): + def _on_custom_notice(self, event: PebbleCustomNoticeEvent) -> None: notice = event.notice assert notice.type == pebble.NoticeType.CUSTOM assert notice.key == key @@ -465,15 +479,15 @@ def _on_custom_notice(self, event: PebbleCustomNoticeEvent): ctx.run(ctx.on.pebble_custom_notice(container=container, notice=notices[-1]), state) -def test_pebble_check_failed(): - infos = [] +def test_pebble_check_failed() -> None: + infos: list[LazyCheckInfo] = [] class MyCharm(CharmBase): - def __init__(self, framework): + def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.foo_pebble_check_failed, self._on_check_failed) - def _on_check_failed(self, event): + def _on_check_failed(self, event: PebbleCheckFailedEvent) -> None: infos.append(event.info) ctx = Context(MyCharm, meta={'name': 'foo', 'containers': {'foo': {}}}) @@ -486,8 +500,8 @@ def _on_check_failed(self, event): successes=3, failures=7, status=pebble.CheckStatus.DOWN, - level=layer.checks['http-check'].level, - startup=layer.checks['http-check'].startup, + level=pebble.CheckLevel(layer.checks['http-check'].level), + startup=pebble.CheckStartup(layer.checks['http-check'].startup), threshold=layer.checks['http-check'].threshold, ) container = Container('foo', check_infos={check}, layers={'layer1': layer}) @@ -500,15 +514,15 @@ def _on_check_failed(self, event): assert infos[0].failures == 7 -def test_pebble_check_recovered(): - infos = [] +def test_pebble_check_recovered() -> None: + infos: list[LazyCheckInfo] = [] class MyCharm(CharmBase): - def __init__(self, framework): + def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.foo_pebble_check_recovered, self._on_check_recovered) - def _on_check_recovered(self, event): + def _on_check_recovered(self, event: PebbleCheckRecoveredEvent) -> None: infos.append(event.info) ctx = Context(MyCharm, meta={'name': 'foo', 'containers': {'foo': {}}}) @@ -520,8 +534,8 @@ def _on_check_recovered(self, event): 'http-check', successes=None, status=pebble.CheckStatus.UP, - level=layer.checks['http-check'].level, - startup=layer.checks['http-check'].startup, + level=pebble.CheckLevel(layer.checks['http-check'].level), + startup=pebble.CheckStartup(layer.checks['http-check'].startup), threshold=layer.checks['http-check'].threshold, ) container = Container('foo', check_infos={check}, layers={'layer1': layer}) @@ -534,9 +548,9 @@ def _on_check_recovered(self, event): assert infos[0].failures == 0 -def test_pebble_check_failed_two_containers(): - foo_infos = [] - bar_infos = [] +def test_pebble_check_failed_two_containers() -> None: + foo_infos: list[LazyCheckInfo] = [] + bar_infos: list[LazyCheckInfo] = [] class MyCharm(CharmBase): def __init__(self, framework: Framework): @@ -544,10 +558,10 @@ def __init__(self, framework: Framework): framework.observe(self.on.foo_pebble_check_failed, self._on_foo_check_failed) framework.observe(self.on.bar_pebble_check_failed, self._on_bar_check_failed) - def _on_foo_check_failed(self, event): + def _on_foo_check_failed(self, event: PebbleCheckFailedEvent) -> None: foo_infos.append(event.info) - def _on_bar_check_failed(self, event): + def _on_bar_check_failed(self, event: PebbleCheckFailedEvent) -> None: bar_infos.append(event.info) ctx = Context(MyCharm, meta={'name': 'foo', 'containers': {'foo': {}, 'bar': {}}}) @@ -560,8 +574,8 @@ def _on_bar_check_failed(self, event): 'http-check', failures=7, status=pebble.CheckStatus.DOWN, - level=layer.checks['http-check'].level, - startup=layer.checks['http-check'].startup, + level=pebble.CheckLevel(layer.checks['http-check'].level), + startup=pebble.CheckStartup(layer.checks['http-check'].startup), threshold=layer.checks['http-check'].threshold, ) foo_container = Container('foo', check_infos={check}, layers={'layer1': layer}) @@ -576,13 +590,13 @@ def _on_bar_check_failed(self, event): assert len(bar_infos) == 0 -def test_pebble_add_layer(): +def test_pebble_add_layer() -> None: class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.foo_pebble_ready, self._on_foo_ready) - def _on_foo_ready(self, _): + def _on_foo_ready(self, _: EventBase) -> None: self.unit.get_container('foo').add_layer( 'foo', {'checks': {'chk1': {'override': 'replace'}}}, @@ -595,14 +609,14 @@ def _on_foo_ready(self, _): assert chk1_info.status == pebble.CheckStatus.UP -def test_pebble_start_check(): +def test_pebble_start_check() -> None: class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.foo_pebble_ready, self._on_foo_ready) framework.observe(self.on.config_changed, self._on_config_changed) - def _on_foo_ready(self, _): + def _on_foo_ready(self, _: EventBase) -> None: container = self.unit.get_container('foo') container.add_layer( 'foo', @@ -617,7 +631,7 @@ def _on_foo_ready(self, _): }, ) - def _on_config_changed(self, _): + def _on_config_changed(self, _: EventBase) -> None: container = self.unit.get_container('foo') container.start_checks('chk1') @@ -636,20 +650,20 @@ def _on_config_changed(self, _): @pytest.fixture -def reset_security_logging(): +def reset_security_logging() -> Iterator[None]: """Ensure that we get a fresh juju-log for the security logging.""" _get_juju_log_and_app_id.cache_clear() yield _get_juju_log_and_app_id.cache_clear() -def test_pebble_stop_check(reset_security_logging: None): +def test_pebble_stop_check(reset_security_logging: None) -> None: class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.config_changed, self._on_config_changed) - def _on_config_changed(self, _): + def _on_config_changed(self, _: EventBase) -> None: container = self.unit.get_container('foo') container.stop_checks('chk1') @@ -662,8 +676,8 @@ def _on_config_changed(self, _): info_in = CheckInfo( 'chk1', status=pebble.CheckStatus.UP, - level=layer.checks['chk1'].level, - startup=layer.checks['chk1'].startup, + level=pebble.CheckLevel(layer.checks['chk1'].level), + startup=pebble.CheckStartup(layer.checks['chk1'].startup), threshold=layer.checks['chk1'].threshold, ) container = Container( @@ -677,13 +691,13 @@ def _on_config_changed(self, _): assert info_out.status == pebble.CheckStatus.INACTIVE -def test_pebble_replan_checks(): +def test_pebble_replan_checks() -> None: class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.config_changed, self._on_config_changed) - def _on_config_changed(self, _): + def _on_config_changed(self, _: EventBase) -> None: container = self.unit.get_container('foo') container.replan() @@ -695,8 +709,8 @@ def _on_config_changed(self, _): info_in = CheckInfo( 'chk1', status=pebble.CheckStatus.INACTIVE, - level=layer.checks['chk1'].level, - startup=layer.checks['chk1'].startup, + level=pebble.CheckLevel(layer.checks['chk1'].level), + startup=pebble.CheckStartup(layer.checks['chk1'].startup), threshold=layer.checks['chk1'].threshold, ) container = Container( @@ -743,14 +757,14 @@ def _on_config_changed(self, _): ], ) def test_add_layer_merge_check( - new_layer_name: str, combine: bool, new_layer_dict: pebble.LayerDict -): + new_layer_name: str, combine: bool, new_layer_dict: LayerDict +) -> None: class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on['my-container'].pebble_ready, self._on_pebble_ready) - def _on_pebble_ready(self, _: PebbleReadyEvent): + def _on_pebble_ready(self, _: PebbleReadyEvent) -> None: container = self.unit.get_container('my-container') container.add_layer(new_layer_name, Layer(new_layer_dict), combine=combine) @@ -769,9 +783,9 @@ def _on_pebble_ready(self, _: PebbleReadyEvent): assert layer_in.checks['server-ready'].threshold is not None check_in = CheckInfo( 'server-ready', - level=layer_in.checks['server-ready'].level, + level=pebble.CheckLevel(layer_in.checks['server-ready'].level), threshold=layer_in.checks['server-ready'].threshold, - startup=layer_in.checks['server-ready'].startup, + startup=pebble.CheckStartup(layer_in.checks['server-ready'].startup), ) container_in = Container( 'my-container', @@ -785,15 +799,15 @@ def _on_pebble_ready(self, _: PebbleReadyEvent): state_out = ctx.run(ctx.on.pebble_ready(container_in), state_in) check_out = state_out.get_container(container_in.name).get_check_info('server-ready') - new_layer_check = new_layer_dict['checks']['server-ready'] + new_layer_check = new_layer_dict.get('checks', {}).get('server-ready', {}) assert check_out.level == pebble.CheckLevel(new_layer_check.get('level', 'ready')) assert check_out.startup == pebble.CheckStartup(new_layer_check.get('startup', 'enabled')) assert check_out.threshold == new_layer_check.get('threshold', 10) @pytest.mark.parametrize('layer1_name,layer2_name', [('a-base', 'b-base'), ('b-base', 'a-base')]) -def test_layers_merge_in_plan(layer1_name, layer2_name): - layer1 = pebble.Layer({ +def test_layers_merge_in_plan(layer1_name: str, layer2_name: str) -> None: + layer1_dict: LayerDict = { 'services': { 'server': { 'override': 'replace', @@ -809,8 +823,8 @@ def test_layers_merge_in_plan(layer1_name, layer2_name): 'level': 'ready', 'startup': 'enabled', 'threshold': 10, - 'period': 1, - 'timeout': 28, + 'period': '1s', + 'timeout': '28s', 'http': {'url': 'http://localhost:5000/version'}, } }, @@ -823,8 +837,8 @@ def test_layers_merge_in_plan(layer1_name, layer2_name): 'labels': {'foo': 'bar'}, } }, - }) - layer2 = pebble.Layer({ + } + layer2_dict: LayerDict = { 'services': { 'server': { 'override': 'merge', @@ -844,7 +858,9 @@ def test_layers_merge_in_plan(layer1_name, layer2_name): 'location': 'https://loki2.example.com', }, }, - }) + } + layer1 = pebble.Layer(layer1_dict) + layer2 = pebble.Layer(layer2_dict) ctx = Context(CharmBase, meta={'name': 'foo', 'containers': {'my-container': {}}}) # TODO also a starting layer. @@ -872,7 +888,9 @@ def test_layers_merge_in_plan(layer1_name, layer2_name): assert check.timeout == 28 assert check.override == 'merge' assert check.level == pebble.CheckLevel.ALIVE - assert check.http['url'] == 'http://localhost:5050/version' + http = check.http + assert http is not None + assert http.get('url') == 'http://localhost:5050/version' log_target = plan.log_targets['loki'] assert log_target.type == 'loki' From 04ef78cf2208911360a641ce08e67e61aad2fd5d Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Mon, 15 Dec 2025 09:11:04 +1300 Subject: [PATCH 12/20] Update testing/tests/test_e2e/test_juju_log.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- testing/tests/test_e2e/test_juju_log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/tests/test_e2e/test_juju_log.py b/testing/tests/test_e2e/test_juju_log.py index e41120f42..728c8795f 100644 --- a/testing/tests/test_e2e/test_juju_log.py +++ b/testing/tests/test_e2e/test_juju_log.py @@ -23,7 +23,7 @@ def __init__(self, framework: Framework): for evt in self.on.events().values(): self.framework.observe(evt, self._on_event) - def _on_event(self, event: EventBase): + def _on_event(self, event: EventBase) -> None: if isinstance(event, CollectStatusEvent): return print('foo!') From be57cb58ea9c428f6ccc4f31c4b940a333e4df09 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Mon, 15 Dec 2025 09:35:06 +1300 Subject: [PATCH 13/20] fix: update the asserts to match the corrected layer values. --- testing/tests/test_e2e/test_pebble.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/tests/test_e2e/test_pebble.py b/testing/tests/test_e2e/test_pebble.py index cc4925d0a..3abaa37fe 100644 --- a/testing/tests/test_e2e/test_pebble.py +++ b/testing/tests/test_e2e/test_pebble.py @@ -884,8 +884,8 @@ def test_layers_merge_in_plan(layer1_name: str, layer2_name: str) -> None: check = plan.checks['server-ready'] assert check.startup == pebble.CheckStartup.ENABLED assert check.threshold == 10 - assert check.period == 1 - assert check.timeout == 28 + assert check.period == '1s' + assert check.timeout == '28s' assert check.override == 'merge' assert check.level == pebble.CheckLevel.ALIVE http = check.http From 2eb63d22339e8ab51d1921ecb0ef1cb34346e6f2 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Mon, 15 Dec 2025 09:35:35 +1300 Subject: [PATCH 14/20] fix: correct the call and type annotation, we're replacing a bound method. --- testing/tests/test_e2e/test_relations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/tests/test_e2e/test_relations.py b/testing/tests/test_e2e/test_relations.py index 3a7a40b1d..984a5388c 100644 --- a/testing/tests/test_e2e/test_relations.py +++ b/testing/tests/test_e2e/test_relations.py @@ -42,7 +42,7 @@ def define_event(cls, event_kind: str, event_type: 'type[EventBase]'): return super().define_event(event_kind, event_type) class MyCharm(CharmBase): - _call: Callable[[MyCharm, _Event], None] | None = None + _call: Callable[[_Event], None] | None = None called = False on: CharmEvents = MyCharmEvents() @@ -54,7 +54,7 @@ def __init__(self, framework: Framework): def _on_event(self, event: EventBase): if self._call: MyCharm.called = True - self._call(self, cast('Any', event)) + self._call(cast('Any', event)) return MyCharm From 55a79303ac4a753ad794837dd64fcdb02e11fa6a Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Mon, 15 Dec 2025 14:49:45 +1300 Subject: [PATCH 15/20] chore: remove -> None. --- testing/tests/test_e2e/test_juju_log.py | 2 +- testing/tests/test_e2e/test_manager.py | 12 +-- testing/tests/test_e2e/test_pebble.py | 90 +++++++++---------- testing/tests/test_e2e/test_ports.py | 8 +- testing/tests/test_e2e/test_rubbish_events.py | 12 +-- testing/tests/test_e2e/test_secrets.py | 76 ++++++++-------- testing/tests/test_e2e/test_status.py | 32 +++---- testing/tests/test_e2e/test_storage.py | 16 ++-- 8 files changed, 120 insertions(+), 128 deletions(-) diff --git a/testing/tests/test_e2e/test_juju_log.py b/testing/tests/test_e2e/test_juju_log.py index 728c8795f..e41120f42 100644 --- a/testing/tests/test_e2e/test_juju_log.py +++ b/testing/tests/test_e2e/test_juju_log.py @@ -23,7 +23,7 @@ def __init__(self, framework: Framework): for evt in self.on.events().values(): self.framework.observe(evt, self._on_event) - def _on_event(self, event: EventBase) -> None: + def _on_event(self, event: EventBase): if isinstance(event, CollectStatusEvent): return print('foo!') diff --git a/testing/tests/test_e2e/test_manager.py b/testing/tests/test_e2e/test_manager.py index 5bf666f89..0b9612c1b 100644 --- a/testing/tests/test_e2e/test_manager.py +++ b/testing/tests/test_e2e/test_manager.py @@ -23,7 +23,7 @@ def __init__(self, framework: Framework): for evt in self.on.events().values(): self.framework.observe(evt, self._on_event) - def _on_event(self, e: EventBase) -> None: + def _on_event(self, e: EventBase): if isinstance(e, CollectStatusEvent): return @@ -32,7 +32,7 @@ def _on_event(self, e: EventBase) -> None: return MyCharm -def test_manager(mycharm: Any) -> None: +def test_manager(mycharm: Any): ctx = Context(mycharm, meta=mycharm.META) with Manager(ctx, ctx.on.start(), State()) as manager: assert isinstance(manager.charm, mycharm) @@ -41,7 +41,7 @@ def test_manager(mycharm: Any) -> None: assert isinstance(state_out, State) -def test_manager_implicit(mycharm: Any) -> None: +def test_manager_implicit(mycharm: Any): ctx = Context(mycharm, meta=mycharm.META) with Manager(ctx, ctx.on.start(), State()) as manager: assert isinstance(manager.charm, mycharm) @@ -51,7 +51,7 @@ def test_manager_implicit(mycharm: Any) -> None: assert manager._emitted -def test_manager_reemit_fails(mycharm: Any) -> None: +def test_manager_reemit_fails(mycharm: Any): ctx = Context(mycharm, meta=mycharm.META) with Manager(ctx, ctx.on.start(), State()) as manager: manager.run() @@ -59,7 +59,7 @@ def test_manager_reemit_fails(mycharm: Any) -> None: manager.run() -def test_context_manager(mycharm: Any) -> None: +def test_context_manager(mycharm: Any): ctx = Context(mycharm, meta=mycharm.META) with ctx(ctx.on.start(), State()) as manager: state_out = manager.run() @@ -67,7 +67,7 @@ def test_context_manager(mycharm: Any) -> None: assert ctx.emitted_events[0].handle.kind == 'start' -def test_context_action_manager(mycharm: Any) -> None: +def test_context_action_manager(mycharm: Any): ctx = Context(mycharm, meta=mycharm.META, actions=mycharm.ACTIONS) with ctx(ctx.on.action('do-x'), State()) as manager: state_out = manager.run() diff --git a/testing/tests/test_e2e/test_pebble.py b/testing/tests/test_e2e/test_pebble.py index 3abaa37fe..1e56b0c2e 100644 --- a/testing/tests/test_e2e/test_pebble.py +++ b/testing/tests/test_e2e/test_pebble.py @@ -38,14 +38,14 @@ def __init__(self, framework: Framework): for evt in self.on.events().values(): self.framework.observe(evt, self._on_event) - def _on_event(self, event: EventBase) -> None: + def _on_event(self, event: EventBase): pass return MyCharm -def test_no_containers(charm_cls: type[CharmBase]) -> None: - def callback(self: CharmBase) -> None: +def test_no_containers(charm_cls: type[CharmBase]): + def callback(self: CharmBase): assert not self.unit.containers trigger( @@ -57,8 +57,8 @@ def callback(self: CharmBase) -> None: ) -def test_containers_from_meta(charm_cls: type[CharmBase]) -> None: - def callback(self: CharmBase) -> None: +def test_containers_from_meta(charm_cls: type[CharmBase]): + def callback(self: CharmBase): assert self.unit.containers assert self.unit.get_container('foo') @@ -72,8 +72,8 @@ def callback(self: CharmBase) -> None: @pytest.mark.parametrize('can_connect', (True, False)) -def test_connectivity(charm_cls: type[CharmBase], can_connect: bool) -> None: - def callback(self: CharmBase) -> None: +def test_connectivity(charm_cls: type[CharmBase], can_connect: bool): + def callback(self: CharmBase): assert can_connect == self.unit.get_container('foo').can_connect() trigger( @@ -85,13 +85,13 @@ def callback(self: CharmBase) -> None: ) -def test_fs_push(charm_cls: type[CharmBase]) -> None: +def test_fs_push(charm_cls: type[CharmBase]): text = 'lorem ipsum/n alles amat gloriae foo' file = tempfile.NamedTemporaryFile() pth = Path(file.name) pth.write_text(text) - def callback(self: CharmBase) -> None: + def callback(self: CharmBase): container = self.unit.get_container('foo') baz = container.pull('/bar/baz.txt') assert baz.read() == text @@ -114,10 +114,10 @@ def callback(self: CharmBase) -> None: @pytest.mark.parametrize('make_dirs', (True, False)) -def test_fs_pull(charm_cls: type[CharmBase], make_dirs: bool) -> None: +def test_fs_pull(charm_cls: type[CharmBase], make_dirs: bool): text = 'lorem ipsum/n alles amat gloriae foo' - def callback(self: CharmBase) -> None: + def callback(self: CharmBase): container = self.unit.get_container('foo') if make_dirs: container.push('/foo/bar/baz.txt', text, make_dirs=make_dirs) @@ -201,8 +201,8 @@ def callback(self: CharmBase) -> None: ('ps', PS), ), ) -def test_exec(charm_cls: type[CharmBase], cmd: str, out: str) -> None: - def callback(self: CharmBase) -> None: +def test_exec(charm_cls: type[CharmBase], cmd: str, out: str): + def callback(self: CharmBase): container = self.unit.get_container('foo') proc = container.exec([cmd]) proc.wait() @@ -234,13 +234,13 @@ def callback(self: CharmBase) -> None: [io.StringIO('hello world!'), None], ), ) -def test_exec_history_stdin(stdin: str | io.StringIO | None, write: str | None) -> None: +def test_exec_history_stdin(stdin: str | io.StringIO | None, write: str | None): class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) self.framework.observe(self.on.foo_pebble_ready, self._on_ready) - def _on_ready(self, _: EventBase) -> None: + def _on_ready(self, _: EventBase): proc = self.unit.get_container('foo').exec(['ls'], stdin=stdin) if write: assert proc.stdin is not None @@ -253,8 +253,8 @@ def _on_ready(self, _: EventBase) -> None: assert ctx.exec_history[container.name][0].stdin == 'hello world!' -def test_pebble_ready(charm_cls: type[CharmBase]) -> None: - def callback(self: CharmBase) -> None: +def test_pebble_ready(charm_cls: type[CharmBase]): + def callback(self: CharmBase): foo = self.unit.get_container('foo') assert foo.can_connect() @@ -270,13 +270,13 @@ def callback(self: CharmBase) -> None: @pytest.mark.parametrize('starting_service_status', pebble.ServiceStatus) -def test_pebble_plan(charm_cls: type[CharmBase], starting_service_status: ServiceStatus) -> None: +def test_pebble_plan(charm_cls: type[CharmBase], starting_service_status: ServiceStatus): class PlanCharm(charm_cls): def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.foo_pebble_ready, self._on_ready) - def _on_ready(self, event: PebbleReadyEvent) -> None: + def _on_ready(self, event: PebbleReadyEvent): foo = event.workload assert foo.get_plan().to_dict() == {'services': {'fooserv': {'startup': 'enabled'}}} @@ -345,7 +345,7 @@ def serv(name: str, obj: ServiceDict) -> pebble.Service: assert container.services['barserv'].startup == pebble.ServiceStartup.DISABLED -def test_exec_wait_error(charm_cls: type[CharmBase]) -> None: +def test_exec_wait_error(charm_cls: type[CharmBase]): state = State( containers={ Container( @@ -366,7 +366,7 @@ def test_exec_wait_error(charm_cls: type[CharmBase]) -> None: @pytest.mark.parametrize('command', (['foo'], ['foo', 'bar'], ['foo', 'bar', 'baz'])) -def test_exec_wait_output(charm_cls: type[CharmBase], command: list[str]) -> None: +def test_exec_wait_output(charm_cls: type[CharmBase], command: list[str]): state = State( containers={ Container( @@ -387,7 +387,7 @@ def test_exec_wait_output(charm_cls: type[CharmBase], command: list[str]) -> Non assert ctx.exec_history[container.name][0].command == command -def test_exec_wait_output_error(charm_cls: type[CharmBase]) -> None: +def test_exec_wait_output_error(charm_cls: type[CharmBase]): state = State( containers={ Container( @@ -406,7 +406,7 @@ def test_exec_wait_output_error(charm_cls: type[CharmBase]) -> None: proc.wait_output() -def test_pebble_custom_notice(charm_cls: type[CharmBase]) -> None: +def test_pebble_custom_notice(charm_cls: type[CharmBase]): notices = [ Notice(key='example.com/foo'), Notice(key='example.com/bar', last_data={'a': 'b'}), @@ -425,7 +425,7 @@ def test_pebble_custom_notice(charm_cls: type[CharmBase]) -> None: assert container.get_notices() == [n._to_ops() for n in notices] -def test_pebble_custom_notice_in_charm() -> None: +def test_pebble_custom_notice_in_charm(): key = 'example.com/test/charm' data = {'foo': 'bar'} user_id = 100 @@ -441,7 +441,7 @@ def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.foo_pebble_custom_notice, self._on_custom_notice) - def _on_custom_notice(self, event: PebbleCustomNoticeEvent) -> None: + def _on_custom_notice(self, event: PebbleCustomNoticeEvent): notice = event.notice assert notice.type == pebble.NoticeType.CUSTOM assert notice.key == key @@ -479,7 +479,7 @@ def _on_custom_notice(self, event: PebbleCustomNoticeEvent) -> None: ctx.run(ctx.on.pebble_custom_notice(container=container, notice=notices[-1]), state) -def test_pebble_check_failed() -> None: +def test_pebble_check_failed(): infos: list[LazyCheckInfo] = [] class MyCharm(CharmBase): @@ -487,7 +487,7 @@ def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.foo_pebble_check_failed, self._on_check_failed) - def _on_check_failed(self, event: PebbleCheckFailedEvent) -> None: + def _on_check_failed(self, event: PebbleCheckFailedEvent): infos.append(event.info) ctx = Context(MyCharm, meta={'name': 'foo', 'containers': {'foo': {}}}) @@ -514,7 +514,7 @@ def _on_check_failed(self, event: PebbleCheckFailedEvent) -> None: assert infos[0].failures == 7 -def test_pebble_check_recovered() -> None: +def test_pebble_check_recovered(): infos: list[LazyCheckInfo] = [] class MyCharm(CharmBase): @@ -522,7 +522,7 @@ def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.foo_pebble_check_recovered, self._on_check_recovered) - def _on_check_recovered(self, event: PebbleCheckRecoveredEvent) -> None: + def _on_check_recovered(self, event: PebbleCheckRecoveredEvent): infos.append(event.info) ctx = Context(MyCharm, meta={'name': 'foo', 'containers': {'foo': {}}}) @@ -548,7 +548,7 @@ def _on_check_recovered(self, event: PebbleCheckRecoveredEvent) -> None: assert infos[0].failures == 0 -def test_pebble_check_failed_two_containers() -> None: +def test_pebble_check_failed_two_containers(): foo_infos: list[LazyCheckInfo] = [] bar_infos: list[LazyCheckInfo] = [] @@ -558,10 +558,10 @@ def __init__(self, framework: Framework): framework.observe(self.on.foo_pebble_check_failed, self._on_foo_check_failed) framework.observe(self.on.bar_pebble_check_failed, self._on_bar_check_failed) - def _on_foo_check_failed(self, event: PebbleCheckFailedEvent) -> None: + def _on_foo_check_failed(self, event: PebbleCheckFailedEvent): foo_infos.append(event.info) - def _on_bar_check_failed(self, event: PebbleCheckFailedEvent) -> None: + def _on_bar_check_failed(self, event: PebbleCheckFailedEvent): bar_infos.append(event.info) ctx = Context(MyCharm, meta={'name': 'foo', 'containers': {'foo': {}, 'bar': {}}}) @@ -590,13 +590,13 @@ def _on_bar_check_failed(self, event: PebbleCheckFailedEvent) -> None: assert len(bar_infos) == 0 -def test_pebble_add_layer() -> None: +def test_pebble_add_layer(): class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.foo_pebble_ready, self._on_foo_ready) - def _on_foo_ready(self, _: EventBase) -> None: + def _on_foo_ready(self, _: EventBase): self.unit.get_container('foo').add_layer( 'foo', {'checks': {'chk1': {'override': 'replace'}}}, @@ -609,14 +609,14 @@ def _on_foo_ready(self, _: EventBase) -> None: assert chk1_info.status == pebble.CheckStatus.UP -def test_pebble_start_check() -> None: +def test_pebble_start_check(): class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.foo_pebble_ready, self._on_foo_ready) framework.observe(self.on.config_changed, self._on_config_changed) - def _on_foo_ready(self, _: EventBase) -> None: + def _on_foo_ready(self, _: EventBase): container = self.unit.get_container('foo') container.add_layer( 'foo', @@ -631,7 +631,7 @@ def _on_foo_ready(self, _: EventBase) -> None: }, ) - def _on_config_changed(self, _: EventBase) -> None: + def _on_config_changed(self, _: EventBase): container = self.unit.get_container('foo') container.start_checks('chk1') @@ -657,13 +657,13 @@ def reset_security_logging() -> Iterator[None]: _get_juju_log_and_app_id.cache_clear() -def test_pebble_stop_check(reset_security_logging: None) -> None: +def test_pebble_stop_check(reset_security_logging: None): class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.config_changed, self._on_config_changed) - def _on_config_changed(self, _: EventBase) -> None: + def _on_config_changed(self, _: EventBase): container = self.unit.get_container('foo') container.stop_checks('chk1') @@ -691,13 +691,13 @@ def _on_config_changed(self, _: EventBase) -> None: assert info_out.status == pebble.CheckStatus.INACTIVE -def test_pebble_replan_checks() -> None: +def test_pebble_replan_checks(): class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.config_changed, self._on_config_changed) - def _on_config_changed(self, _: EventBase) -> None: + def _on_config_changed(self, _: EventBase): container = self.unit.get_container('foo') container.replan() @@ -756,15 +756,13 @@ def _on_config_changed(self, _: EventBase) -> None: }, ], ) -def test_add_layer_merge_check( - new_layer_name: str, combine: bool, new_layer_dict: LayerDict -) -> None: +def test_add_layer_merge_check(new_layer_name: str, combine: bool, new_layer_dict: LayerDict): class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on['my-container'].pebble_ready, self._on_pebble_ready) - def _on_pebble_ready(self, _: PebbleReadyEvent) -> None: + def _on_pebble_ready(self, _: PebbleReadyEvent): container = self.unit.get_container('my-container') container.add_layer(new_layer_name, Layer(new_layer_dict), combine=combine) @@ -806,7 +804,7 @@ def _on_pebble_ready(self, _: PebbleReadyEvent) -> None: @pytest.mark.parametrize('layer1_name,layer2_name', [('a-base', 'b-base'), ('b-base', 'a-base')]) -def test_layers_merge_in_plan(layer1_name: str, layer2_name: str) -> None: +def test_layers_merge_in_plan(layer1_name: str, layer2_name: str): layer1_dict: LayerDict = { 'services': { 'server': { diff --git a/testing/tests/test_e2e/test_ports.py b/testing/tests/test_e2e/test_ports.py index 5e8c03fb4..aa4a33dc4 100644 --- a/testing/tests/test_e2e/test_ports.py +++ b/testing/tests/test_e2e/test_ports.py @@ -29,7 +29,7 @@ def ctx() -> Context[MyCharm]: return Context(MyCharm, meta=MyCharm.META) -def test_open_port(ctx: Context[MyCharm]) -> None: +def test_open_port(ctx: Context[MyCharm]): out = ctx.run(ctx.on.start(), State()) ports = tuple(out.opened_ports) assert len(ports) == 1 @@ -39,18 +39,18 @@ def test_open_port(ctx: Context[MyCharm]) -> None: assert port.port == 12 -def test_close_port(ctx: Context[MyCharm]) -> None: +def test_close_port(ctx: Context[MyCharm]): out = ctx.run(ctx.on.stop(), State(opened_ports={TCPPort(42)})) assert not out.opened_ports -def test_port_no_arguments() -> None: +def test_port_no_arguments(): with pytest.raises(RuntimeError): Port() @pytest.mark.parametrize('klass', (TCPPort, UDPPort)) -def test_port_port(klass: type[Port]) -> None: +def test_port_port(klass: type[Port]): with pytest.raises(StateValidationError): klass(port=0) with pytest.raises(StateValidationError): diff --git a/testing/tests/test_e2e/test_rubbish_events.py b/testing/tests/test_e2e/test_rubbish_events.py index 8d95a0267..74b2b5fe4 100644 --- a/testing/tests/test_e2e/test_rubbish_events.py +++ b/testing/tests/test_e2e/test_rubbish_events.py @@ -39,21 +39,21 @@ def __init__(self, framework: Framework): self.framework.observe(self.sub.on.sub, self._on_event) self.framework.observe(self.on.qux, self._on_event) - def _on_event(self, e: EventBase) -> None: + def _on_event(self, e: EventBase): MyCharm.evts.append(e) return MyCharm @pytest.mark.parametrize('evt_name', ('rubbish', 'foo', 'bar')) -def test_rubbish_event_raises(mycharm: type[CharmBase], evt_name: str) -> None: +def test_rubbish_event_raises(mycharm: type[CharmBase], evt_name: str): with pytest.raises(AttributeError): trigger(State(), evt_name, mycharm, meta={'name': 'foo'}) def test_rubbish_pebble_ready_event_raises( mycharm: type[CharmBase], monkeypatch: pytest.MonkeyPatch -) -> None: +): monkeypatch.setenv('SCENARIO_SKIP_CONSISTENCY_CHECKS', '1') # else it will whine about the container not being in state and meta; # but if we put the container in meta, it will actually register an event! @@ -62,14 +62,14 @@ def test_rubbish_pebble_ready_event_raises( @pytest.mark.parametrize('evt_name', ('qux',)) -def test_custom_events_fail(mycharm: type[CharmBase], evt_name: str) -> None: +def test_custom_events_fail(mycharm: type[CharmBase], evt_name: str): with pytest.raises(AttributeError): trigger(State(), evt_name, mycharm, meta={'name': 'foo'}) # cfr: https://github.com/PietroPasotti/ops-scenario/pull/11#discussion_r1101694961 @pytest.mark.parametrize('evt_name', ('sub',)) -def test_custom_events_sub_raise(mycharm: type[CharmBase], evt_name: str) -> None: +def test_custom_events_sub_raise(mycharm: type[CharmBase], evt_name: str): with pytest.raises(AttributeError): trigger(State(), evt_name, mycharm, meta={'name': 'foo'}) @@ -86,7 +86,7 @@ def test_custom_events_sub_raise(mycharm: type[CharmBase], evt_name: str) -> Non ('bar-relation-changed', True), ), ) -def test_is_custom_event(mycharm: type[CharmBase], evt_name: str, expected: bool) -> None: +def test_is_custom_event(mycharm: type[CharmBase], evt_name: str, expected: bool): spec: _CharmSpec[CharmBase] = _CharmSpec( charm_type=mycharm, meta={'name': 'mycharm', 'requires': {'foo': {}}} ) diff --git a/testing/tests/test_e2e/test_secrets.py b/testing/tests/test_e2e/test_secrets.py index 1a36df6a2..58ad89ea9 100644 --- a/testing/tests/test_e2e/test_secrets.py +++ b/testing/tests/test_e2e/test_secrets.py @@ -26,13 +26,13 @@ def __init__(self, framework: Framework): for evt in self.on.events().values(): self.framework.observe(evt, self._on_event) - def _on_event(self, event: EventBase) -> None: + def _on_event(self, event: EventBase): pass return MyCharm -def test_get_secret_no_secret(mycharm: type[CharmBase]) -> None: +def test_get_secret_no_secret(mycharm: type[CharmBase]): ctx = Context(mycharm, meta={'name': 'local'}) with ctx(ctx.on.update_status(), State()) as mgr: with pytest.raises(SecretNotFoundError): @@ -42,7 +42,7 @@ def test_get_secret_no_secret(mycharm: type[CharmBase]) -> None: @pytest.mark.parametrize('owner', ('app', 'unit')) -def test_get_secret(mycharm: type[CharmBase], owner: Literal['app', 'unit']) -> None: +def test_get_secret(mycharm: type[CharmBase], owner: Literal['app', 'unit']): ctx = Context(mycharm, meta={'name': 'local'}) secret = Secret({'a': 'b'}, owner=owner) with ctx( @@ -53,7 +53,7 @@ def test_get_secret(mycharm: type[CharmBase], owner: Literal['app', 'unit']) -> @pytest.mark.parametrize('owner', ('app', 'unit')) -def test_get_secret_get_refresh(mycharm: type[CharmBase], owner: Literal['app', 'unit']) -> None: +def test_get_secret_get_refresh(mycharm: type[CharmBase], owner: Literal['app', 'unit']): ctx = Context(mycharm, meta={'name': 'local'}) secret = Secret( tracked_content={'a': 'b'}, @@ -69,7 +69,7 @@ def test_get_secret_get_refresh(mycharm: type[CharmBase], owner: Literal['app', @pytest.mark.parametrize('app', (True, False)) -def test_get_secret_nonowner_peek_update(mycharm: type[CharmBase], app: bool) -> None: +def test_get_secret_nonowner_peek_update(mycharm: type[CharmBase], app: bool): ctx = Context(mycharm, meta={'name': 'local'}) secret = Secret( tracked_content={'a': 'b'}, @@ -93,9 +93,7 @@ def test_get_secret_nonowner_peek_update(mycharm: type[CharmBase], app: bool) -> @pytest.mark.parametrize('owner', ('app', 'unit')) -def test_get_secret_owner_peek_update( - mycharm: type[CharmBase], owner: Literal['app', 'unit'] -) -> None: +def test_get_secret_owner_peek_update(mycharm: type[CharmBase], owner: Literal['app', 'unit']): ctx = Context(mycharm, meta={'name': 'local'}) secret = Secret( tracked_content={'a': 'b'}, @@ -117,9 +115,7 @@ def test_get_secret_owner_peek_update( @pytest.mark.parametrize('owner', ('app', 'unit')) -def test_secret_changed_owner_evt_fails( - mycharm: type[CharmBase], owner: Literal['app', 'unit'] -) -> None: +def test_secret_changed_owner_evt_fails(mycharm: type[CharmBase], owner: Literal['app', 'unit']): ctx = Context(mycharm, meta={'name': 'local'}) secret = Secret( tracked_content={'a': 'b'}, @@ -138,9 +134,7 @@ def test_secret_changed_owner_evt_fails( ('remove', 1), ], ) -def test_consumer_events_failures( - mycharm: type[CharmBase], evt_suffix: str, revision: int | None -) -> None: +def test_consumer_events_failures(mycharm: type[CharmBase], evt_suffix: str, revision: int | None): ctx = Context(mycharm, meta={'name': 'local'}) secret = Secret( tracked_content={'a': 'b'}, @@ -154,7 +148,7 @@ def test_consumer_events_failures( @pytest.mark.parametrize('app', (True, False)) -def test_add(mycharm: type[CharmBase], app: bool) -> None: +def test_add(mycharm: type[CharmBase], app: bool): ctx = Context(mycharm, meta={'name': 'local'}) with ctx( ctx.on.update_status(), @@ -173,7 +167,7 @@ def test_add(mycharm: type[CharmBase], app: bool) -> None: assert secret.label == 'mylabel' -def test_set_legacy_behaviour(mycharm: type[CharmBase]) -> None: +def test_set_legacy_behaviour(mycharm: type[CharmBase]): # in juju < 3.1.7, secret owners always used to track the latest revision. # ref: https://bugs.launchpad.net/juju/+bug/2037120 ctx = Context(mycharm, meta={'name': 'local'}, juju_version='3.1.6') @@ -211,7 +205,7 @@ def test_set_legacy_behaviour(mycharm: type[CharmBase]) -> None: ) -def test_set(mycharm: type[CharmBase]) -> None: +def test_set(mycharm: type[CharmBase]): ctx = Context(mycharm, meta={'name': 'local'}) rev1, rev2 = {'foo': 'bar'}, {'foo': 'baz', 'qux': 'roz'} with ctx( @@ -243,7 +237,7 @@ def test_set(mycharm: type[CharmBase]) -> None: ) -def test_set_juju33(mycharm: type[CharmBase]) -> None: +def test_set_juju33(mycharm: type[CharmBase]): ctx = Context(mycharm, meta={'name': 'local'}, juju_version='3.3.1') rev1, rev2 = {'foo': 'bar'}, {'foo': 'baz', 'qux': 'roz'} with ctx( @@ -269,7 +263,7 @@ def test_set_juju33(mycharm: type[CharmBase]) -> None: @pytest.mark.parametrize('app', (True, False)) -def test_meta(mycharm: type[CharmBase], app: bool) -> None: +def test_meta(mycharm: type[CharmBase], app: bool): ctx = Context(mycharm, meta={'name': 'local'}) secret = Secret( {'a': 'b'}, @@ -301,7 +295,7 @@ def test_meta(mycharm: type[CharmBase], app: bool) -> None: @pytest.mark.parametrize('owner', ('app', 'unit', None)) def test_secret_permission_model( mycharm: type[CharmBase], leader: bool, owner: Literal['app', 'unit'] | None -) -> None: +): expect_manage = bool( # if you're the leader and own this app secret (owner == 'app' and leader) @@ -354,7 +348,7 @@ def test_secret_permission_model( @pytest.mark.parametrize('app', (True, False)) -def test_grant(mycharm: type[CharmBase], app: bool) -> None: +def test_grant(mycharm: type[CharmBase], app: bool): ctx = Context(mycharm, meta={'name': 'local', 'requires': {'foo': {'interface': 'bar'}}}) secret = Secret( {'a': 'b'}, @@ -383,7 +377,7 @@ def test_grant(mycharm: type[CharmBase], app: bool) -> None: assert vals == [{'remote'}] if app else [{'remote/0'}] -def test_update_metadata(mycharm: type[CharmBase]) -> None: +def test_update_metadata(mycharm: type[CharmBase]): exp = datetime.datetime(2050, 12, 12) ctx = Context(mycharm, meta={'name': 'local'}) @@ -415,13 +409,13 @@ def test_update_metadata(mycharm: type[CharmBase]) -> None: @pytest.mark.parametrize('leader', (True, False)) -def test_grant_after_add(leader: bool) -> None: +def test_grant_after_add(leader: bool): class GrantingCharm(CharmBase): def __init__(self, *args: Any): super().__init__(*args) self.framework.observe(self.on.start, self._on_start) - def _on_start(self, _: EventBase) -> None: + def _on_start(self, _: EventBase): if leader: secret = self.app.add_secret({'foo': 'bar'}) else: @@ -433,7 +427,7 @@ def _on_start(self, _: EventBase) -> None: ctx.run(ctx.on.start(), state) -def test_grant_nonowner(mycharm: type[CharmBase]) -> None: +def test_grant_nonowner(mycharm: type[CharmBase]): secret = Secret( {'a': 'b'}, label='mylabel', @@ -442,7 +436,7 @@ def test_grant_nonowner(mycharm: type[CharmBase]) -> None: ) secret_id = secret.id - def post_event(charm: CharmBase) -> None: + def post_event(charm: CharmBase): secret = charm.model.get_secret(id=secret_id) secret = charm.model.get_secret(label='mylabel') foo = charm.model.get_relation('foo') @@ -463,7 +457,7 @@ def post_event(charm: CharmBase) -> None: ) -def test_add_grant_revoke_remove() -> None: +def test_add_grant_revoke_remove(): class GrantingCharm(CharmBase): pass @@ -507,13 +501,13 @@ class GrantingCharm(CharmBase): output.get_secret(label='mylabel') -def test_secret_removed_event() -> None: +def test_secret_removed_event(): class SecretCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) self.framework.observe(self.on.secret_remove, self._on_secret_remove) - def _on_secret_remove(self, event: Any) -> None: + def _on_secret_remove(self, event: Any): event.secret.remove_revision(event.revision) ctx = Context(SecretCharm, meta={'name': 'foo'}) @@ -527,13 +521,13 @@ def _on_secret_remove(self, event: Any) -> None: assert ctx.removed_secret_revisions == [old_revision] -def test_secret_expired_event() -> None: +def test_secret_expired_event(): class SecretCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) self.framework.observe(self.on.secret_expired, self._on_secret_expired) - def _on_secret_expired(self, event: Any) -> None: + def _on_secret_expired(self, event: Any): event.secret.set_content({'password': 'newpass'}) event.secret.remove_revision(event.revision) @@ -548,13 +542,13 @@ def _on_secret_expired(self, event: Any) -> None: assert ctx.removed_secret_revisions == [old_revision] -def test_remove_bad_revision() -> None: +def test_remove_bad_revision(): class SecretCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) self.framework.observe(self.on.secret_remove, self._on_secret_remove) - def _on_secret_remove(self, event: Any) -> None: + def _on_secret_remove(self, event: Any): with pytest.raises(ValueError): event.secret.remove_revision(event.revision) @@ -570,13 +564,13 @@ def _on_secret_remove(self, event: Any) -> None: ) -def test_set_label_on_get() -> None: +def test_set_label_on_get(): class SecretCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) self.framework.observe(self.on.start, self._on_start) - def _on_start(self, _: EventBase) -> None: + def _on_start(self, _: EventBase): id = self.unit.add_secret({'foo': 'bar'}).id secret = self.model.get_secret(id=id, label='label1') assert secret.label == 'label1' @@ -588,12 +582,12 @@ def _on_start(self, _: EventBase) -> None: assert state.get_secret(label='label2').tracked_content == {'foo': 'bar'} -def test_no_additional_positional_arguments() -> None: +def test_no_additional_positional_arguments(): with pytest.raises(TypeError): Secret({}, {}) # type: ignore[misc] -def test_default_values() -> None: +def test_default_values(): contents = {'foo': 'bar'} secret = Secret(contents) assert secret.latest_content == secret.tracked_content == contents @@ -606,7 +600,7 @@ def test_default_values() -> None: assert secret.remote_grants == {} -def test_add_secret(secrets_context: Context[SecretsCharm]) -> None: +def test_add_secret(secrets_context: Context[SecretsCharm]): state = State(leader=True) state = secrets_context.run(secrets_context.on.action('add-secret'), state) @@ -643,7 +637,7 @@ def test_add_secret(secrets_context: Context[SecretsCharm]) -> None: 'label,description,expire,rotate', ], ) -def test_add_secret_with_metadata(secrets_context: Context[SecretsCharm], fields: str) -> None: +def test_add_secret_with_metadata(secrets_context: Context[SecretsCharm], fields: str): state = State(leader=True) state = secrets_context.run( secrets_context.on.action('add-with-meta', params={'fields': fields}), state @@ -687,7 +681,7 @@ def test_add_secret_with_metadata(secrets_context: Context[SecretsCharm], fields ) def test_set_secret( secrets_context: Context[SecretsCharm], flow: str, lookup_by: Literal['id', 'label'] -) -> None: +): secret = Secret({'some': 'content'}, owner='app', id='theid', label='thelabel') state = State(leader=True, secrets={secret}) params = {'flow': flow, f'secret{lookup_by}': f'the{lookup_by}'} @@ -715,7 +709,7 @@ def test_set_secret( assert info['rotation'] == rotation_values[counts['rotate']] -def common_assertions(scenario_secret: Secret | None, result: Result) -> None: +def common_assertions(scenario_secret: Secret | None, result: Result): if scenario_secret: assert scenario_secret.owner == 'app' assert not scenario_secret.remote_grants diff --git a/testing/tests/test_e2e/test_status.py b/testing/tests/test_e2e/test_status.py index 04409319b..b79210112 100644 --- a/testing/tests/test_e2e/test_status.py +++ b/testing/tests/test_e2e/test_status.py @@ -27,14 +27,14 @@ def __init__(self, framework: Framework): for evt in self.on.events().values(): self.framework.observe(evt, self._on_event) - def _on_event(self, event: EventBase) -> None: + def _on_event(self, event: EventBase): pass return MyCharm -def test_initial_status(mycharm: type[CharmBase]) -> None: - def post_event(charm: CharmBase) -> None: +def test_initial_status(mycharm: type[CharmBase]): + def post_event(charm: CharmBase): assert charm.unit.status == UnknownStatus() out = trigger( @@ -48,13 +48,13 @@ def post_event(charm: CharmBase) -> None: assert out.unit_status == UnknownStatus() -def test_status_history(mycharm: type[CharmBase]) -> None: +def test_status_history(mycharm: type[CharmBase]): class StatusCharm(mycharm): def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.update_status, self._on_update_status) - def _on_update_status(self, _: EventBase) -> None: + def _on_update_status(self, _: EventBase): for obj in (self.unit, self.app): obj.status = ops.ActiveStatus('1') obj.status = ops.BlockedStatus('2') @@ -82,13 +82,13 @@ def _on_update_status(self, _: EventBase) -> None: ] -def test_status_history_preservation(mycharm: type[CharmBase]) -> None: +def test_status_history_preservation(mycharm: type[CharmBase]): class StatusCharm(mycharm): def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.update_status, self._on_update_status) - def _on_update_status(self, _: EventBase) -> None: + def _on_update_status(self, _: EventBase): for obj in (self.unit, self.app): obj.status = WaitingStatus('3') @@ -113,7 +113,7 @@ def _on_update_status(self, _: EventBase) -> None: assert ctx.app_status_history == [ActiveStatus('bar')] -def test_workload_history(mycharm: type[CharmBase]) -> None: +def test_workload_history(mycharm: type[CharmBase]): class WorkloadCharm(mycharm): def __init__(self, framework: Framework): super().__init__(framework) @@ -121,13 +121,13 @@ def __init__(self, framework: Framework): framework.observe(self.on.start, self._on_start) framework.observe(self.on.update_status, self._on_update_status) - def _on_install(self, _: EventBase) -> None: + def _on_install(self, _: EventBase): self.unit.set_workload_version('1') - def _on_start(self, _: EventBase) -> None: + def _on_start(self, _: EventBase): self.unit.set_workload_version('1.1') - def _on_update_status(self, _: EventBase) -> None: + def _on_update_status(self, _: EventBase): self.unit.set_workload_version('1.2') ctx = Context( @@ -154,7 +154,7 @@ def _on_update_status(self, _: EventBase) -> None: UnknownStatus(), ), ) -def test_status_comparison(status: ops.StatusBase) -> None: +def test_status_comparison(status: ops.StatusBase): if isinstance(status, UnknownStatus): ops_status = ops.UnknownStatus() else: @@ -183,13 +183,13 @@ def test_status_comparison(status: ops.StatusBase) -> None: MaintenanceStatus('qux'), ), ) -def test_status_success(status: ops.StatusBase) -> None: +def test_status_success(status: ops.StatusBase): class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.update_status, self._on_update_status) - def _on_update_status(self, _: EventBase) -> None: + def _on_update_status(self, _: EventBase): self.unit.status = status ctx = Context(MyCharm, meta={'name': 'foo'}) @@ -203,13 +203,13 @@ def _on_update_status(self, _: EventBase) -> None: UnknownStatus(), ), ) -def test_status_error(status: ops.StatusBase, monkeypatch: pytest.MonkeyPatch) -> None: +def test_status_error(status: ops.StatusBase, monkeypatch: pytest.MonkeyPatch): class MyCharm(CharmBase): def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.update_status, self._on_update_status) - def _on_update_status(self, _: EventBase) -> None: + def _on_update_status(self, _: EventBase): self.unit.status = status monkeypatch.setenv('SCENARIO_BARE_CHARM_ERRORS', 'false') diff --git a/testing/tests/test_e2e/test_storage.py b/testing/tests/test_e2e/test_storage.py index cf2b6722e..a79cfb6e6 100644 --- a/testing/tests/test_e2e/test_storage.py +++ b/testing/tests/test_e2e/test_storage.py @@ -24,13 +24,13 @@ def no_storage_ctx() -> Context[MyCharmWithoutStorage]: return Context(MyCharmWithoutStorage, meta=MyCharmWithoutStorage.META) -def test_storage_get_null(no_storage_ctx: Context[MyCharmWithoutStorage]) -> None: +def test_storage_get_null(no_storage_ctx: Context[MyCharmWithoutStorage]): with no_storage_ctx(no_storage_ctx.on.update_status(), State()) as mgr: storages = mgr.charm.model.storages assert not len(storages) -def test_storage_get_unknown_name(storage_ctx: Context[MyCharmWithStorage]) -> None: +def test_storage_get_unknown_name(storage_ctx: Context[MyCharmWithStorage]): with storage_ctx(storage_ctx.on.update_status(), State()) as mgr: storages = mgr.charm.model.storages # not in metadata @@ -38,7 +38,7 @@ def test_storage_get_unknown_name(storage_ctx: Context[MyCharmWithStorage]) -> N storages['bar'] -def test_storage_request_unknown_name(storage_ctx: Context[MyCharmWithStorage]) -> None: +def test_storage_request_unknown_name(storage_ctx: Context[MyCharmWithStorage]): with storage_ctx(storage_ctx.on.update_status(), State()) as mgr: storages = mgr.charm.model.storages # not in metadata @@ -46,7 +46,7 @@ def test_storage_request_unknown_name(storage_ctx: Context[MyCharmWithStorage]) storages.request('bar') -def test_storage_get_some(storage_ctx: Context[MyCharmWithStorage]) -> None: +def test_storage_get_some(storage_ctx: Context[MyCharmWithStorage]): with storage_ctx(storage_ctx.on.update_status(), State()) as mgr: storages = mgr.charm.model.storages # known but none attached @@ -54,7 +54,7 @@ def test_storage_get_some(storage_ctx: Context[MyCharmWithStorage]) -> None: @pytest.mark.parametrize('n', (1, 3, 5)) -def test_storage_add(storage_ctx: Context[MyCharmWithStorage], n: int) -> None: +def test_storage_add(storage_ctx: Context[MyCharmWithStorage], n: int): with storage_ctx(storage_ctx.on.update_status(), State()) as mgr: storages = mgr.charm.model.storages storages.request('foo', n) @@ -62,7 +62,7 @@ def test_storage_add(storage_ctx: Context[MyCharmWithStorage], n: int) -> None: assert storage_ctx.requested_storages['foo'] == n -def test_storage_usage(storage_ctx: Context[MyCharmWithStorage]) -> None: +def test_storage_usage(storage_ctx: Context[MyCharmWithStorage]): storage = Storage('foo') # setup storage with some content (storage.get_filesystem(storage_ctx) / 'myfile.txt').write_text('helloworld') # type: ignore[reportUnknownMemberType] @@ -83,11 +83,11 @@ def test_storage_usage(storage_ctx: Context[MyCharmWithStorage]) -> None: ).read_text() == 'helloworlds' -def test_storage_attached_event(storage_ctx: Context[MyCharmWithStorage]) -> None: +def test_storage_attached_event(storage_ctx: Context[MyCharmWithStorage]): storage = Storage('foo') storage_ctx.run(storage_ctx.on.storage_attached(storage), State(storages={storage})) -def test_storage_detaching_event(storage_ctx: Context[MyCharmWithStorage]) -> None: +def test_storage_detaching_event(storage_ctx: Context[MyCharmWithStorage]): storage = Storage('foo') storage_ctx.run(storage_ctx.on.storage_detaching(storage), State(storages={storage})) From e243933eac6410df27aad153b38bde1ee07c7d60 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Mon, 15 Dec 2025 14:51:27 +1300 Subject: [PATCH 16/20] chore: remove scopes from type: ignore. --- testing/tests/test_e2e/test_pebble.py | 8 ++++---- testing/tests/test_e2e/test_relations.py | 4 ++-- testing/tests/test_e2e/test_secrets.py | 2 +- testing/tests/test_e2e/test_storage.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/testing/tests/test_e2e/test_pebble.py b/testing/tests/test_e2e/test_pebble.py index 1e56b0c2e..04dd66311 100644 --- a/testing/tests/test_e2e/test_pebble.py +++ b/testing/tests/test_e2e/test_pebble.py @@ -24,7 +24,7 @@ from scenario import Context from scenario.state import CheckInfo, Container, Exec, Mount, Notice, State -from ..helpers import jsonpatch_delta, trigger # type: ignore[reportUnknownVariableType] +from ..helpers import jsonpatch_delta, trigger # type: ignore if TYPE_CHECKING: from ops.pebble import LayerDict, ServiceDict @@ -164,7 +164,7 @@ def callback(self: CharmBase): assert file.read_text() == text # shortcut for API niceness purposes: - file = container.get_filesystem(ctx) / 'foo' / 'bar' / 'baz.txt' # type: ignore[reportUnknownMemberType] + file = container.get_filesystem(ctx) / 'foo' / 'bar' / 'baz.txt' # type: ignore assert file.read_text() == text else: @@ -360,9 +360,9 @@ def test_exec_wait_error(charm_cls: type[CharmBase]): with ctx(ctx.on.start(), state) as mgr: container = mgr.charm.unit.get_container('foo') proc = container.exec(['foo']) - with pytest.raises(ExecError) as exc_info: # type: ignore[reportUnknownVariableType] + with pytest.raises(ExecError) as exc_info: # type: ignore proc.wait_output() - assert exc_info.value.stdout == 'hello pebble' # type: ignore[reportUnknownMemberType] + assert exc_info.value.stdout == 'hello pebble' # type: ignore @pytest.mark.parametrize('command', (['foo'], ['foo', 'bar'], ['foo', 'bar', 'baz'])) diff --git a/testing/tests/test_e2e/test_relations.py b/testing/tests/test_e2e/test_relations.py index 984a5388c..e1746792b 100644 --- a/testing/tests/test_e2e/test_relations.py +++ b/testing/tests/test_e2e/test_relations.py @@ -538,13 +538,13 @@ def callback(charm: CharmBase, event: EventBase): assert 'remote unit ID unset; no remote unit data present' in caplog.text -@pytest.mark.parametrize('data', (set(), {}, [], (), 1, 1.0, None, b'')) # type: ignore[reportUnknownArgumentType] +@pytest.mark.parametrize('data', (set(), {}, [], (), 1, 1.0, None, b'')) # type: ignore def test_relation_unit_data_bad_types(mycharm: Any, data: object): with pytest.raises(StateValidationError): Relation(endpoint='foo', interface='foo', remote_units_data={0: {'a': cast('Any', data)}}) -@pytest.mark.parametrize('data', (set(), {}, [], (), 1, 1.0, None, b'')) # type: ignore[reportUnknownArgumentType] +@pytest.mark.parametrize('data', (set(), {}, [], (), 1, 1.0, None, b'')) # type: ignore def test_relation_app_data_bad_types(mycharm: Any, data: object): with pytest.raises(StateValidationError): Relation(endpoint='foo', interface='foo', local_app_data={'a': cast('Any', data)}) diff --git a/testing/tests/test_e2e/test_secrets.py b/testing/tests/test_e2e/test_secrets.py index 58ad89ea9..243a9302d 100644 --- a/testing/tests/test_e2e/test_secrets.py +++ b/testing/tests/test_e2e/test_secrets.py @@ -584,7 +584,7 @@ def _on_start(self, _: EventBase): def test_no_additional_positional_arguments(): with pytest.raises(TypeError): - Secret({}, {}) # type: ignore[misc] + Secret({}, {}) # type: ignore def test_default_values(): diff --git a/testing/tests/test_e2e/test_storage.py b/testing/tests/test_e2e/test_storage.py index a79cfb6e6..b9bdf360b 100644 --- a/testing/tests/test_e2e/test_storage.py +++ b/testing/tests/test_e2e/test_storage.py @@ -65,7 +65,7 @@ def test_storage_add(storage_ctx: Context[MyCharmWithStorage], n: int): def test_storage_usage(storage_ctx: Context[MyCharmWithStorage]): storage = Storage('foo') # setup storage with some content - (storage.get_filesystem(storage_ctx) / 'myfile.txt').write_text('helloworld') # type: ignore[reportUnknownMemberType] + (storage.get_filesystem(storage_ctx) / 'myfile.txt').write_text('helloworld') # type: ignore with storage_ctx(storage_ctx.on.update_status(), State(storages={storage})) as mgr: foo = mgr.charm.model.storages['foo'][0] @@ -79,7 +79,7 @@ def test_storage_usage(storage_ctx: Context[MyCharmWithStorage]): # post-mortem: inspect fs contents. assert ( - storage.get_filesystem(storage_ctx) / 'path.py' # type: ignore[reportUnknownMemberType] + storage.get_filesystem(storage_ctx) / 'path.py' # type: ignore ).read_text() == 'helloworlds' From effb48f14249ca0e79b5197b79102eeed3368380 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Tue, 16 Dec 2025 15:02:59 +1300 Subject: [PATCH 17/20] Post merge fixes. --- testing/tests/test_e2e/test_pebble.py | 4 ++-- testing/tests/test_e2e/test_ports.py | 2 +- testing/tests/test_e2e/test_storage.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/testing/tests/test_e2e/test_pebble.py b/testing/tests/test_e2e/test_pebble.py index 3ec8cfcab..ed98d2b77 100644 --- a/testing/tests/test_e2e/test_pebble.py +++ b/testing/tests/test_e2e/test_pebble.py @@ -78,7 +78,7 @@ def callback(self: ops.CharmBase): ) -def test_fs_push(tmp_path, charm_cls: type[ops.CharmBase]): +def test_fs_push(tmp_path: pathlib.Path, charm_cls: type[ops.CharmBase]): text = 'lorem ipsum/n alles amat gloriae foo' pth = tmp_path / 'textfile' @@ -107,7 +107,7 @@ def callback(self: ops.CharmBase): @pytest.mark.parametrize('make_dirs', (True, False)) -def test_fs_pull(tmp_path, charm_cls: type[ops.CharmBase], make_dirs: bool): +def test_fs_pull(tmp_path: pathlib.Path, charm_cls: type[ops.CharmBase], make_dirs: bool): text = 'lorem ipsum/n alles amat gloriae foo' def callback(self: ops.CharmBase): diff --git a/testing/tests/test_e2e/test_ports.py b/testing/tests/test_e2e/test_ports.py index f141eb29b..f2c69c01f 100644 --- a/testing/tests/test_e2e/test_ports.py +++ b/testing/tests/test_e2e/test_ports.py @@ -32,7 +32,7 @@ def _close_port(self, _: StopEvent): @pytest.fixture def ctx() -> Context[MyCharm]: - return Context(MyCharm, meta=MyCharm.META) + return Context(MyCharm, meta=dict(MyCharm.META)) def test_open_port(ctx: Context[MyCharm]): diff --git a/testing/tests/test_e2e/test_storage.py b/testing/tests/test_e2e/test_storage.py index 34750c203..820248d58 100644 --- a/testing/tests/test_e2e/test_storage.py +++ b/testing/tests/test_e2e/test_storage.py @@ -25,12 +25,12 @@ class MyCharmWithoutStorage(CharmBase): @pytest.fixture def storage_ctx() -> Context[MyCharmWithStorage]: - return Context(MyCharmWithStorage, meta=MyCharmWithStorage.META) + return Context(MyCharmWithStorage, meta=dict(MyCharmWithStorage.META)) @pytest.fixture def no_storage_ctx() -> Context[MyCharmWithoutStorage]: - return Context(MyCharmWithoutStorage, meta=MyCharmWithoutStorage.META) + return Context(MyCharmWithoutStorage, meta=dict(MyCharmWithoutStorage.META)) def test_storage_get_null(no_storage_ctx: Context[MyCharmWithoutStorage]): From 7028747c29284bbbdd8ca0769a757586c9fc1378 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Fri, 19 Dec 2025 08:23:07 +1300 Subject: [PATCH 18/20] Add generic type to avoid ignore. --- testing/src/scenario/state.py | 2 +- testing/tests/test_e2e/test_pebble.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/src/scenario/state.py b/testing/src/scenario/state.py index 06b2b57c6..db185c68f 100644 --- a/testing/src/scenario/state.py +++ b/testing/src/scenario/state.py @@ -1139,7 +1139,7 @@ def services(self) -> dict[str, pebble.ServiceInfo]: infos[name] = info return infos - def get_filesystem(self, ctx: Context) -> pathlib.Path: + def get_filesystem(self, ctx: Context[CharmBase]) -> pathlib.Path: """Simulated Pebble filesystem in this context. Returns: diff --git a/testing/tests/test_e2e/test_pebble.py b/testing/tests/test_e2e/test_pebble.py index ed98d2b77..cac5bdcc2 100644 --- a/testing/tests/test_e2e/test_pebble.py +++ b/testing/tests/test_e2e/test_pebble.py @@ -156,7 +156,7 @@ def callback(self: ops.CharmBase): assert file.read_text() == text # shortcut for API niceness purposes: - file = container.get_filesystem(ctx) / 'foo' / 'bar' / 'baz.txt' # type: ignore + file = container.get_filesystem(ctx) / 'foo' / 'bar' / 'baz.txt' assert file.read_text() == text else: From 19c2982f6b42bee6eb899b06d6c297a1eaff27ff Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Fri, 19 Dec 2025 08:41:55 +1300 Subject: [PATCH 19/20] Reformat per review request. --- testing/tests/test_e2e/test_pebble.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/testing/tests/test_e2e/test_pebble.py b/testing/tests/test_e2e/test_pebble.py index cac5bdcc2..3bd5f7cef 100644 --- a/testing/tests/test_e2e/test_pebble.py +++ b/testing/tests/test_e2e/test_pebble.py @@ -145,14 +145,12 @@ def callback(self: ops.CharmBase): file = tmp_path / 'bar' / 'baz.txt' # another is: - assert ( - file == pathlib.Path(out.get_container('foo').mounts['foo'].source) / 'bar' / 'baz.txt' - ) + base = pathlib.Path(out.get_container('foo').mounts['foo'].source) + assert file == base / 'bar' / 'baz.txt' # but that is actually a symlink to the context's root tmp folder: - assert ( - pathlib.Path(ctx._tmp.name) / 'containers' / 'foo' / 'foo' / 'bar' / 'baz.txt' - ).read_text() == text + base = pathlib.Path(ctx._tmp.name) + assert (base / 'containers' / 'foo' / 'foo' / 'bar' / 'baz.txt').read_text() == text assert file.read_text() == text # shortcut for API niceness purposes: From f884387675f6042fb0ae0c19c7504eebc07727e3 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Fri, 19 Dec 2025 08:42:52 +1300 Subject: [PATCH 20/20] Remove return type annotation per review request. --- testing/tests/test_e2e/test_pebble.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/testing/tests/test_e2e/test_pebble.py b/testing/tests/test_e2e/test_pebble.py index 3bd5f7cef..3411c7b98 100644 --- a/testing/tests/test_e2e/test_pebble.py +++ b/testing/tests/test_e2e/test_pebble.py @@ -7,7 +7,6 @@ import datetime import io import pathlib -from collections.abc import Iterator from typing import TYPE_CHECKING import pytest @@ -642,7 +641,7 @@ def _on_config_changed(self, _: ops.EventBase): @pytest.fixture -def reset_security_logging() -> Iterator[None]: +def reset_security_logging(): """Ensure that we get a fresh juju-log for the security logging.""" _get_juju_log_and_app_id.cache_clear() yield