From 33e396f33891cdb73320b4df835505a06386d9fc Mon Sep 17 00:00:00 2001 From: Guillaume Belanger Date: Wed, 28 Aug 2024 14:09:07 -0400 Subject: [PATCH] chore: replace harness with scenario (n3 and n4 interfaces) (#348) Signed-off-by: guillaume --- requirements.in | 2 +- requirements.txt | 54 ++--- src/charm.py | 7 +- src/k8s_service.py | 2 + test-requirements.in | 1 + test-requirements.txt | 196 +++++++++++++----- .../test_provider_charm/metadata.yaml | 21 -- .../test_provider_charm/src/charm.py | 47 ----- .../test_requirer_charm/metadata.yaml | 21 -- .../test_requirer_charm/src/charm.py | 30 --- .../sdcore_upf/v0/test_fiveg_n3_provider.py | 142 ++++++++----- .../sdcore_upf/v0/test_fiveg_n3_requirer.py | 86 ++++---- .../sdcore_upf/v0/test_fiveg_n4_provider.py | 133 +++++++----- .../sdcore_upf/v0/test_fiveg_n4_requirer.py | 89 ++++---- tests/unit/test_charm.py | 15 +- tests/unit/test_dpdk.py | 3 +- 16 files changed, 450 insertions(+), 399 deletions(-) delete mode 100644 tests/unit/lib/charms/sdcore_upf/v0/test_charms/test_provider_charm/metadata.yaml delete mode 100644 tests/unit/lib/charms/sdcore_upf/v0/test_charms/test_provider_charm/src/charm.py delete mode 100644 tests/unit/lib/charms/sdcore_upf/v0/test_charms/test_requirer_charm/metadata.yaml delete mode 100644 tests/unit/lib/charms/sdcore_upf/v0/test_charms/test_requirer_charm/src/charm.py diff --git a/requirements.in b/requirements.in index 88c1c34..478da82 100644 --- a/requirements.in +++ b/requirements.in @@ -2,7 +2,7 @@ httpx ipaddress ops jinja2 -juju==3.5.2.0 +juju lightkube lightkube-models macaddress diff --git a/requirements.txt b/requirements.txt index 6dfe7ca..6412e43 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,16 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile requirements.in # -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.3.0 +anyio==4.4.0 # via httpx -bcrypt==4.1.2 +bcrypt==4.2.0 # via paramiko -cachetools==5.3.2 +cachetools==5.5.0 # via google-auth certifi==2024.7.4 # via @@ -18,7 +18,7 @@ certifi==2024.7.4 # httpx # kubernetes # requests -cffi==1.16.0 +cffi==1.17.0 # via # cryptography # pynacl @@ -26,23 +26,23 @@ charset-normalizer==3.3.2 # via requests click==8.1.7 # via typer -cosl==0.0.23 +cosl==0.0.24 # via -r requirements.in -cryptography==42.0.5 +cryptography==43.0.0 # via paramiko -google-auth==2.28.1 +google-auth==2.34.0 # via kubernetes h11==0.14.0 # via httpcore -httpcore==1.0.4 +httpcore==1.0.5 # via httpx -httpx==0.27.0 +httpx==0.27.2 # via # -r requirements.in # lightkube -hvac==2.1.0 +hvac==2.3.0 # via juju -idna==3.7 +idna==3.8 # via # anyio # httpx @@ -55,7 +55,7 @@ jinja2==3.1.4 # via -r requirements.in juju==3.5.2.0 # via -r requirements.in -kubernetes==29.0.0 +kubernetes==30.1.0 # via juju lightkube==0.15.4 # via -r requirements.in @@ -80,26 +80,26 @@ ops==2.15.0 # -r requirements.in # cosl # ops-scenario -ops-scenario==6.0.1 +ops-scenario==6.1.6 # via pytest-interface-tester -packaging==23.2 +packaging==24.1 # via # juju # pytest -paramiko==3.4.0 +paramiko==3.4.1 # via juju pluggy==1.5.0 # via pytest -protobuf==4.25.3 +protobuf==5.27.4 # via macaroonbakery -pyasn1==0.5.1 +pyasn1==0.6.0 # via # juju # pyasn1-modules # rsa -pyasn1-modules==0.3.0 +pyasn1-modules==0.4.0 # via google-auth -pycparser==2.21 +pycparser==2.22 # via cffi pydantic==2.8.2 # via @@ -126,7 +126,7 @@ pytest==8.3.2 # via pytest-interface-tester pytest-interface-tester==3.1.0 # via -r requirements.in -python-dateutil==2.8.2 +python-dateutil==2.9.0.post0 # via kubernetes pytz==2024.1 # via pyrfc3339 @@ -139,13 +139,13 @@ pyyaml==6.0.2 # lightkube # ops # ops-scenario -requests==2.32.0 +requests==2.32.3 # via # hvac # kubernetes # macaroonbakery # requests-oauthlib -requests-oauthlib==1.3.1 +requests-oauthlib==2.0.0 # via kubernetes rsa==4.9 # via google-auth @@ -165,7 +165,7 @@ toposort==1.10 # via juju typer==0.7.0 # via pytest-interface-tester -typing-extensions==4.10.0 +typing-extensions==4.12.2 # via # cosl # pydantic @@ -177,9 +177,9 @@ urllib3==2.2.2 # via # kubernetes # requests -websocket-client==1.7.0 +websocket-client==1.8.0 # via # kubernetes # ops -websockets==12.0 +websockets==13.0 # via juju diff --git a/src/charm.py b/src/charm.py index b425448..4e36740 100755 --- a/src/charm.py +++ b/src/charm.py @@ -11,7 +11,6 @@ from subprocess import check_output from typing import Any, Dict, List, Optional, Tuple, Union -from charm_config import CharmConfig, CharmConfigInvalidError, CNIType, UpfMode from charms.kubernetes_charm_libraries.v0.hugepages_volumes_patch import ( HugePagesVolume, KubernetesHugePagesPatchCharmLib, @@ -27,9 +26,7 @@ ) from charms.sdcore_upf_k8s.v0.fiveg_n3 import N3Provides from charms.sdcore_upf_k8s.v0.fiveg_n4 import N4Provides -from dpdk import DPDK from jinja2 import Environment, FileSystemLoader -from k8s_service import K8sService from lightkube.core.client import Client from lightkube.models.meta_v1 import ObjectMeta from lightkube.resources.core_v1 import Node, Pod @@ -39,6 +36,10 @@ from ops.main import main from ops.pebble import ChangeError, ConnectionError, ExecError, Layer, PathError +from charm_config import CharmConfig, CharmConfigInvalidError, CNIType, UpfMode +from dpdk import DPDK +from k8s_service import K8sService + logger = logging.getLogger(__name__) BESSD_CONTAINER_CONFIG_PATH = "/etc/bess/conf" diff --git a/src/k8s_service.py b/src/k8s_service.py index 01adb7c..b630eb0 100644 --- a/src/k8s_service.py +++ b/src/k8s_service.py @@ -3,6 +3,7 @@ # See LICENSE file for licensing details. """A class to manage the external UPF service.""" + import logging from typing import Optional @@ -17,6 +18,7 @@ class K8sService: """A class to manage the external UPF service.""" + def __init__(self, namespace: str, service_name: str, app_name: str, pfcp_port: int): self.namespace = namespace self.service_name = service_name diff --git a/test-requirements.in b/test-requirements.in index ab69765..a48738c 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -9,3 +9,4 @@ pytest-interface-tester pydantic==2.8.2 pytest-operator ruff +ops-scenario diff --git a/test-requirements.txt b/test-requirements.txt index e5061ae..9ba6d36 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,46 +4,68 @@ # # pip-compile --constraint=requirements.txt test-requirements.in # -annotated-types==0.6.0 - # via pydantic +annotated-types==0.7.0 + # via + # -c requirements.txt + # pydantic asttokens==2.4.1 # via stack-data -bcrypt==4.1.2 - # via paramiko -cachetools==5.3.2 - # via google-auth +bcrypt==4.2.0 + # via + # -c requirements.txt + # paramiko +cachetools==5.5.0 + # via + # -c requirements.txt + # google-auth certifi==2024.7.4 # via + # -c requirements.txt # kubernetes # requests -cffi==1.16.0 +cffi==1.17.0 # via + # -c requirements.txt # cryptography # pynacl charset-normalizer==3.3.2 - # via requests + # via + # -c requirements.txt + # requests click==8.1.7 - # via typer + # via + # -c requirements.txt + # typer codespell==2.3.0 # via -r test-requirements.in coverage[toml]==7.6.1 # via -r test-requirements.in -cryptography==42.0.5 - # via paramiko +cryptography==43.0.0 + # via + # -c requirements.txt + # paramiko decorator==5.1.1 # via # ipdb # ipython executing==2.0.1 # via stack-data -google-auth==2.28.1 - # via kubernetes -hvac==2.1.0 - # via juju -idna==3.7 - # via requests +google-auth==2.34.0 + # via + # -c requirements.txt + # kubernetes +hvac==2.3.0 + # via + # -c requirements.txt + # juju +idna==3.8 + # via + # -c requirements.txt + # requests iniconfig==2.0.0 - # via pytest + # via + # -c requirements.txt + # pytest ipdb==0.13.13 # via pytest-operator ipython==8.26.0 @@ -51,85 +73,122 @@ ipython==8.26.0 jedi==0.19.1 # via ipython jinja2==3.1.4 - # via pytest-operator + # via + # -c requirements.txt + # pytest-operator juju==3.5.2.0 # via + # -c requirements.txt # -r test-requirements.in # pytest-operator -kubernetes==29.0.0 - # via juju +kubernetes==30.1.0 + # via + # -c requirements.txt + # juju macaroonbakery==1.3.4 - # via juju + # via + # -c requirements.txt + # juju markupsafe==2.1.5 - # via jinja2 + # via + # -c requirements.txt + # jinja2 matplotlib-inline==0.1.7 # via ipython mypy-extensions==1.0.0 - # via typing-inspect + # via + # -c requirements.txt + # typing-inspect nodeenv==1.9.1 # via pyright oauthlib==3.2.2 # via + # -c requirements.txt # kubernetes # requests-oauthlib ops==2.15.0 - # via ops-scenario -ops-scenario==6.0.1 - # via pytest-interface-tester -packaging==23.2 # via + # -c requirements.txt + # ops-scenario +ops-scenario==6.1.6 + # via + # -c requirements.txt + # -r test-requirements.in + # pytest-interface-tester +packaging==24.1 + # via + # -c requirements.txt # juju # pytest parameterized==0.9.0 # via -r test-requirements.in -paramiko==3.4.0 - # via juju +paramiko==3.4.1 + # via + # -c requirements.txt + # juju parso==0.8.4 # via jedi pexpect==4.9.0 # via ipython pluggy==1.5.0 - # via pytest + # via + # -c requirements.txt + # pytest prompt-toolkit==3.0.47 # via ipython -protobuf==4.25.3 - # via macaroonbakery +protobuf==5.27.4 + # via + # -c requirements.txt + # macaroonbakery ptyprocess==0.7.0 # via pexpect pure-eval==0.2.3 # via stack-data -pyasn1==0.5.1 +pyasn1==0.6.0 # via + # -c requirements.txt # juju # pyasn1-modules # rsa -pyasn1-modules==0.3.0 - # via google-auth -pycparser==2.21 - # via cffi +pyasn1-modules==0.4.0 + # via + # -c requirements.txt + # google-auth +pycparser==2.22 + # via + # -c requirements.txt + # cffi pydantic==2.8.2 # via + # -c requirements.txt # -r test-requirements.in # pytest-interface-tester pydantic-core==2.20.1 - # via pydantic + # via + # -c requirements.txt + # pydantic pygments==2.18.0 # via ipython pymacaroons==0.13.0 - # via macaroonbakery + # via + # -c requirements.txt + # macaroonbakery pynacl==1.5.0 # via + # -c requirements.txt # macaroonbakery # paramiko # pymacaroons pyrfc3339==1.1 # via + # -c requirements.txt # juju # macaroonbakery pyright==1.1.377 # via -r test-requirements.in pytest==8.3.2 # via + # -c requirements.txt # -r test-requirements.in # pytest-asyncio # pytest-interface-tester @@ -139,34 +198,47 @@ pytest-asyncio==0.21.2 # -r test-requirements.in # pytest-operator pytest-interface-tester==3.1.0 - # via -r test-requirements.in + # via + # -c requirements.txt + # -r test-requirements.in pytest-operator==0.36.0 # via -r test-requirements.in -python-dateutil==2.8.2 - # via kubernetes +python-dateutil==2.9.0.post0 + # via + # -c requirements.txt + # kubernetes pytz==2024.1 - # via pyrfc3339 + # via + # -c requirements.txt + # pyrfc3339 pyyaml==6.0.2 # via + # -c requirements.txt # juju # kubernetes # ops # ops-scenario # pytest-operator -requests==2.32.0 +requests==2.32.3 # via + # -c requirements.txt # hvac # kubernetes # macaroonbakery # requests-oauthlib -requests-oauthlib==1.3.1 - # via kubernetes +requests-oauthlib==2.0.0 + # via + # -c requirements.txt + # kubernetes rsa==4.9 - # via google-auth -ruff==0.5.7 + # via + # -c requirements.txt + # google-auth +ruff==0.6.2 # via -r test-requirements.in six==1.16.0 # via + # -c requirements.txt # asttokens # kubernetes # macaroonbakery @@ -175,30 +247,40 @@ six==1.16.0 stack-data==0.6.3 # via ipython toposort==1.10 - # via juju + # via + # -c requirements.txt + # juju traitlets==5.14.3 # via # ipython # matplotlib-inline typer==0.7.0 - # via pytest-interface-tester -typing-extensions==4.10.0 # via - # ipython + # -c requirements.txt + # pytest-interface-tester +typing-extensions==4.12.2 + # via + # -c requirements.txt # pydantic # pydantic-core # typing-inspect typing-inspect==0.9.0 - # via juju + # via + # -c requirements.txt + # juju urllib3==2.2.2 # via + # -c requirements.txt # kubernetes # requests wcwidth==0.2.13 # via prompt-toolkit -websocket-client==1.7.0 +websocket-client==1.8.0 # via + # -c requirements.txt # kubernetes # ops -websockets==12.0 - # via juju +websockets==13.0 + # via + # -c requirements.txt + # juju diff --git a/tests/unit/lib/charms/sdcore_upf/v0/test_charms/test_provider_charm/metadata.yaml b/tests/unit/lib/charms/sdcore_upf/v0/test_charms/test_provider_charm/metadata.yaml deleted file mode 100644 index e804eeb..0000000 --- a/tests/unit/lib/charms/sdcore_upf/v0/test_charms/test_provider_charm/metadata.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -name: whatever-charm -description: whatever-charm -summary: whatever-charm -containers: - whatever-container: - resource: whatever-image - -resources: - whatever-image: - type: oci-image - description: whatever image - -provides: - fiveg_n3: - interface: fiveg_n3 - - fiveg_n4: - interface: fiveg_n4 diff --git a/tests/unit/lib/charms/sdcore_upf/v0/test_charms/test_provider_charm/src/charm.py b/tests/unit/lib/charms/sdcore_upf/v0/test_charms/test_provider_charm/src/charm.py deleted file mode 100644 index f8c9c53..0000000 --- a/tests/unit/lib/charms/sdcore_upf/v0/test_charms/test_provider_charm/src/charm.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -import logging - -from charms.sdcore_upf_k8s.v0.fiveg_n3 import N3Provides -from charms.sdcore_upf_k8s.v0.fiveg_n4 import N4Provides -from ops.charm import CharmBase -from ops.main import main - -logger = logging.getLogger(__name__) - - -class WhateverCharm(CharmBase): - TEST_UPF_IP_ADDRESS = "" - TEST_UPF_HOSTNAME = "" - TEST_UPF_PORT = 0 - - def __init__(self, *args): - """Create a new instance of this object for each event.""" - super().__init__(*args) - self.fiveg_n3_provider = N3Provides(self, "fiveg_n3") - self.fiveg_n4_provider = N4Provides(self, "fiveg_n4") - - self.framework.observe( - self.fiveg_n3_provider.on.fiveg_n3_request, self._on_fiveg_n3_request - ) - self.framework.observe( - self.fiveg_n4_provider.on.fiveg_n4_request, self._on_fiveg_n4_request - ) - - def _on_fiveg_n3_request(self, event): - self.fiveg_n3_provider.publish_upf_information( - relation_id=event.relation_id, - upf_ip_address=self.TEST_UPF_IP_ADDRESS, - ) - - def _on_fiveg_n4_request(self, event): - self.fiveg_n4_provider.publish_upf_n4_information( - relation_id=event.relation_id, - upf_hostname=self.TEST_UPF_HOSTNAME, - upf_n4_port=self.TEST_UPF_PORT, - ) - - -if __name__ == "__main__": - main(WhateverCharm) diff --git a/tests/unit/lib/charms/sdcore_upf/v0/test_charms/test_requirer_charm/metadata.yaml b/tests/unit/lib/charms/sdcore_upf/v0/test_charms/test_requirer_charm/metadata.yaml deleted file mode 100644 index 2adbc16..0000000 --- a/tests/unit/lib/charms/sdcore_upf/v0/test_charms/test_requirer_charm/metadata.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -name: whatever-charm -description: whatever-charm -summary: whatever-charm -containers: - whatever-container: - resource: whatever-image - -resources: - whatever-image: - type: oci-image - description: whatever image - -requires: - fiveg_n3: - interface: fiveg_n3 - - fiveg_n4: - interface: fiveg_n4 diff --git a/tests/unit/lib/charms/sdcore_upf/v0/test_charms/test_requirer_charm/src/charm.py b/tests/unit/lib/charms/sdcore_upf/v0/test_charms/test_requirer_charm/src/charm.py deleted file mode 100644 index 1f265c7..0000000 --- a/tests/unit/lib/charms/sdcore_upf/v0/test_charms/test_requirer_charm/src/charm.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -import logging - -from charms.sdcore_upf_k8s.v0.fiveg_n3 import N3Requires -from charms.sdcore_upf_k8s.v0.fiveg_n4 import N4Requires -from ops.charm import CharmBase -from ops.main import main -from ops.model import ActiveStatus - -logger = logging.getLogger(__name__) - - -class WhateverCharm(CharmBase): - def __init__(self, *args): - """Create a new instance of this object for each event.""" - super().__init__(*args) - self.fiveg_n3 = N3Requires(self, "fiveg_n3") - self.fiveg_n4 = N4Requires(self, "fiveg_n4") - - self.framework.observe(self.fiveg_n3.on.fiveg_n3_available, self._on_relation_available) - self.framework.observe(self.fiveg_n4.on.fiveg_n4_available, self._on_relation_available) - - def _on_relation_available(self, event): - self.model.unit.status = ActiveStatus() - - -if __name__ == "__main__": - main(WhateverCharm) diff --git a/tests/unit/lib/charms/sdcore_upf/v0/test_fiveg_n3_provider.py b/tests/unit/lib/charms/sdcore_upf/v0/test_fiveg_n3_provider.py index 4a529d2..8b083d2 100644 --- a/tests/unit/lib/charms/sdcore_upf/v0/test_fiveg_n3_provider.py +++ b/tests/unit/lib/charms/sdcore_upf/v0/test_fiveg_n3_provider.py @@ -1,66 +1,114 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. -from unittest.mock import PropertyMock, patch import pytest -from ops import testing +import scenario +from charms.sdcore_upf_k8s.v0.fiveg_n3 import FiveGN3RequestEvent, N3Provides +from ops.charm import ActionEvent, CharmBase -from tests.unit.lib.charms.sdcore_upf.v0.test_charms.test_provider_charm.src.charm import ( - WhateverCharm, -) -RELATION_NAME = "fiveg_n3" -REMOVE_APP = "whatever-app" -TEST_CHARM_PATH = ( - "tests.unit.lib.charms.sdcore_upf.v0.test_charms.test_provider_charm.src.charm.WhateverCharm" -) +class N3Provider(CharmBase): + def __init__(self, *args): + super().__init__(*args) + self.fiveg_n3_provider = N3Provides(self, "fiveg_n3") + self.framework.observe( + self.on.publish_upf_information_action, + self._on_publish_upf_information_action, + ) + + def _on_publish_upf_information_action(self, event: ActionEvent): + upf_ip_address = event.params.get("ip-address") + relation_id = event.params.get("relation-id") + assert upf_ip_address + assert relation_id + self.fiveg_n3_provider.publish_upf_information( + upf_ip_address=upf_ip_address, + relation_id=int(relation_id), + ) class TestN3Provides: - patcher_upf_ip_address = patch( - f"{TEST_CHARM_PATH}.TEST_UPF_IP_ADDRESS", new_callable=PropertyMock - ) + @pytest.fixture(autouse=True) + def context(self): + self.ctx = scenario.Context( + charm_type=N3Provider, + meta={ + "name": "n3-provider", + "provides": {"fiveg_n3": {"interface": "fiveg_n3"}}, + }, + actions={ + "publish-upf-information": { + "params": { + "ip-address": {"type": "string"}, + "relation-id": {"type": "string"}, + }, + }, + }, + ) - @pytest.fixture() - def setUp(self) -> None: - self.mock_upf_ip_address = TestN3Provides.patcher_upf_ip_address.start() + def test_given_fiveg_n3_relation_when_set_upf_information_then_info_added_to_relation_data( # noqa: E501 + self, + ): + fiveg_n3_relation = scenario.Relation( + endpoint="fiveg_n3", + interface="fiveg_n3", + ) + state_in = scenario.State( + leader=True, + relations=[fiveg_n3_relation], + ) - @staticmethod - def tearDown() -> None: - patch.stopall() + action = scenario.Action( + name="publish-upf-information", + params={ + "ip-address": "1.2.3.4", + "relation-id": str(fiveg_n3_relation.relation_id), + }, + ) - @pytest.fixture(autouse=True) - def setup_harness(self, setUp, request): - self.harness = testing.Harness(WhateverCharm) - self.harness.set_model_name(name="whatever") - self.harness.set_leader(is_leader=True) - self.harness.begin() - yield self.harness - self.harness.cleanup() - request.addfinalizer(self.tearDown) - - def test_given_fiveg_n3_relation_when_relation_created_then_upf_ip_address_is_published_in_the_relation_data( # noqa: E501 + action_output = self.ctx.run_action(action, state_in) + + assert action_output.state.relations[0].local_app_data["upf_ip_address"] == "1.2.3.4" + + def test_given_invalid_upf_information_when_set_upf_information_then_error_raised( self, ): - test_upf_ip = "1.2.3.4" - self.mock_upf_ip_address.return_value = test_upf_ip - relation_id = self.harness.add_relation(relation_name=RELATION_NAME, remote_app=REMOVE_APP) - self.harness.add_relation_unit(relation_id, f"{REMOVE_APP}/0") + fiveg_n3_relation = scenario.Relation( + endpoint="fiveg_n3", + interface="fiveg_n3", + ) + state_in = scenario.State( + leader=True, + relations=[fiveg_n3_relation], + ) - relation_data = self.harness.get_relation_data( - relation_id=relation_id, app_or_unit=self.harness.charm.app + action = scenario.Action( + name="publish-upf-information", + params={ + "ip-address": "abcdef", + "relation-id": str(fiveg_n3_relation.relation_id), + }, ) - assert test_upf_ip == relation_data["upf_ip_address"] - def test_given_invalid_upf_ip_address_when_relation_created_then_value_error_is_raised( + with pytest.raises(Exception) as e: + self.ctx.run_action(action, state_in) + + assert "Invalid UPF IP address" in str(e.value) + + def test_given_when_relation_joined_then_fiveg_n3_request_event_emitted( self, ): - invalid_upf_ip = "777.888.9999.0" - self.mock_upf_ip_address.return_value = invalid_upf_ip - - with pytest.raises(ValueError): - relation_id = self.harness.add_relation( - relation_name=RELATION_NAME, remote_app=REMOVE_APP - ) - self.harness.add_relation_unit(relation_id, f"{REMOVE_APP}/0") + fiveg_n3_relation = scenario.Relation( + endpoint="fiveg_n3", + interface="fiveg_n3", + ) + state_in = scenario.State( + leader=True, + relations=[fiveg_n3_relation], + ) + + self.ctx.run(fiveg_n3_relation.joined_event, state_in) + + assert len(self.ctx.emitted_events) == 2 + assert isinstance(self.ctx.emitted_events[1], FiveGN3RequestEvent) diff --git a/tests/unit/lib/charms/sdcore_upf/v0/test_fiveg_n3_requirer.py b/tests/unit/lib/charms/sdcore_upf/v0/test_fiveg_n3_requirer.py index 118bdaf..c29b88a 100644 --- a/tests/unit/lib/charms/sdcore_upf/v0/test_fiveg_n3_requirer.py +++ b/tests/unit/lib/charms/sdcore_upf/v0/test_fiveg_n3_requirer.py @@ -1,55 +1,61 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. -from unittest.mock import call, patch import pytest -from ops import BoundEvent, testing +import scenario +from charms.sdcore_upf_k8s.v0.fiveg_n3 import N3AvailableEvent, N3Requires +from ops.charm import CharmBase -from tests.unit.lib.charms.sdcore_upf.v0.test_charms.test_requirer_charm.src.charm import ( - WhateverCharm, -) +class N3Requirer(CharmBase): + def __init__(self, *args): + super().__init__(*args) + self.fiveg_n3_requirer = N3Requires(self, "fiveg_n3") -class TestN3Requires: - patcher_n3_available = patch( - "charms.sdcore_upf_k8s.v0.fiveg_n3.N3RequirerCharmEvents.fiveg_n3_available" - ) - - @pytest.fixture() - def setUp(self) -> None: - self.mock_n3_available = TestN3Requires.patcher_n3_available.start() - self.mock_n3_available.__class__ = BoundEvent - - @staticmethod - def tearDown() -> None: - patch.stopall() +class TestN3Provides: @pytest.fixture(autouse=True) - def setup_harness(self, setUp, request): - self.harness = testing.Harness(WhateverCharm) - self.harness.set_model_name(name="whatever") - self.harness.begin() - yield self.harness - self.harness.cleanup() - request.addfinalizer(self.tearDown) - - def test_given_relation_with_n3_provider_when_fiveg_n3_available_event_then_n3_information_is_provided( # noqa: E501 + def context(self): + self.ctx = scenario.Context( + charm_type=N3Requirer, + meta={ + "name": "n3-requirer", + "requires": {"fiveg_n3": {"interface": "fiveg_n3"}}, + }, + ) + + def test_given_upf_ip_address_in_relation_data_when_relation_changed_then_fiveg_n3_request_event_emitted( # noqa: E501 self, ): - test_upf_ip = "1.2.3.4" - relation_id = self.harness.add_relation( - relation_name="fiveg_n3", remote_app="whatever-app" + fiveg_n3_relation = scenario.Relation( + endpoint="fiveg_n3", + interface="fiveg_n3", + remote_app_data={"upf_ip_address": "1.2.3.4"}, + ) + state_in = scenario.State( + leader=True, + relations=[fiveg_n3_relation], ) - self.harness.add_relation_unit(relation_id, "whatever-app/0") - self.harness.update_relation_data( - relation_id=relation_id, - app_or_unit="whatever-app", - key_values={"upf_ip_address": test_upf_ip}, + self.ctx.run(fiveg_n3_relation.changed_event, state_in) + + assert len(self.ctx.emitted_events) == 2 + assert isinstance(self.ctx.emitted_events[1], N3AvailableEvent) + + def test_given_upf_ip_address_not_in_relation_data_when_relation_changed_then_fiveg_n3_request_event_emitted( # noqa: E501 + self, + ): + fiveg_n3_relation = scenario.Relation( + endpoint="fiveg_n3", + interface="fiveg_n3", + remote_app_data={}, ) + state_in = scenario.State( + leader=True, + relations=[fiveg_n3_relation], + ) + + self.ctx.run(fiveg_n3_relation.changed_event, state_in) - calls = [ - call.emit(upf_ip_address=test_upf_ip), - ] - self.mock_n3_available.assert_has_calls(calls) + assert len(self.ctx.emitted_events) == 1 diff --git a/tests/unit/lib/charms/sdcore_upf/v0/test_fiveg_n4_provider.py b/tests/unit/lib/charms/sdcore_upf/v0/test_fiveg_n4_provider.py index 95da559..d7ba465 100644 --- a/tests/unit/lib/charms/sdcore_upf/v0/test_fiveg_n4_provider.py +++ b/tests/unit/lib/charms/sdcore_upf/v0/test_fiveg_n4_provider.py @@ -1,76 +1,95 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. -from unittest.mock import PropertyMock, patch import pytest -from ops import testing +import scenario +from charms.sdcore_upf_k8s.v0.fiveg_n4 import FiveGN4RequestEvent, N4Provides +from ops.charm import ActionEvent, CharmBase -from tests.unit.lib.charms.sdcore_upf.v0.test_charms.test_provider_charm.src.charm import ( - WhateverCharm, -) -TEST_CHARM_PATH = ( - "tests.unit.lib.charms.sdcore_upf.v0.test_charms.test_provider_charm.src.charm.WhateverCharm" -) -VALID_HOSTNAME = "upf.edge-cloud.test.com" -VALID_PORT = 1234 - - -class TestN4Provides: - patcher_upf_hostname = patch(f"{TEST_CHARM_PATH}.TEST_UPF_HOSTNAME", new_callable=PropertyMock) - patcher_upf_port = patch(f"{TEST_CHARM_PATH}.TEST_UPF_PORT", new_callable=PropertyMock) +class N4Provider(CharmBase): + def __init__(self, *args): + super().__init__(*args) + self.fiveg_n4_provider = N4Provides(self, "fiveg_n4") + self.framework.observe( + self.on.publish_upf_information_action, + self._on_publish_upf_information_action, + ) - @pytest.fixture() - def setUp(self) -> None: - self.mock_upf_hostname = TestN4Provides.patcher_upf_hostname.start() - self.mock_upf_port = TestN4Provides.patcher_upf_port.start() - self.mock_upf_hostname.return_value = VALID_HOSTNAME - self.mock_upf_port.return_value = VALID_PORT + def _on_publish_upf_information_action(self, event: ActionEvent): + hostname = event.params.get("hostname") + port = event.params.get("port") + relation_id = event.params.get("relation-id") + assert hostname + assert port + assert relation_id + self.fiveg_n4_provider.publish_upf_n4_information( + relation_id=int(relation_id), + upf_hostname=hostname, + upf_n4_port=int(port), + ) - @staticmethod - def tearDown() -> None: - patch.stopall() +class TestN4Provides: @pytest.fixture(autouse=True) - def setup_harness(self, setUp, request): - self.harness = testing.Harness(WhateverCharm) - self.harness.set_model_name(name="whatever") - self.harness.set_leader(is_leader=True) - self.harness.begin() - yield self.harness - self.harness.cleanup() - request.addfinalizer(self.tearDown) - - def add_fiveg_n4_relation(self) -> int: - relation_id = self.harness.add_relation( - relation_name="fiveg_n4", remote_app="whatever-app" + def context(self): + self.ctx = scenario.Context( + charm_type=N4Provider, + meta={ + "name": "n4-provider", + "provides": {"fiveg_n4": {"interface": "fiveg_n4"}}, + }, + actions={ + "publish-upf-information": { + "params": { + "relation-id": {"type": "string"}, + "hostname": {"type": "string"}, + "port": {"type": "string"}, + }, + }, + }, ) - self.harness.add_relation_unit(relation_id, "whatever-app/0") - return relation_id - def test_given_fiveg_n4_relation_when_relation_created_then_upf_hostname_and_upf_port_is_published_in_the_relation_data( # noqa: E501 + def test_given_fiveg_n4_relation_when_set_upf_information_then_info_added_to_relation_data( # noqa: E501 self, ): - relation_id = self.add_fiveg_n4_relation() - relation_data = self.harness.get_relation_data( - relation_id=relation_id, app_or_unit=self.harness.charm.app + fiveg_n4_relation = scenario.Relation( + endpoint="fiveg_n4", + interface="fiveg_n4", + ) + state_in = scenario.State( + leader=True, + relations=[fiveg_n4_relation], ) - assert VALID_HOSTNAME == relation_data["upf_hostname"] - assert str(VALID_PORT) == relation_data["upf_port"] - def test_given_invalid_upf_hostname_when_relation_created_then_value_error_is_raised( - self, - ): - test_invalid_upf_hostname = None - self.mock_upf_hostname.return_value = test_invalid_upf_hostname - with pytest.raises(ValueError): - self.add_fiveg_n4_relation() + action = scenario.Action( + name="publish-upf-information", + params={ + "relation-id": str(fiveg_n4_relation.relation_id), + "hostname": "upf", + "port": "1234", + }, + ) + + action_output = self.ctx.run_action(action, state_in) + + assert action_output.state.relations[0].local_app_data["upf_hostname"] == "upf" + assert action_output.state.relations[0].local_app_data["upf_port"] == "1234" - def test_given_invalid_upf_port_when_relation_created_then_value_error_is_raised( + def test_given_when_relation_joined_then_fiveg_n4_request_event_emitted( self, ): - test_invalid_upf_port = "not_an_int" - self.mock_upf_port.return_value = test_invalid_upf_port - with pytest.raises(ValueError): - self.add_fiveg_n4_relation() + fiveg_n4_relation = scenario.Relation( + endpoint="fiveg_n4", + interface="fiveg_n4", + ) + state_in = scenario.State( + leader=True, + relations=[fiveg_n4_relation], + ) + + self.ctx.run(fiveg_n4_relation.joined_event, state_in) + + assert len(self.ctx.emitted_events) == 2 + assert isinstance(self.ctx.emitted_events[1], FiveGN4RequestEvent) diff --git a/tests/unit/lib/charms/sdcore_upf/v0/test_fiveg_n4_requirer.py b/tests/unit/lib/charms/sdcore_upf/v0/test_fiveg_n4_requirer.py index 4c7c60b..e04aff7 100644 --- a/tests/unit/lib/charms/sdcore_upf/v0/test_fiveg_n4_requirer.py +++ b/tests/unit/lib/charms/sdcore_upf/v0/test_fiveg_n4_requirer.py @@ -1,55 +1,64 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. -from unittest.mock import call, patch import pytest -from ops import BoundEvent, testing +import scenario +from charms.sdcore_upf_k8s.v0.fiveg_n4 import N4AvailableEvent, N4Requires +from ops.charm import CharmBase -from tests.unit.lib.charms.sdcore_upf.v0.test_charms.test_requirer_charm.src.charm import ( - WhateverCharm, -) +class N4Requirer(CharmBase): + def __init__(self, *args): + super().__init__(*args) + self.fiveg_n4_requirer = N4Requires(self, "fiveg_n4") -class TestN4Requires: - patch_n4_available = patch( - "charms.sdcore_upf_k8s.v0.fiveg_n4.N4RequirerCharmEvents.fiveg_n4_available" - ) - @pytest.fixture() - def setUp(self) -> None: - self.mock_n4_available = TestN4Requires.patch_n4_available.start() - self.mock_n4_available.__class__ = BoundEvent +class TestN4Provides: + @pytest.fixture(autouse=True) + def context(self): + self.ctx = scenario.Context( + charm_type=N4Requirer, + meta={ + "name": "n4-requirer", + "requires": {"fiveg_n4": {"interface": "fiveg_n4"}}, + }, + ) + + def test_given_upf_hostname_in_relation_data_when_relation_changed_then_fiveg_n4_request_event_emitted( # noqa: E501 + self, + ): + fiveg_n4_relation = scenario.Relation( + endpoint="fiveg_n4", + interface="fiveg_n4", + remote_app_data={ + "upf_hostname": "1.2.3.4", + "upf_port": "1234", + }, + ) + state_in = scenario.State( + leader=True, + relations=[fiveg_n4_relation], + ) - @staticmethod - def tearDown() -> None: - patch.stopall() + self.ctx.run(fiveg_n4_relation.changed_event, state_in) - @pytest.fixture(autouse=True) - def setup_harness(self, setUp, request): - self.harness = testing.Harness(WhateverCharm) - self.harness.set_model_name(name="whatever") - self.harness.begin() - yield self.harness - self.harness.cleanup() - request.addfinalizer(self.tearDown) - - def test_given_relation_with_n4_provider_when_fiveg_n4_available_event_then_n4_information_is_provided( # noqa: E501 + assert len(self.ctx.emitted_events) == 2 + assert isinstance(self.ctx.emitted_events[1], N4AvailableEvent) + + def test_given_upf_hostname_not_in_relation_data_when_relation_changed_then_fiveg_n4_request_event_emitted( # noqa: E501 self, ): - test_upf_hostname = "upf.edge-cloud.test.com" - test_upf_port = 1234 - relation_id = self.harness.add_relation( - relation_name="fiveg_n4", remote_app="whatever-app" + fiveg_n4_relation = scenario.Relation( + endpoint="fiveg_n4", + interface="fiveg_n4", + remote_app_data={}, ) - self.harness.add_relation_unit(relation_id, "whatever-app/0") - self.harness.update_relation_data( - relation_id=relation_id, - app_or_unit="whatever-app", - key_values={"upf_hostname": test_upf_hostname, "upf_port": str(test_upf_port)}, + state_in = scenario.State( + leader=True, + relations=[fiveg_n4_relation], ) - calls = [ - call.emit(upf_hostname=test_upf_hostname, upf_port=str(test_upf_port)), - ] - self.mock_n4_available.assert_has_calls(calls) + self.ctx.run(fiveg_n4_relation.changed_event, state_in) + + assert len(self.ctx.emitted_events) == 1 diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 1e281bc..dc411fc 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -5,6 +5,14 @@ from unittest.mock import Mock, call, patch import pytest +from charms.kubernetes_charm_libraries.v0.multus import ( + NetworkAnnotation, + NetworkAttachmentDefinition, +) +from lightkube.models.core_v1 import Node, NodeStatus +from ops import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus, testing +from ops.pebble import ConnectionError + from charm import ( ACCESS_INTERFACE_NAME, ACCESS_NETWORK_ATTACHMENT_DEFINITION_NAME, @@ -14,13 +22,6 @@ DPDK_CORE_INTERFACE_RESOURCE_NAME, UPFOperatorCharm, ) -from charms.kubernetes_charm_libraries.v0.multus import ( - NetworkAnnotation, - NetworkAttachmentDefinition, -) -from lightkube.models.core_v1 import Node, NodeStatus -from ops import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus, testing -from ops.pebble import ConnectionError MULTUS_LIBRARY = "charms.kubernetes_charm_libraries.v0.multus.KubernetesMultusCharmLib" K8S_CLIENT = "charms.kubernetes_charm_libraries.v0.multus.KubernetesClient" diff --git a/tests/unit/test_dpdk.py b/tests/unit/test_dpdk.py index 7a0fb0d..77bdc6a 100644 --- a/tests/unit/test_dpdk.py +++ b/tests/unit/test_dpdk.py @@ -4,7 +4,6 @@ from unittest.mock import MagicMock, Mock, patch import pytest -from dpdk import DPDK, DPDKError from lightkube.core.exceptions import ApiError from lightkube.models.apps_v1 import StatefulSetSpec from lightkube.models.core_v1 import ( @@ -17,6 +16,8 @@ from lightkube.models.meta_v1 import LabelSelector, ObjectMeta from lightkube.resources.apps_v1 import StatefulSet +from dpdk import DPDK, DPDKError + TEST_CONTAINER_NAME = "bullseye" TEST_RESOURCE_REQUESTS = {"test_request": 1234} TEST_RESOURCE_LIMITS = {"test_limit": 4321}