Skip to content

Commit

Permalink
Merge pull request #28 from canonical/multiple-endpoint-support
Browse files Browse the repository at this point in the history
multiple endpoint support
  • Loading branch information
PietroPasotti authored Nov 19, 2024
2 parents 3ef010f + 0f8d5ab commit 87e4bbf
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 13 deletions.
8 changes: 6 additions & 2 deletions interface_tester/interface_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,11 @@ class _InterfaceTestContext:

interface_name: str
"""The name of the interface that this test is about."""
endpoint: str
"""Endpoint being tested."""
version: int
"""The version of the interface that this test is about."""
role: Role

charm_type: CharmType
"""Charm class being tested"""
supported_endpoints: dict
Expand Down Expand Up @@ -481,7 +482,10 @@ 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)

endpoint = self.ctx.endpoint or self._get_endpoint(
supported_endpoints, role, interface_name=interface_name
)

for rel in state_template.relations:
if rel.interface == interface_name:
Expand Down
21 changes: 17 additions & 4 deletions interface_tester/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def __init__(
self._meta = None
self._actions = None
self._config = None
self._endpoint = None
self._interface_name = None
self._interface_version = 0
self._juju_version = None
Expand All @@ -62,6 +63,7 @@ def configure(
branch: Optional[str] = None,
base_path: Optional[str] = None,
interface_name: Optional[str] = None,
endpoint: Optional[str] = None,
interface_version: Optional[int] = None,
state_template: Optional[State] = None,
juju_version: Optional[str] = None,
Expand All @@ -72,6 +74,8 @@ def configure(
"""
:arg interface_name: the interface to test.
:arg endpoint: the endpoint to test.
If omitted, will test all endpoints with this interface.
:param interface_version: what version of this interface we should be testing.
:arg state_template: template state to use with the scenario test.
The plugin will inject the relation spec under test, unless already defined.
Expand All @@ -95,6 +99,8 @@ def configure(
self._config = config
if repo:
self._repo = repo
if endpoint:
self._endpoint = endpoint
if interface_name:
self._interface_name = interface_name
if interface_version is not None:
Expand Down Expand Up @@ -278,13 +284,17 @@ def _yield_tests(
raise RuntimeError(f"this charm does not declare any endpoint using {interface_name}.")

role: RoleLiteral
for role in supported_endpoints:
for role, endpoints in supported_endpoints.items():
logger.debug(f"collecting scenes for {role}")

spec = tests[role]
schema = spec["schema"]
for test in spec["tests"]:
yield test, role, schema
for endpoint in endpoints:
if self._endpoint and endpoint != self._endpoint:
logger.debug(f"skipped compatible endpoint {endpoint}")
continue
yield test, role, schema, endpoint

def __repr__(self):
return f"""<Interface Tester:
Expand All @@ -310,11 +320,12 @@ def run(self) -> bool:
errors = []
ran_some = False

for test_fn, role, schema in self._yield_tests():
for test_fn, role, schema, endpoint in self._yield_tests():
ctx = _InterfaceTestContext(
role=role,
schema=schema,
interface_name=self._interface_name,
endpoint=endpoint,
version=self._interface_version,
charm_type=self._charm_type,
state_template=self._state_template,
Expand Down Expand Up @@ -351,6 +362,8 @@ def run(self) -> bool:
)

if not ran_some:
msg = f"no tests gathered for {self._interface_name}/v{self._interface_version}"
msg = f"no tests gathered for {self._interface_name!r}/v{self._interface_version}"
if self._endpoint:
msg += f" and endpoint {self._endpoint!r}"
logger.warning(msg)
raise NoTestsRun(msg)
2 changes: 1 addition & 1 deletion 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.2.0"
version = "3.3.0"
authors = [
{ name = "Pietro Pasotti", email = "[email protected]" },
]
Expand Down
6 changes: 5 additions & 1 deletion tests/resources/charm-like-path/tests/interface/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ def interface_tester(interface_tester: CRILikePathTester):
charm_type=DummiCharm,
meta={
"name": "dummi",
"provides": {"tracing": {"interface": "tracing"}},
"provides": {
"tracing": {"interface": "tracing"},
"mysql-1": {"interface": "mysql"},
"mysql-2": {"interface": "mysql"},
},
"requires": {"tracing": {"interface": "tracing"}},
},
state_template=State(leader=True),
Expand Down
55 changes: 50 additions & 5 deletions tests/unit/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ def interface_tester():
meta={
"name": "dummi",
# interface tests should be agnostic to endpoint names
"provides": {"dead": {"interface": "tracing"}},
"provides": {
"dead": {"interface": "tracing"},
"mysql-1": {"interface": "mysql"},
"mysql-2": {"interface": "mysql"},
},
"requires": {"beef-req": {"interface": "tracing"}},
},
state_template=State(leader=True),
Expand All @@ -57,7 +61,7 @@ def test_local_run(interface_tester):
interface_tester.run()


def _setup_with_test_file(test_file: str, schema_file: str = None):
def _setup_with_test_file(test_file: str, schema_file: str = None, interface: str = "tracing"):
td = tempfile.TemporaryDirectory()
temppath = Path(td.name)

Expand All @@ -68,7 +72,7 @@ def _collect_interface_test_specs(self):
pth = temppath / "interfaces" / self._interface_name / f"v{self._interface_version}"

test_dir = pth / "interface_tests"
test_dir.mkdir(parents=True)
test_dir.mkdir(parents=True, exist_ok=True)
test_provider = test_dir / "test_provider.py"
test_provider.write_text(test_file)

Expand All @@ -84,12 +88,16 @@ def _collect_interface_test_specs(self):

interface_tester = TempDirTester()
interface_tester.configure(
interface_name="tracing",
interface_name=interface,
charm_type=DummiCharm,
meta={
"name": "dummi",
# interface tests should be agnostic to endpoint names
"provides": {"dead": {"interface": "tracing"}},
"provides": {
"dead": {"interface": "tracing"},
"mysql-1": {"interface": "mysql"},
"mysql-2": {"interface": "mysql"},
},
"requires": {"beef-req": {"interface": "tracing"}},
},
state_template=State(leader=True),
Expand Down Expand Up @@ -523,3 +531,40 @@ def test_data_on_changed():
)
with pytest.raises(SchemaValidationError):
tester.run()


@pytest.mark.parametrize("endpoint", ("mysql-1", "mysql-2"))
@pytest.mark.parametrize("evt_type", ("changed", "created", "joined", "departed", "broken"))
def test_multiple_endpoints(endpoint, evt_type):
tester = _setup_with_test_file(
dedent(
f"""
from scenario import State, Relation
from interface_tester.interface_test import Tester
from interface_tester.schema_base import DataBagSchema
def test_data_on_changed():
t = Tester(State(
relations={{Relation(
endpoint='foobadoodle-doo', # should not matter
interface='mysql',
remote_app_name='remote',
local_app_data={{}}
)}}
))
state_out = t.run("{endpoint}-relation-{evt_type}")
t.assert_schema_valid(schema=DataBagSchema())
"""
),
interface="mysql",
)

tests = tuple(tester._yield_tests())
# dummicharm is a provider of two mysql-interface endpoints called mysql-1 and mysql-2,
# so we have two tests
assert len(tests) == 2
assert set(t[1] for t in tests) == {"provider"}
assert [t[3] for t in tests] == ["mysql-1", "mysql-2"]

tester.run()

0 comments on commit 87e4bbf

Please sign in to comment.