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

fixed endpoint determination from input_state #15

Merged
merged 16 commits into from
Feb 9, 2024
7 changes: 7 additions & 0 deletions interface_tester/collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ def load_schema_module(schema_path: Path) -> types.ModuleType:
if module_name in sys.modules:
del sys.modules[module_name]

if pydantic.version.VERSION.split(".") <= ["2"]:
# in pydantic v1 it's necessary; in v2 it isn't.

# Otherwise we'll get an error when we re-run @validator
logger.debug("Clearing pydantic.class_validators._FUNCS")
pydantic.class_validators._FUNCS.clear() # noqa

try:
module = importlib.import_module(module_name)
except ImportError:
Expand Down
62 changes: 40 additions & 22 deletions interface_tester/interface_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def assert_schema_valid(self, schema: Optional["DataBagSchema"] = None):
}
)
except ValidationError as e:
errors.append(e.errors()[0])
errors.append(str(e))
if errors:
raise SchemaValidationError(errors)

Expand Down Expand Up @@ -426,6 +426,20 @@ def _coerce_event(self, raw_event: Union[str, Event], relation: Relation) -> Eve
f"Invalid test case: {self} cannot cast {raw_event} to Event."
)

@staticmethod
def _get_endpoint(supported_endpoints: dict, role: Role, interface_name: str):
endpoints_for_interface = supported_endpoints[role]

if len(endpoints_for_interface) < 1:
raise ValueError(f"no endpoint found for {role}/{interface_name}.")
elif len(endpoints_for_interface) > 1:
raise ValueError(
f"Multiple endpoints found for {role}/{interface_name}: "
f"{endpoints_for_interface}: cannot guess which one it is "
f"we're supposed to be testing"
)
return endpoints_for_interface[0]

def _generate_relations_state(
self, state_template: State, input_state: State, supported_endpoints, role: Role
) -> List[Relation]:
Expand All @@ -437,6 +451,9 @@ def _generate_relations_state(
"""
interface_name = self.ctx.interface_name

# determine what charm endpoint we're testing.
endpoint = self._get_endpoint(supported_endpoints, role, interface_name=interface_name)

for rel in state_template.relations:
if rel.interface == interface_name:
logger.warning(
Expand All @@ -448,18 +465,31 @@ def _generate_relations_state(
def filter_relations(rels: List[Relation], op: Callable):
return [r for r in rels if op(r.interface, interface_name)]

# the baseline is: all relations whose interface IS NOT the interface we're testing.
# the baseline is: all relations provided by the charm in the state_template,
# whose interface IS NOT the interface we're testing. We assume the test (input_state) is
# the ultimate owner of the state when it comes to the interface we're testing.
# We don't allow the charm to mess with it.
relations = filter_relations(state_template.relations, op=operator.ne)

if input_state:
# if the charm we're testing specified some relations in its input state, we add those
# whose interface IS the same as the one we're testing. If other relation interfaces
# were specified, they will be ignored.
relations.extend(filter_relations(input_state.relations, op=operator.eq))

if ignored := filter_relations(input_state.relations, op=operator.eq):
# if the interface test we're running specified some relations in its input_state,
# we add those whose interface IS the same as the one we're testing.
# If other relation interfaces were specified (for whatever reason?),
# they will be ignored.
relations_from_input_state = filter_relations(input_state.relations, op=operator.eq)

# 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
]

relations.extend(relations_with_endpoint)

if ignored := filter_relations(input_state.relations, op=operator.ne):
# this is a sign of a bad test.
logger.warning(
"irrelevant relations specified in input state for %s/%s."
"irrelevant relations specified in input_state for %s/%s."
"These will be ignored. details: %s" % (interface_name, role, ignored)
)

Expand All @@ -468,19 +498,6 @@ def filter_relations(rels: List[Relation], op: Callable):
if not filter_relations(relations, op=operator.eq):
# if neither the charm nor the interface specified any custom relation spec for
# the interface we're testing, we will provide one.
endpoints_for_interface = supported_endpoints[role]

if len(endpoints_for_interface) < 1:
raise ValueError(f"no endpoint found for {role}/{interface_name}.")
elif len(endpoints_for_interface) > 1:
raise ValueError(
f"Multiple endpoints found for {role}/{interface_name}: "
f"{endpoints_for_interface}: cannot guess which one it is "
f"we're supposed to be testing"
)
else:
endpoint = endpoints_for_interface[0]

relations.append(
Relation(
interface=interface_name,
Expand All @@ -491,4 +508,5 @@ def filter_relations(rels: List[Relation], op: Callable):
"%s: merged %s and %s --> relations=%s"
% (self, input_state, state_template, relations)
)

return relations
3 changes: 3 additions & 0 deletions interface_tester/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,11 @@ def run(self) -> bool:
with tester_context(ctx):
test_fn()
except Exception as e:
logger.error(f"Interface tester plugin failed with {e}", exc_info=True)
PietroPasotti marked this conversation as resolved.
Show resolved Hide resolved

if self._RAISE_IMMEDIATELY:
raise e

errors.append((ctx, e))
ran_some = True

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 = "2.0.0"
version = "2.0.1"
authors = [
{ name = "Pietro Pasotti", email = "[email protected]" },
]
Expand All @@ -20,7 +20,7 @@ license.text = "Apache-2.0"
keywords = ["juju", "relation interfaces"]

dependencies = [
"pydantic>=2",
"pydantic>= 1.10.7",
"typer==0.7.0",
"ops-scenario>=5.2",
"pytest"
Expand Down
22 changes: 11 additions & 11 deletions tests/unit/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def test_error_if_skip_schema_before_run():
def test_data_on_changed():
t = Tester(State(
relations=[Relation(
endpoint='tracing',
endpoint='foobadooble', # should not matter
interface='tracing',
remote_app_name='remote',
local_app_data={}
Expand All @@ -133,7 +133,7 @@ def test_error_if_assert_relation_data_empty_before_run():
def test_data_on_changed():
t = Tester(State(
relations=[Relation(
endpoint='tracing',
endpoint='foobadooble', # should not matter
interface='tracing',
remote_app_name='remote',
local_app_data={}
Expand All @@ -160,7 +160,7 @@ def test_error_if_assert_schema_valid_before_run():
def test_data_on_changed():
t = Tester(State(
relations=[Relation(
endpoint='tracing',
endpoint='foobadooble', # should not matter
interface='tracing',
remote_app_name='remote',
local_app_data={}
Expand All @@ -186,7 +186,7 @@ def test_error_if_assert_schema_without_schema():
def test_data_on_changed():
t = Tester(State(
relations=[Relation(
endpoint='tracing',
endpoint='foobadooble', # should not matter
interface='tracing',
remote_app_name='remote',
local_app_data={}
Expand All @@ -213,7 +213,7 @@ def test_error_if_return_before_schema_call():
def test_data_on_changed():
t = Tester(State(
relations=[Relation(
endpoint='tracing',
endpoint='foobadooble', # should not matter
interface='tracing',
remote_app_name='remote',
local_app_data={}
Expand All @@ -239,7 +239,7 @@ def test_error_if_return_without_run():
def test_data_on_changed():
t = Tester(State(
relations=[Relation(
endpoint='tracing',
endpoint='foobadooble', # should not matter
interface='tracing',
remote_app_name='remote',
local_app_data={}
Expand Down Expand Up @@ -285,7 +285,7 @@ def test_valid_run():
def test_data_on_changed():
t = Tester(State(
relations=[Relation(
endpoint='tracing',
endpoint='foobadooble', # should not matter
interface='tracing',
remote_app_name='remote',
local_app_data={}
Expand All @@ -312,7 +312,7 @@ def test_valid_run_default_schema():
def test_data_on_changed():
t = Tester(State(
relations=[Relation(
endpoint='tracing',
endpoint='foobadooble', # should not matter
interface='tracing',
remote_app_name='remote',
local_app_data={"foo":"1"},
Expand Down Expand Up @@ -355,7 +355,7 @@ def test_default_schema_validation_failure():
def test_data_on_changed():
t = Tester(State(
relations=[Relation(
endpoint='tracing',
endpoint='foobadooble', # should not matter
interface='tracing',
remote_app_name='remote',
local_app_data={"foo":"abc"},
Expand Down Expand Up @@ -408,7 +408,7 @@ class FooBarSchema(DataBagSchema):
def test_data_on_changed():
t = Tester(State(
relations=[Relation(
endpoint='tracing',
endpoint='foobadooble', # should not matter
interface='tracing',
remote_app_name='remote',
local_app_data={"foo":"1"},
Expand Down Expand Up @@ -445,7 +445,7 @@ class FooBarSchema(DataBagSchema):
def test_data_on_changed():
t = Tester(State(
relations=[Relation(
endpoint='tracing',
endpoint='foobadooble', # should not matter
interface='tracing',
remote_app_name='remote',
local_app_data={"foo":"abc"},
Expand Down
Loading