Skip to content

Commit

Permalink
Merge pull request #21 from tonyandrewmeyer/scenario-7
Browse files Browse the repository at this point in the history
chore: adjust for the Scenario 7.0 API
  • Loading branch information
PietroPasotti authored Sep 18, 2024
2 parents 983fc62 + 6a22325 commit 443060f
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 50 deletions.
59 changes: 36 additions & 23 deletions interface_tester/interface_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand All @@ -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 = {}
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion interface_tester/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "[email protected]" },
]
Expand All @@ -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"
]

Expand Down
48 changes: 24 additions & 24 deletions tests/unit/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
"""
Expand All @@ -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()
Expand All @@ -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()
"""
Expand All @@ -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()
"""
Expand All @@ -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()
Expand All @@ -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")
"""
Expand All @@ -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={}
)]
)}
))
"""
Expand Down Expand Up @@ -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())
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 443060f

Please sign in to comment.