From e984c35e97c8930a39fb44f1e1f4fbbe58220486 Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Thu, 14 Nov 2024 16:52:56 +0100 Subject: [PATCH 1/8] multiple endpoint support --- interface_tester/interface_test.py | 8 +++- interface_tester/plugin.py | 8 ++++ .../tests/interface/conftest.py | 6 ++- tests/unit/test_e2e.py | 37 ++++++++++++++++++- 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/interface_tester/interface_test.py b/interface_tester/interface_test.py index e26f4b6..6baf96b 100644 --- a/interface_tester/interface_test.py +++ b/interface_tester/interface_test.py @@ -80,6 +80,9 @@ class _InterfaceTestContext: state_template: Optional[State] """Initial state that this test should be run with, according to the charm.""" + endpoint: str = None + """Endpoint being tested. Only required if there's multiple endpoints with the same interface.""" + """The role (provider|requirer) that this test is about.""" schema: Optional["DataBagSchema"] = None """Databag schema to validate the output relation with.""" @@ -481,7 +484,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: diff --git a/interface_tester/plugin.py b/interface_tester/plugin.py index 5dc1e85..0c475a8 100644 --- a/interface_tester/plugin.py +++ b/interface_tester/plugin.py @@ -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 @@ -68,6 +69,7 @@ def configure( meta: Optional[Dict[str, Any]] = None, actions: Optional[Dict[str, Any]] = None, config: Optional[Dict[str, Any]] = None, + endpoint: Optional[str] = None, ): """ @@ -82,6 +84,8 @@ def configure( :param meta: charm metadata.yaml contents. :param actions: charm actions.yaml contents. :param config: charm config.yaml contents. + :param endpoint: endpoint to test. In case there are multiple + endpoints with the same interface. :param juju_version: juju version that Scenario will simulate (also sets JUJU_VERSION envvar at charm runtime.) """ @@ -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: @@ -295,6 +301,7 @@ def __repr__(self): \tmeta={self._meta} \tactions={self._actions} \tconfig={self._config} + \tendpoint={self._endpoint} \tinterface_name={self._interface_name} \tinterface_version={self._interface_version} \tjuju_version={self._juju_version} @@ -324,6 +331,7 @@ def run(self) -> bool: supported_endpoints=self._gather_supported_endpoints(), test_fn=test_fn, juju_version=self._juju_version, + endpoint=self._endpoint, ) try: with tester_context(ctx): diff --git a/tests/resources/charm-like-path/tests/interface/conftest.py b/tests/resources/charm-like-path/tests/interface/conftest.py index 7026569..a2570a5 100644 --- a/tests/resources/charm-like-path/tests/interface/conftest.py +++ b/tests/resources/charm-like-path/tests/interface/conftest.py @@ -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), diff --git a/tests/unit/test_e2e.py b/tests/unit/test_e2e.py index aefe229..8e831ab 100644 --- a/tests/unit/test_e2e.py +++ b/tests/unit/test_e2e.py @@ -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), @@ -523,3 +527,34 @@ 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='{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()) + """ + ) + ) + + tester.run() \ No newline at end of file From 8f1baf30ac1264dd6acfc88e3bd02baf67763fc5 Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Thu, 14 Nov 2024 17:08:57 +0100 Subject: [PATCH 2/8] split out endpoints --- interface_tester/plugin.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/interface_tester/plugin.py b/interface_tester/plugin.py index 0c475a8..175d314 100644 --- a/interface_tester/plugin.py +++ b/interface_tester/plugin.py @@ -69,7 +69,6 @@ def configure( meta: Optional[Dict[str, Any]] = None, actions: Optional[Dict[str, Any]] = None, config: Optional[Dict[str, Any]] = None, - endpoint: Optional[str] = None, ): """ @@ -84,8 +83,6 @@ def configure( :param meta: charm metadata.yaml contents. :param actions: charm actions.yaml contents. :param config: charm config.yaml contents. - :param endpoint: endpoint to test. In case there are multiple - endpoints with the same interface. :param juju_version: juju version that Scenario will simulate (also sets JUJU_VERSION envvar at charm runtime.) """ @@ -99,8 +96,6 @@ 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: @@ -284,13 +279,14 @@ 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: + yield test, role, schema, endpoint def __repr__(self): return f""" 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, @@ -331,7 +326,7 @@ def run(self) -> bool: supported_endpoints=self._gather_supported_endpoints(), test_fn=test_fn, juju_version=self._juju_version, - endpoint=self._endpoint, + endpoint=endpoint, ) try: with tester_context(ctx): From bd020f6c8ccbcc82289f88867f4fbf6d679d7806 Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Thu, 14 Nov 2024 17:11:27 +0100 Subject: [PATCH 3/8] vbump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a942e8a..88d948b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ build-backend = "setuptools.build_meta" [project] name = "pytest-interface-tester" -version = "3.2.0" +version = "3.2.1" authors = [ { name = "Pietro Pasotti", email = "pietro.pasotti@canonical.com" }, ] From a06bf9b4ad137b14d8da8d0f1ad4871d770086e5 Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Thu, 14 Nov 2024 17:12:03 +0100 Subject: [PATCH 4/8] fmt --- tests/resources/charm-like-path/tests/interface/conftest.py | 2 +- tests/unit/test_e2e.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/resources/charm-like-path/tests/interface/conftest.py b/tests/resources/charm-like-path/tests/interface/conftest.py index a2570a5..11e83a0 100644 --- a/tests/resources/charm-like-path/tests/interface/conftest.py +++ b/tests/resources/charm-like-path/tests/interface/conftest.py @@ -33,7 +33,7 @@ def interface_tester(interface_tester: CRILikePathTester): "tracing": {"interface": "tracing"}, "mysql-1": {"interface": "mysql"}, "mysql-2": {"interface": "mysql"}, - }, + }, "requires": {"tracing": {"interface": "tracing"}}, }, state_template=State(leader=True), diff --git a/tests/unit/test_e2e.py b/tests/unit/test_e2e.py index 8e831ab..1d4dbdf 100644 --- a/tests/unit/test_e2e.py +++ b/tests/unit/test_e2e.py @@ -529,9 +529,7 @@ def test_data_on_changed(): tester.run() -@pytest.mark.parametrize( - "endpoint", ("mysql-1", "mysql-2") -) +@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( @@ -557,4 +555,4 @@ def test_data_on_changed(): ) ) - tester.run() \ No newline at end of file + tester.run() From a925242ad826a3ecf4c2f69f102b2d7701987c24 Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Thu, 14 Nov 2024 17:16:43 +0100 Subject: [PATCH 5/8] fmt --- interface_tester/interface_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/interface_tester/interface_test.py b/interface_tester/interface_test.py index 6baf96b..35568be 100644 --- a/interface_tester/interface_test.py +++ b/interface_tester/interface_test.py @@ -81,7 +81,10 @@ class _InterfaceTestContext: """Initial state that this test should be run with, according to the charm.""" endpoint: str = None - """Endpoint being tested. Only required if there's multiple endpoints with the same interface.""" + """ + Endpoint being tested. + Only required if there's multiple endpoints with the same interface. + """ """The role (provider|requirer) that this test is about.""" schema: Optional["DataBagSchema"] = None From 72e7fa764902c4609364799cedf3968043984f4f Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Fri, 15 Nov 2024 13:30:16 +0100 Subject: [PATCH 6/8] added endpoint param to constrain the run --- interface_tester/interface_test.py | 9 ++------- interface_tester/plugin.py | 15 ++++++++++++--- pyproject.toml | 2 +- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/interface_tester/interface_test.py b/interface_tester/interface_test.py index 35568be..0e4910c 100644 --- a/interface_tester/interface_test.py +++ b/interface_tester/interface_test.py @@ -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 @@ -80,12 +81,6 @@ class _InterfaceTestContext: state_template: Optional[State] """Initial state that this test should be run with, according to the charm.""" - endpoint: str = None - """ - Endpoint being tested. - Only required if there's multiple endpoints with the same interface. - """ - """The role (provider|requirer) that this test is about.""" schema: Optional["DataBagSchema"] = None """Databag schema to validate the output relation with.""" diff --git a/interface_tester/plugin.py b/interface_tester/plugin.py index 175d314..807c95b 100644 --- a/interface_tester/plugin.py +++ b/interface_tester/plugin.py @@ -63,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, @@ -73,6 +74,7 @@ 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. @@ -96,6 +98,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: @@ -286,6 +290,9 @@ def _yield_tests( schema = spec["schema"] for test in spec["tests"]: 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): @@ -317,6 +324,7 @@ def run(self) -> bool: 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, @@ -325,8 +333,7 @@ def run(self) -> bool: actions=self.actions, supported_endpoints=self._gather_supported_endpoints(), test_fn=test_fn, - juju_version=self._juju_version, - endpoint=endpoint, + juju_version=self._juju_version ) try: with tester_context(ctx): @@ -354,6 +361,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) diff --git a/pyproject.toml b/pyproject.toml index 88d948b..dee9f77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ build-backend = "setuptools.build_meta" [project] name = "pytest-interface-tester" -version = "3.2.1" +version = "3.3.0" authors = [ { name = "Pietro Pasotti", email = "pietro.pasotti@canonical.com" }, ] From 0beaae595ae5299dc32d6e6ea3df847b86693c10 Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Fri, 15 Nov 2024 14:53:31 +0100 Subject: [PATCH 7/8] fixed test --- interface_tester/plugin.py | 2 +- tests/unit/test_e2e.py | 26 +++++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/interface_tester/plugin.py b/interface_tester/plugin.py index 807c95b..637abde 100644 --- a/interface_tester/plugin.py +++ b/interface_tester/plugin.py @@ -333,7 +333,7 @@ def run(self) -> bool: actions=self.actions, supported_endpoints=self._gather_supported_endpoints(), test_fn=test_fn, - juju_version=self._juju_version + juju_version=self._juju_version, ) try: with tester_context(ctx): diff --git a/tests/unit/test_e2e.py b/tests/unit/test_e2e.py index 1d4dbdf..b77359a 100644 --- a/tests/unit/test_e2e.py +++ b/tests/unit/test_e2e.py @@ -61,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) @@ -72,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) @@ -88,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), @@ -543,8 +547,8 @@ def test_multiple_endpoints(endpoint, evt_type): def test_data_on_changed(): t = Tester(State( relations={{Relation( - endpoint='{endpoint}', # should not matter - interface='tracing', + endpoint='foobadoodle-doo', # should not matter + interface='mysql', remote_app_name='remote', local_app_data={{}} )}} @@ -552,7 +556,15 @@ def test_data_on_changed(): 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() From 0f8d5ab203611c3758e6689b4f4d0b9369c124b5 Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Fri, 15 Nov 2024 14:54:14 +0100 Subject: [PATCH 8/8] lint --- interface_tester/plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interface_tester/plugin.py b/interface_tester/plugin.py index 637abde..3cf0473 100644 --- a/interface_tester/plugin.py +++ b/interface_tester/plugin.py @@ -74,7 +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. + :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.