Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: adjust for the Scenario 7.0 API #21

Merged
merged 8 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading