diff --git a/interface_tester/interface_test.py b/interface_tester/interface_test.py index daa008c..f0e2258 100644 --- a/interface_tester/interface_test.py +++ b/interface_tester/interface_test.py @@ -13,8 +13,9 @@ import pydantic from ops.testing import CharmType from pydantic import ValidationError -from scenario import Context, Event, Relation, State, state -from scenario.state import _EventPath +from scenario import Context, Relation, State +from scenario.context import CharmEvents +from scenario.state import _DEFAULT_JUJU_DATABAG, _Event, _EventPath from interface_tester.errors import InvalidTestCaseError, SchemaValidationError @@ -251,7 +252,7 @@ def ctx(self) -> Optional[_InterfaceTestContext]: """ return _TESTER_CTX - def run(self, event: Union[str, Event]) -> State: + def run(self, event: Union[str, _Event]) -> State: """Simulate the emission on an event in the initial state you passed to the initializer. Calling this method will run scenario and verify that the charm being tested can handle @@ -326,7 +327,7 @@ def assert_relation_data_empty(self): # remove the default unit databag keys or we'll get false positives. local_unit_data_keys = set(relation.local_unit_data).difference( - set(state.DEFAULT_JUJU_DATABAG.keys()) + set(_DEFAULT_JUJU_DATABAG.keys()) ) if local_unit_data_keys: @@ -365,7 +366,7 @@ def _detach(self): # release singleton Tester.__instance__ = None - def _run(self, event: Union[str, Event]): + def _run(self, event: Union[str, _Event]): logger.debug("running %s" % event) self._has_run = True @@ -379,22 +380,24 @@ def _run(self, event: Union[str, Event]): # some required config, a "happy" status, network information, OTHER relations. # Typically, should NOT touch the relation that this interface test is about # -> so we overwrite and warn on conflict: state_template is the baseline, - state = (self.ctx.state_template or State()).copy() + state = ( + dataclasses.replace(self.ctx.state_template) if self.ctx.state_template else State() + ) relations = self._generate_relations_state( state, input_state, self.ctx.supported_endpoints, self.ctx.role ) # State is frozen; replace - modified_state = state.replace(relations=relations) + modified_state = dataclasses.replace(state, relations=relations) # the Relation instance this test is about: relation = next(filter(lambda r: r.interface == self.ctx.interface_name, relations)) - evt: Event = self._cast_event(event, relation) + evt: _Event = self._cast_event(event, relation) logger.info("collected test for %s with %s" % (self.ctx.interface_name, evt.name)) return self._run_scenario(evt, modified_state) - def _run_scenario(self, event: Event, state: State): + def _run_scenario(self, event: Union[str, _Event], state: State): logger.debug("running scenario with state=%s, event=%s" % (state, event)) kwargs = {} @@ -410,20 +413,28 @@ def _run_scenario(self, event: Event, state: State): ) return ctx.run(event, state) - def _cast_event(self, raw_event: Union[str, Event], relation: Relation): - # test.EVENT might be a string or an Event. Cast to Event. - event = Event(raw_event) if isinstance(raw_event, str) else raw_event - - if not isinstance(event, Event): + def _cast_event(self, raw_event: Union[str, _Event], relation: Relation): + if not isinstance(raw_event, (_Event, str)): raise InvalidTestCaseError( - f"Expected Event or str, not {type(raw_event)}. " - f"Invalid test case: {self} cannot cast {raw_event} to Event." + f"Bad interface test specification: event {raw_event} should be a relation event " + f"string or _Event." ) - if not event._is_relation_event: - raise InvalidTestCaseError( - f"Bad interface test specification: event {raw_event} " "is not a relation event." - ) + if isinstance(raw_event, str): + if raw_event.endswith("-relation-changed"): + event = CharmEvents.relation_changed(relation) + elif raw_event.endswith("-relation-departed"): + event = CharmEvents.relation_departed(relation) + elif raw_event.endswith("-relation-broken"): + event = CharmEvents.relation_broken(relation) + elif raw_event.endswith("-relation-joined"): + event = CharmEvents.relation_joined(relation) + elif raw_event.endswith("-relation-created"): + event = CharmEvents.relation_created(relation) + else: + raise InvalidTestCaseError( + f"Bad interface test specification: event {raw_event} is not a relation event." + ) # todo: if the user passes a relation event that is NOT about the relation # interface that this test is about, at this point we are injecting the wrong @@ -434,8 +445,10 @@ def _cast_event(self, raw_event: Union[str, Event], relation: Relation): # next we need to ensure that the event's .relation is our relation, and that the endpoint # in the relation and the event path match that of the charm we're testing. - charm_event = event.replace( - relation=relation, path=relation.endpoint + typing.cast(_EventPath, event.path).suffix + charm_event = dataclasses.replace( + event, + relation=relation, + path=relation.endpoint + typing.cast(_EventPath, event.path).suffix, ) return charm_event @@ -495,7 +508,7 @@ def filter_relations(rels: List[Relation], op: Callable): # relations that come from the state_template presumably have the right endpoint, # but those that we get from interface tests cannot. relations_with_endpoint = [ - r.replace(endpoint=endpoint) for r in relations_from_input_state + dataclasses.replace(r, endpoint=endpoint) for r in relations_from_input_state ] relations.extend(relations_with_endpoint) diff --git a/interface_tester/plugin.py b/interface_tester/plugin.py index 26331cd..5dc1e85 100644 --- a/interface_tester/plugin.py +++ b/interface_tester/plugin.py @@ -7,7 +7,9 @@ from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Type from ops.testing import CharmType -from scenario.state import MetadataNotFoundError, State, _CharmSpec +from scenario import State +from scenario.errors import MetadataNotFoundError +from scenario.state import _CharmSpec from interface_tester.collector import InterfaceTestSpec, gather_test_spec_for_version from interface_tester.errors import ( diff --git a/pyproject.toml b/pyproject.toml index 4b43cd6..fb40f97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ build-backend = "setuptools.build_meta" [project] name = "pytest-interface-tester" -version = "3.1.0" +version = "3.1.1" authors = [ { name = "Pietro Pasotti", email = "pietro.pasotti@canonical.com" }, ] @@ -22,7 +22,7 @@ keywords = ["juju", "relation interfaces"] dependencies = [ "pydantic>= 1.10.7", "typer==0.7.0", - "ops-scenario>=5.2", + "ops-scenario>=7.0.1", "pytest" ] diff --git a/tests/unit/test_e2e.py b/tests/unit/test_e2e.py index e6f9f12..3653b5c 100644 --- a/tests/unit/test_e2e.py +++ b/tests/unit/test_e2e.py @@ -108,12 +108,12 @@ def test_error_if_skip_schema_before_run(): def test_data_on_changed(): t = Tester(State( - relations=[Relation( + relations={Relation( endpoint='foobadooble', # should not matter interface='tracing', remote_app_name='remote', local_app_data={} - )] + )} )) t.skip_schema_validation() """ @@ -134,12 +134,12 @@ def test_error_if_not_relation_event(): def test_data_on_changed(): t = Tester(State( - relations=[Relation( + relations={Relation( endpoint='foobadooble', # should not matter interface='tracing', remote_app_name='remote', local_app_data={} - )] + )} )) t.run("foobadooble-changed") t.skip_schema_validation() @@ -165,12 +165,12 @@ def test_error_if_assert_relation_data_empty_before_run(): def test_data_on_changed(): t = Tester(State( - relations=[Relation( + relations={Relation( endpoint='foobadooble', # should not matter interface='tracing', remote_app_name='remote', local_app_data={} - )] + )} )) t.assert_relation_data_empty() """ @@ -192,12 +192,12 @@ def test_error_if_assert_schema_valid_before_run(): def test_data_on_changed(): t = Tester(State( - relations=[Relation( + relations={Relation( endpoint='foobadooble', # should not matter interface='tracing', remote_app_name='remote', local_app_data={} - )] + )} )) t.assert_schema_valid() """ @@ -218,12 +218,12 @@ def test_error_if_assert_schema_without_schema(): def test_data_on_changed(): t = Tester(State( - relations=[Relation( + relations={Relation( endpoint='foobadooble', # should not matter interface='tracing', remote_app_name='remote', local_app_data={} - )] + )} )) state_out = t.run("axolotl-relation-changed") t.assert_schema_valid() @@ -245,12 +245,12 @@ def test_error_if_return_before_schema_call(): def test_data_on_changed(): t = Tester(State( - relations=[Relation( + relations={Relation( endpoint='foobadooble', # should not matter interface='tracing', remote_app_name='remote', local_app_data={} - )] + )} )) state_out = t.run("axolotl-relation-changed") """ @@ -271,12 +271,12 @@ def test_error_if_return_without_run(): def test_data_on_changed(): t = Tester(State( - relations=[Relation( + relations={Relation( endpoint='foobadooble', # should not matter interface='tracing', remote_app_name='remote', local_app_data={} - )] + )} )) """ @@ -321,12 +321,12 @@ def test_valid_run(endpoint, evt_type): def test_data_on_changed(): t = Tester(State( - relations=[Relation( + relations={{Relation( endpoint='{endpoint}', # should not matter interface='tracing', remote_app_name='remote', local_app_data={{}} - )] + )}} )) state_out = t.run("{endpoint}-relation-{evt_type}") t.assert_schema_valid(schema=DataBagSchema()) @@ -348,13 +348,13 @@ def test_valid_run_default_schema(): def test_data_on_changed(): t = Tester(State( - relations=[Relation( + relations={Relation( endpoint='foobadooble', # should not matter interface='tracing', remote_app_name='remote', local_app_data={"foo":"1"}, local_unit_data={"bar": "smackbeef"} - )] + )} )) state_out = t.run("axolotl-relation-changed") t.assert_schema_valid() @@ -391,13 +391,13 @@ def test_default_schema_validation_failure(): def test_data_on_changed(): t = Tester(State( - relations=[Relation( + relations={Relation( endpoint='foobadooble', # should not matter interface='tracing', remote_app_name='remote', local_app_data={"foo":"abc"}, local_unit_data={"bar": "smackbeef"} - )] + )} )) state_out = t.run("axolotl-relation-changed") t.assert_schema_valid() @@ -444,13 +444,13 @@ class FooBarSchema(DataBagSchema): def test_data_on_changed(): t = Tester(State( - relations=[Relation( + relations={Relation( endpoint='foobadooble', # should not matter interface='tracing', remote_app_name='remote', local_app_data={"foo":"1"}, local_unit_data={"bar": "smackbeef"} - )] + )} )) state_out = t.run("axolotl-relation-changed") t.assert_schema_valid(schema=FooBarSchema) @@ -481,13 +481,13 @@ class FooBarSchema(DataBagSchema): def test_data_on_changed(): t = Tester(State( - relations=[Relation( + relations={Relation( endpoint='foobadooble', # should not matter interface='tracing', remote_app_name='remote', local_app_data={"foo":"abc"}, local_unit_data={"bar": "smackbeef"} - )] + )} )) state_out = t.run("axolotl-relation-changed") t.assert_schema_valid(schema=FooBarSchema)