diff --git a/pyproject.toml b/pyproject.toml index ed1ee4ede..131ad6492 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -226,21 +226,6 @@ exclude = ["tracing/ops_tracing/vendor/*"] "testing/tests/*" = [ # All documentation linting. "D", - # TODO: the below ignores should be fixed - "CPY", # flake8-copyright - "I001", # isort - "B017", # Do not assert blind exception - "B018", # Useless attribute access - "E501", # Line too long - "N999", # Invalid module name - "N813", # CamelCase imported as lowercase - "S105", # Possible hardcoded password - "S108", # Probably insecure usage of /tmp - "W291", # Trailing whitespace - "UP037", # Remove quotes from type annotation - "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` - "RUF015", # Prefer `next` over single element slice - "SIM115", # Use a context manager for opening files ] "ops/_private/timeconv.py" = [ "RUF001", # String contains ambiguous `µ` (MICRO SIGN). Did you mean `μ` (GREEK SMALL LETTER MU)? diff --git a/testing/tests/__init__.py b/testing/tests/__init__.py index e69de29bb..db3bfe1a6 100644 --- a/testing/tests/__init__.py +++ b/testing/tests/__init__.py @@ -0,0 +1,2 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. diff --git a/testing/tests/helpers.py b/testing/tests/helpers.py index 913dcbf98..ed285ccf3 100644 --- a/testing/tests/helpers.py +++ b/testing/tests/helpers.py @@ -1,13 +1,15 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations import dataclasses import logging +from collections.abc import Callable from pathlib import Path from typing import TYPE_CHECKING, Any, TypeVar -from collections.abc import Callable import jsonpatch - from scenario.context import _DEFAULT_JUJU_VERSION, Context from scenario.state import _Event @@ -20,17 +22,17 @@ def trigger( - state: 'State', - event: str | '_Event', - charm_type: type['CharmType'], - pre_event: Callable[['CharmType'], None] | None = None, - post_event: Callable[['CharmType'], None] | None = None, + state: State, + event: str | _Event, + charm_type: type[CharmType], + pre_event: Callable[[CharmType], None] | None = None, + post_event: Callable[[CharmType], None] | None = None, meta: dict[str, Any] | None = None, actions: dict[str, Any] | None = None, config: dict[str, Any] | None = None, charm_root: str | Path | None = None, juju_version: str = _DEFAULT_JUJU_VERSION, -) -> 'State': +) -> State: ctx = Context( charm_type=charm_type, meta=meta, @@ -42,10 +44,10 @@ def trigger( if isinstance(event, str): if event.startswith('relation_'): assert len(tuple(state.relations)) == 1, 'shortcut only works with one relation' - event = getattr(ctx.on, event)(tuple(state.relations)[0]) + event = getattr(ctx.on, event)(next(iter(state.relations))) elif event.startswith('pebble_'): assert len(tuple(state.containers)) == 1, 'shortcut only works with one container' - event = getattr(ctx.on, event)(tuple(state.containers)[0]) + event = getattr(ctx.on, event)(next(iter(state.containers))) else: event = getattr(ctx.on, event)() assert isinstance(event, _Event) @@ -58,7 +60,7 @@ def trigger( return state_out -def jsonpatch_delta(self, other: 'State'): +def jsonpatch_delta(self, other: State): dict_other = dataclasses.asdict(other) dict_self = dataclasses.asdict(self) for attr in ( diff --git a/testing/tests/test_charm_spec_autoload.py b/testing/tests/test_charm_spec_autoload.py index d35f2e6e2..1d77caf6e 100644 --- a/testing/tests/test_charm_spec_autoload.py +++ b/testing/tests/test_charm_spec_autoload.py @@ -1,3 +1,6 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations import importlib @@ -7,7 +10,6 @@ import pytest import yaml - from scenario import Context, Relation, State from scenario.context import ContextSetupError from scenario.state import CharmType, MetadataNotFoundError, _CharmSpec diff --git a/testing/tests/test_consistency_checker.py b/testing/tests/test_consistency_checker.py index 99ebd969b..81ce7eb4e 100644 --- a/testing/tests/test_consistency_checker.py +++ b/testing/tests/test_consistency_checker.py @@ -1,10 +1,11 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations import dataclasses import pytest -import ops - from scenario._consistency_checker import check_consistency from scenario.context import Context from scenario.errors import InconsistentScenarioError @@ -29,15 +30,17 @@ _Event, ) +import ops + class MyCharm(ops.CharmBase): pass def assert_inconsistent( - state: 'State', - event: '_Event', - charm_spec: '_CharmSpec', + state: State, + event: _Event, + charm_spec: _CharmSpec, juju_version='3.0', unit_id=0, ): @@ -46,9 +49,9 @@ def assert_inconsistent( def assert_consistent( - state: 'State', - event: '_Event', - charm_spec: '_CharmSpec', + state: State, + event: _Event, + charm_spec: _CharmSpec, juju_version='3.0', unit_id=0, ): diff --git a/testing/tests/test_context.py b/testing/tests/test_context.py index a871b8d52..054698193 100644 --- a/testing/tests/test_context.py +++ b/testing/tests/test_context.py @@ -1,15 +1,18 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations import os from unittest.mock import patch import pytest -from ops import CharmBase - from scenario import Context, State from scenario.errors import UncaughtCharmError from scenario.state import _Event, _next_action_id +from ops import CharmBase + class MyCharm(CharmBase): pass diff --git a/testing/tests/test_context_on.py b/testing/tests/test_context_on.py index 15687f10b..88d11f358 100644 --- a/testing/tests/test_context_on.py +++ b/testing/tests/test_context_on.py @@ -1,14 +1,16 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations import copy import datetime import typing -import ops import pytest - import scenario +import ops META = { 'name': 'context-charm', @@ -333,7 +335,7 @@ def test_relation_departed_event(): relation = scenario.Relation('baz') state_in = scenario.State(relations=[relation]) # These look like: - # ctx.run(ctx.on.baz_relation_departed(unit=unit_ordinal, departing_unit=unit_ordinal), state) + # ctx.run(ctx.on.baz_relation_departed(unit=unit_num, departing_unit=unit_num), state) with ctx(ctx.on.relation_departed(relation, remote_unit=2, departing_unit=1), state_in) as mgr: mgr.run() relation_event, collect_status = mgr.charm.observed diff --git a/testing/tests/test_e2e/__init__.py b/testing/tests/test_e2e/__init__.py index e69de29bb..db3bfe1a6 100644 --- a/testing/tests/test_e2e/__init__.py +++ b/testing/tests/test_e2e/__init__.py @@ -0,0 +1,2 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. diff --git a/testing/tests/test_e2e/conftest.py b/testing/tests/test_e2e/conftest.py index e5ea4ddd1..2768758c1 100644 --- a/testing/tests/test_e2e/conftest.py +++ b/testing/tests/test_e2e/conftest.py @@ -1,12 +1,15 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations -from typing import Any from collections.abc import Generator +from typing import Any import pytest import yaml - from scenario import Context + from test.charms.test_secrets.src.charm import SecretsCharm diff --git a/testing/tests/test_e2e/test_actions.py b/testing/tests/test_e2e/test_actions.py index 40d65fc6a..58cbabd44 100644 --- a/testing/tests/test_e2e/test_actions.py +++ b/testing/tests/test_e2e/test_actions.py @@ -1,13 +1,16 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations import pytest +from scenario import Context +from scenario.state import State, _Action, _next_action_id + from ops import __version__ as ops_version +from ops._private.harness import ActionFailed from ops.charm import ActionEvent, CharmBase from ops.framework import Framework -from ops._private.harness import ActionFailed - -from scenario import Context -from scenario.state import State, _Action, _next_action_id @pytest.fixture(scope='function') diff --git a/testing/tests/test_e2e/test_cloud_spec.py b/testing/tests/test_e2e/test_cloud_spec.py index 33eb4b11a..8a952d134 100644 --- a/testing/tests/test_e2e/test_cloud_spec.py +++ b/testing/tests/test_e2e/test_cloud_spec.py @@ -1,10 +1,13 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations -import ops import pytest - import scenario +import ops + class MyCharm(ops.CharmBase): def __init__(self, framework: ops.Framework): diff --git a/testing/tests/test_e2e/test_config.py b/testing/tests/test_e2e/test_config.py index cb226d59f..a3dc014ef 100644 --- a/testing/tests/test_e2e/test_config.py +++ b/testing/tests/test_e2e/test_config.py @@ -1,10 +1,14 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations import pytest +from scenario.state import State + from ops.charm import CharmBase from ops.framework import Framework -from scenario.state import State from ..helpers import trigger diff --git a/testing/tests/test_e2e/test_deferred.py b/testing/tests/test_e2e/test_deferred.py index 643d96e32..94043cc2a 100644 --- a/testing/tests/test_e2e/test_deferred.py +++ b/testing/tests/test_e2e/test_deferred.py @@ -1,13 +1,18 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations import typing +from collections.abc import Mapping -import ops import pytest -from ops.framework import LifecycleEvent - from scenario import Context from scenario.state import Container, Relation, State, _Event + +import ops +from ops.framework import LifecycleEvent + from ..helpers import trigger CHARM_CALLED = 0 @@ -16,13 +21,13 @@ @pytest.fixture(scope='function') def mycharm(): class MyCharm(ops.CharmBase): - META: dict[str, typing.Any] = { + META: Mapping[str, typing.Any] = { 'name': 'mycharm', 'requires': {'foo': {'interface': 'bar'}}, 'containers': {'foo': {'type': 'oci-image'}}, } defer_next = 0 - captured: list[ops.EventBase] = [] + captured: typing.ClassVar[list[ops.EventBase]] = [] def __init__(self, framework: ops.Framework): super().__init__(framework) diff --git a/testing/tests/test_e2e/test_event.py b/testing/tests/test_e2e/test_event.py index 83c0b449f..979db8158 100644 --- a/testing/tests/test_e2e/test_event.py +++ b/testing/tests/test_e2e/test_event.py @@ -1,12 +1,14 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations -import ops import pytest -from ops import CharmBase - from scenario import Context from scenario.state import State, _CharmSpec, _Event, _EventType +import ops + @pytest.mark.parametrize( 'evt, expected_type', @@ -40,7 +42,7 @@ def test_event_type(evt, expected_type): assert event._is_secret_event is (expected_type is _EventType.SECRET) assert event._is_action_event is (expected_type is _EventType.ACTION) - class MyCharm(CharmBase): + class MyCharm(ops.CharmBase): pass spec = _CharmSpec( @@ -61,10 +63,7 @@ class MyCharm(CharmBase): def test_emitted_framework(): - class MyCharm(CharmBase): - META = {'name': 'joop'} - - ctx = Context(MyCharm, meta=MyCharm.META, capture_framework_events=True) + ctx = Context(ops.CharmBase, meta={'name': 'joop'}, capture_framework_events=True) ctx.run(ctx.on.update_status(), State()) assert len(ctx.emitted_events) == 4 assert list(map(type, ctx.emitted_events)) == [ @@ -76,9 +75,7 @@ class MyCharm(CharmBase): def test_emitted_deferred(): - class MyCharm(CharmBase): - META = {'name': 'joop'} - + class MyCharm(ops.CharmBase): def __init__(self, framework: ops.Framework): super().__init__(framework) framework.observe(self.on.update_status, self._on_update_status) @@ -88,7 +85,7 @@ def _on_update_status(self, _: ops.UpdateStatusEvent): ctx = Context( MyCharm, - meta=MyCharm.META, + meta={'name': 'joop'}, capture_deferred_events=True, capture_framework_events=True, ) diff --git a/testing/tests/test_e2e/test_juju_log.py b/testing/tests/test_e2e/test_juju_log.py index cbbbc1494..2e18446f0 100644 --- a/testing/tests/test_e2e/test_juju_log.py +++ b/testing/tests/test_e2e/test_juju_log.py @@ -1,20 +1,25 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations import logging +from collections.abc import Mapping +from typing import Any import pytest -from ops.charm import CharmBase, CollectStatusEvent - from scenario import Context from scenario.state import JujuLogLine, State +from ops.charm import CharmBase, CollectStatusEvent + logger = logging.getLogger('testing logger') @pytest.fixture(scope='function') def mycharm(): class MyCharm(CharmBase): - META = {'name': 'mycharm'} + META: Mapping[str, Any] = {'name': 'mycharm'} def __init__(self, framework): super().__init__(framework) diff --git a/testing/tests/test_e2e/test_manager.py b/testing/tests/test_e2e/test_manager.py index 84c5c5d5a..816d28acb 100644 --- a/testing/tests/test_e2e/test_manager.py +++ b/testing/tests/test_e2e/test_manager.py @@ -1,18 +1,24 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations -import pytest -from ops import ActiveStatus -from ops.charm import CharmBase, CollectStatusEvent +from collections.abc import Mapping +from typing import Any +import pytest from scenario import Context, State from scenario.context import AlreadyEmittedError, Manager +from ops import ActiveStatus +from ops.charm import CharmBase, CollectStatusEvent + @pytest.fixture(scope='function') def mycharm(): class MyCharm(CharmBase): - META = {'name': 'mycharm'} - ACTIONS = {'do-x': {}} + META: Mapping[str, Any] = {'name': 'mycharm'} + ACTIONS: Mapping[str, Any] = {'do-x': {}} def __init__(self, framework): super().__init__(framework) diff --git a/testing/tests/test_e2e/test_network.py b/testing/tests/test_e2e/test_network.py index 412a21245..c5d4eecba 100644 --- a/testing/tests/test_e2e/test_network.py +++ b/testing/tests/test_e2e/test_network.py @@ -1,10 +1,9 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations import pytest -from ops import RelationNotFoundError -from ops.charm import CharmBase -from ops.framework import Framework - from scenario import Context from scenario.state import ( Address, @@ -15,6 +14,10 @@ SubordinateRelation, ) +from ops import RelationNotFoundError +from ops.charm import CharmBase +from ops.framework import Framework + @pytest.fixture(scope='function') def mycharm(): @@ -93,7 +96,7 @@ def test_no_sub_binding(mycharm): ) as mgr: with pytest.raises(RelationNotFoundError): # sub relations have no network - mgr.charm.model.get_binding('bar').network + mgr.charm.model.get_binding('bar').network # noqa: B018 # Used to trigger the error. def test_no_relation_error(mycharm): @@ -123,7 +126,7 @@ def test_no_relation_error(mycharm): ), ) as mgr: with pytest.raises(RelationNotFoundError): - mgr.charm.model.get_binding('foo').network + mgr.charm.model.get_binding('foo').network # noqa: B018 # Used to trigger the error. def test_juju_info_network_default(mycharm): diff --git a/testing/tests/test_e2e/test_pebble.py b/testing/tests/test_e2e/test_pebble.py index ce82d3cab..b3bb52243 100644 --- a/testing/tests/test_e2e/test_pebble.py +++ b/testing/tests/test_e2e/test_pebble.py @@ -1,20 +1,23 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations import dataclasses import datetime import io -import tempfile from pathlib import Path import pytest +from scenario import Context +from scenario.state import CheckInfo, Container, Exec, Mount, Notice, State + from ops import PebbleCustomNoticeEvent, PebbleReadyEvent, pebble from ops.charm import CharmBase from ops.framework import 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 @@ -73,10 +76,10 @@ def callback(self: CharmBase): ) -def test_fs_push(charm_cls): +def test_fs_push(tmp_path, charm_cls): text = 'lorem ipsum/n alles amat gloriae foo' - file = tempfile.NamedTemporaryFile() - pth = Path(file.name) + + pth = tmp_path / 'textfile' pth.write_text(text) def callback(self: CharmBase): @@ -102,7 +105,7 @@ def callback(self: CharmBase): @pytest.mark.parametrize('make_dirs', (True, False)) -def test_fs_pull(charm_cls, make_dirs): +def test_fs_pull(tmp_path, charm_cls, make_dirs): text = 'lorem ipsum/n alles amat gloriae foo' def callback(self: CharmBase): @@ -120,11 +123,10 @@ def callback(self: CharmBase): with pytest.raises((FileNotFoundError, pebble.PathError)): container.pull('/foo/bar/baz.txt') - td = tempfile.TemporaryDirectory() container = Container( name='foo', can_connect=True, - mounts={'foo': Mount(location='/foo', source=td.name)}, + mounts={'foo': Mount(location='/foo', source=tmp_path)}, ) state = State(containers={container}) @@ -137,10 +139,8 @@ def callback(self: CharmBase): callback(mgr.charm) if make_dirs: - # file = (out.get_container("foo").mounts["foo"].source + "bar/baz.txt").open("/foo/bar/baz.txt") - # this is one way to retrieve the file - file = Path(td.name + '/bar/baz.txt') + file = tmp_path / 'bar' / 'baz.txt' # another is: assert file == Path(out.get_container('foo').mounts['foo'].source) / 'bar' / 'baz.txt' diff --git a/testing/tests/test_e2e/test_play_assertions.py b/testing/tests/test_e2e/test_play_assertions.py index d804ac962..206ff6b83 100644 --- a/testing/tests/test_e2e/test_play_assertions.py +++ b/testing/tests/test_e2e/test_play_assertions.py @@ -1,14 +1,18 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations import dataclasses import pytest +from scenario.state import BlockedStatus as ScenarioBlockedStatus +from scenario.state import Relation, State + from ops.charm import CharmBase from ops.framework import Framework from ops.model import ActiveStatus, BlockedStatus -from scenario.state import Relation, State -from scenario.state import BlockedStatus as ScenarioBlockedStatus from ..helpers import jsonpatch_delta, trigger diff --git a/testing/tests/test_e2e/test_ports.py b/testing/tests/test_e2e/test_ports.py index 9d122e778..9b37f5fb0 100644 --- a/testing/tests/test_e2e/test_ports.py +++ b/testing/tests/test_e2e/test_ports.py @@ -1,14 +1,20 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations -import pytest -from ops import CharmBase, Framework, StartEvent, StopEvent +from collections.abc import Mapping +from typing import Any +import pytest from scenario import Context, State from scenario.state import Port, StateValidationError, TCPPort, UDPPort +from ops import CharmBase, Framework, StartEvent, StopEvent + class MyCharm(CharmBase): - META = {'name': 'edgar'} + META: Mapping[str, Any] = {'name': 'edgar'} def __init__(self, framework: Framework): super().__init__(framework) @@ -31,7 +37,7 @@ def ctx(): def test_open_port(ctx): out = ctx.run(ctx.on.start(), State()) assert len(out.opened_ports) == 1 - port = tuple(out.opened_ports)[0] + port = next(iter(out.opened_ports)) assert port.protocol == 'tcp' assert port.port == 12 diff --git a/testing/tests/test_e2e/test_relations.py b/testing/tests/test_e2e/test_relations.py index 6cf20c511..bbf7db34f 100644 --- a/testing/tests/test_e2e/test_relations.py +++ b/testing/tests/test_e2e/test_relations.py @@ -1,34 +1,37 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations from collections.abc import Callable -import ops import pytest -from ops.charm import ( - CharmBase, - CharmEvents, - CollectStatusEvent, - RelationBrokenEvent, - RelationCreatedEvent, - RelationDepartedEvent, - RelationEvent, -) -from ops.framework import EventBase, Framework - import scenario from scenario import Context from scenario.errors import UncaughtCharmError from scenario.state import ( _DEFAULT_JUJU_DATABAG, - _Event, PeerRelation, Relation, RelationBase, State, StateValidationError, SubordinateRelation, + _Event, _next_relation_id, ) + +import ops +from ops.charm import ( + CharmBase, + CharmEvents, + CollectStatusEvent, + RelationBrokenEvent, + RelationCreatedEvent, + RelationDepartedEvent, + RelationEvent, +) +from ops.framework import EventBase, Framework from tests.helpers import trigger @@ -36,7 +39,7 @@ def mycharm(): class MyCharmEvents(CharmEvents): @classmethod - def define_event(cls, event_kind: str, event_type: 'type[EventBase]'): + def define_event(cls, event_kind: str, event_type: type[EventBase]): if getattr(cls, event_kind, None): delattr(cls, event_kind) return super().define_event(event_kind, event_type) diff --git a/testing/tests/test_e2e/test_resource.py b/testing/tests/test_e2e/test_resource.py index 2aeac5408..0860d0411 100644 --- a/testing/tests/test_e2e/test_resource.py +++ b/testing/tests/test_e2e/test_resource.py @@ -5,11 +5,11 @@ import pathlib -import ops import pytest - from scenario import Context, Resource, State +import ops + class ResourceCharm(ops.CharmBase): def __init__(self, framework): @@ -24,7 +24,7 @@ def test_get_resource(): 'resources': {'foo': {'type': 'file'}, 'bar': {'type': 'file'}}, }, ) - resource1 = Resource(name='foo', path=pathlib.Path('/tmp/foo')) + resource1 = Resource(name='foo', path=pathlib.Path('/var/charm/foo')) resource2 = Resource(name='bar', path=pathlib.Path('~/bar')) with ctx(ctx.on.update_status(), state=State(resources={resource1, resource2})) as mgr: assert mgr.charm.model.resources.fetch('foo') == resource1.path diff --git a/testing/tests/test_e2e/test_rubbish_events.py b/testing/tests/test_e2e/test_rubbish_events.py index 9240facd3..b94403e03 100644 --- a/testing/tests/test_e2e/test_rubbish_events.py +++ b/testing/tests/test_e2e/test_rubbish_events.py @@ -1,10 +1,16 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations +from typing import ClassVar + import pytest +from scenario.state import State, _CharmSpec, _Event + from ops.charm import CharmBase, CharmEvents from ops.framework import EventBase, EventSource, Framework, Object -from scenario.state import State, _CharmSpec, _Event from ..helpers import trigger @@ -29,7 +35,7 @@ class Sub(Object): class MyCharm(CharmBase): on = MyCharmEvents() - evts = [] + evts: ClassVar[list[EventBase]] = [] def __init__(self, framework: Framework): super().__init__(framework) diff --git a/testing/tests/test_e2e/test_secrets.py b/testing/tests/test_e2e/test_secrets.py index 6b07a6097..c0170d2b2 100644 --- a/testing/tests/test_e2e/test_secrets.py +++ b/testing/tests/test_e2e/test_secrets.py @@ -1,3 +1,6 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations import collections @@ -6,14 +9,13 @@ from unittest.mock import ANY import pytest +from scenario import Context +from scenario.state import Relation, Secret, State + from ops.charm import CharmBase from ops.framework import Framework -from ops.model import ModelError +from ops.model import ModelError, SecretNotFoundError, SecretRotate from ops.model import Secret as ops_Secret -from ops.model import SecretNotFoundError, SecretRotate - -from scenario import Context -from scenario.state import Relation, Secret, State from test.charms.test_secrets.src.charm import Result, SecretsCharm from tests.helpers import trigger diff --git a/testing/tests/test_e2e/test_state.py b/testing/tests/test_e2e/test_state.py index b8ffa1523..500098d02 100644 --- a/testing/tests/test_e2e/test_state.py +++ b/testing/tests/test_e2e/test_state.py @@ -1,23 +1,19 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations import copy import tempfile +from collections.abc import Callable, Generator, Iterable from dataclasses import asdict, replace from typing import Any -from collections.abc import Callable, Iterable, Generator - -import yaml -import ops import pytest -from ops.charm import CharmBase, CharmEvents, CollectStatusEvent -from ops.framework import EventBase, Framework -from ops.model import ActiveStatus, UnknownStatus, WaitingStatus - +import yaml +from scenario.context import Context from scenario.state import ( _DEFAULT_JUJU_DATABAG, - _Event, - _next_storage_index, Address, BindAddress, CheckInfo, @@ -37,9 +33,15 @@ StoredState, SubordinateRelation, TCPPort, + _Event, + _next_storage_index, layer_from_rockcraft, ) -from scenario.context import Context + +import ops +from ops.charm import CharmBase, CharmEvents, CollectStatusEvent +from ops.framework import EventBase, Framework +from ops.model import ActiveStatus, UnknownStatus, WaitingStatus from tests.helpers import jsonpatch_delta, sort_patch, trigger CUSTOM_EVT_SUFFIXES = { @@ -60,7 +62,7 @@ def mycharm(): class MyCharmEvents(CharmEvents): @classmethod - def define_event(cls, event_kind: str, event_type: 'type[EventBase]'): + def define_event(cls, event_kind: str, event_type: type[EventBase]): if getattr(cls, event_kind, None): delattr(cls, event_kind) return super().define_event(event_kind, event_type) @@ -216,9 +218,9 @@ def event_handler(charm: CharmBase, _): # this will NOT raise an exception because we're not in an event context! # we're right before the event context is entered in fact. - with pytest.raises(Exception): + with pytest.raises(ops.RelationDataAccessError): rel.data[rel.app]['a'] = 'b' - with pytest.raises(Exception): + with pytest.raises(ops.RelationDataAccessError): rel.data[charm.model.get_unit('remote/1')]['c'] = 'd' assert charm.unit.is_leader() @@ -504,14 +506,14 @@ def test_state_immutable(obj_in, attribute: str, get_method: str, key_attr: str, elif attribute == 'opened_ports': # There's no State.get_opened_ports, because in a charm tests you just # want to assert the port is/is not in the set. - obj_out = [p for p in state_out.opened_ports if p == obj_in][0] + obj_out = next(p for p in state_out.opened_ports if p == obj_in) elif attribute == 'secrets': # State.get_secret only takes keyword arguments, while the others take # only positional arguments. obj_out = state_out.get_secret(id=obj_in.id) elif attribute == 'resources': # Charms can't change resources, so there's no State.get_resource. - obj_out = [r for r in state_out.resources if r == obj_in][0] + obj_out = next(r for r in state_out.resources if r == obj_in) else: obj_out = getattr(state_out, get_method)(getattr(obj_in, key_attr)) assert obj_in is not obj_out @@ -830,7 +832,7 @@ class Charm(ops.CharmBase): assert state.get_relations('sub')[0].interface == 'below' assert isinstance(state.storages, frozenset) assert len(state.storages) == 1 - assert tuple(state.storages)[0].name == 'storage' + assert next(iter(state.storages)).name == 'storage' assert isinstance(state.stored_states, frozenset) assert len(state.stored_states) == 1 assert state.get_stored_state('_stored', owner_path='Charm').name == '_stored' @@ -881,7 +883,7 @@ class Charm(ops.CharmBase): assert state.get_relation(relation.id).remote_app_data == {'a': 'b'} assert isinstance(state.storages, frozenset) assert len(state.storages) == 1 - assert tuple(state.storages)[0].name == 'storage' + assert next(iter(state.storages)).name == 'storage' assert isinstance(state.stored_states, frozenset) assert len(state.stored_states) == 1 assert state.get_stored_state('_stored', owner_path='Charm').name == '_stored' diff --git a/testing/tests/test_e2e/test_status.py b/testing/tests/test_e2e/test_status.py index 872c8a4ad..4c45241b1 100644 --- a/testing/tests/test_e2e/test_status.py +++ b/testing/tests/test_e2e/test_status.py @@ -1,11 +1,11 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations -import ops import pytest -from ops.charm import CharmBase -from ops.framework import Framework - from scenario import Context +from scenario.errors import UncaughtCharmError from scenario.state import ( ActiveStatus, BlockedStatus, @@ -15,7 +15,11 @@ UnknownStatus, WaitingStatus, ) -from scenario.errors import UncaughtCharmError + +import ops +from ops.charm import CharmBase +from ops.framework import Framework + from ..helpers import trigger diff --git a/testing/tests/test_e2e/test_storage.py b/testing/tests/test_e2e/test_storage.py index 3ac6c2075..604be52f3 100644 --- a/testing/tests/test_e2e/test_storage.py +++ b/testing/tests/test_e2e/test_storage.py @@ -1,17 +1,26 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations -import pytest -from ops import CharmBase, ModelError +from collections.abc import Mapping +from typing import Any +import pytest from scenario import Context, State, Storage +from ops import CharmBase, ModelError + class MyCharmWithStorage(CharmBase): - META = {'name': 'charlene', 'storage': {'foo': {'type': 'filesystem'}}} + META: Mapping[str, Any] = { + 'name': 'charlene', + 'storage': {'foo': {'type': 'filesystem'}}, + } class MyCharmWithoutStorage(CharmBase): - META = {'name': 'patrick'} + META: Mapping[str, Any] = {'name': 'patrick'} @pytest.fixture diff --git a/testing/tests/test_e2e/test_stored_state.py b/testing/tests/test_e2e/test_stored_state.py index 39a36c3ce..55ae55a9d 100644 --- a/testing/tests/test_e2e/test_stored_state.py +++ b/testing/tests/test_e2e/test_stored_state.py @@ -1,22 +1,27 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations +from collections.abc import Mapping +from typing import Any, ClassVar + import pytest +from scenario.state import State, StoredState import ops -from ops.framework import StoredState as ops_storedstate - -from scenario.state import State, StoredState +from ops.framework import StoredState as OpsStoredstate from tests.helpers import trigger @pytest.fixture(scope='function') def mycharm(): class MyCharm(ops.CharmBase): - META = {'name': 'mycharm'} + META: Mapping[str, Any] = {'name': 'mycharm'} - _read = {} - _stored = ops_storedstate() - _stored2 = ops_storedstate() + _read: ClassVar[dict[str, Any]] = {} + _stored = OpsStoredstate() + _stored2 = OpsStoredstate() def __init__(self, framework: ops.Framework): super().__init__(framework) diff --git a/testing/tests/test_e2e/test_trace_data.py b/testing/tests/test_e2e/test_trace_data.py index a86cf865b..7a0f48550 100644 --- a/testing/tests/test_e2e/test_trace_data.py +++ b/testing/tests/test_e2e/test_trace_data.py @@ -1,9 +1,12 @@ -from __future__ import annotations +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. -import ops +from __future__ import annotations from scenario import Context, State +import ops + META = { 'name': 'traced_charm', 'requires': {'charm-tracing': {'interface': 'tracing', 'limit': 1}}, diff --git a/testing/tests/test_e2e/test_vroot.py b/testing/tests/test_e2e/test_vroot.py index dd299f8e8..e5ee12a96 100644 --- a/testing/tests/test_e2e/test_vroot.py +++ b/testing/tests/test_e2e/test_vroot.py @@ -1,20 +1,26 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations import tempfile +from collections.abc import Mapping from pathlib import Path +from typing import Any import pytest import yaml +from scenario import Context, State + from ops.charm import CharmBase from ops.framework import Framework from ops.model import ActiveStatus -from scenario import Context, State from ..helpers import trigger class MyCharm(CharmBase): - META = {'name': 'my-charm'} + META: Mapping[str, Any] = {'name': 'my-charm'} def __init__(self, framework: Framework): super().__init__(framework) diff --git a/testing/tests/test_emitted_events_util.py b/testing/tests/test_emitted_events_util.py index 6792823e8..86c2bfdbb 100644 --- a/testing/tests/test_emitted_events_util.py +++ b/testing/tests/test_emitted_events_util.py @@ -1,11 +1,17 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations -from ops.charm import CharmBase, CharmEvents, CollectStatusEvent, StartEvent -from ops.framework import CommitEvent, EventBase, EventSource, PreCommitEvent +from collections.abc import Mapping +from typing import Any from scenario import Context, State from scenario.state import _Event +from ops.charm import CharmBase, CharmEvents, CollectStatusEvent, StartEvent +from ops.framework import CommitEvent, EventBase, EventSource, PreCommitEvent + class Foo(EventBase): pass @@ -16,7 +22,7 @@ class MyCharmEvents(CharmEvents): class MyCharm(CharmBase): - META = {'name': 'mycharm'} + META: Mapping[str, Any] = {'name': 'mycharm'} on = MyCharmEvents() def __init__(self, *args, **kwargs): diff --git a/testing/tests/test_plugin.py b/testing/tests/test_plugin.py index 47d4419ea..3c7109d6d 100644 --- a/testing/tests/test_plugin.py +++ b/testing/tests/test_plugin.py @@ -1,3 +1,6 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations import sys diff --git a/testing/tests/test_runtime.py b/testing/tests/test_runtime.py index 3c90c5c12..835f56405 100644 --- a/testing/tests/test_runtime.py +++ b/testing/tests/test_runtime.py @@ -1,17 +1,19 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + from __future__ import annotations import os from tempfile import TemporaryDirectory import pytest +from scenario import ActiveStatus, Context +from scenario._runtime import Runtime, UncaughtCharmError +from scenario.state import Relation, State, _CharmSpec, _Event import ops from ops._main import _Abort -from scenario import Context, ActiveStatus -from scenario.state import Relation, State, _CharmSpec, _Event -from scenario._runtime import Runtime, UncaughtCharmError - def charm_type(): class _CharmEvents(ops.CharmEvents):