From 8e993d7e0f52f3d1eeadb943659f187c041b4c25 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Thu, 6 Nov 2025 13:58:34 +0100 Subject: [PATCH 01/14] feat: Update integration test workflow and dependencies for Allure reporting --- .github/workflows/integration_test.yaml | 10 ++++++---- tox.ini | 12 +++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 0c59fef..3c8f152 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -13,8 +13,10 @@ jobs: pre-run-script: | -c "chmod +x tests/integration/pre_run_script.sh ./tests/integration/pre_run_script.sh" - # self-hosted-runner: true - # self-hosted-runner-label: "edge" - python-version: "3.12" - runs-on: "ubuntu-24.04" + self-hosted-runner: true + self-hosted-runner-label: "edge" modules: '["test_charm", "test_proxy"]' + allure-report: + if: always() && !cancelled() + needs: integration-tests + uses: canonical/operator-workflows/.github/workflows/allure_report.yaml@main diff --git a/tox.ini b/tox.ini index 2ac829b..cac9b3e 100644 --- a/tox.ini +++ b/tox.ini @@ -94,14 +94,16 @@ commands = [testenv:integration] description = Run integration tests deps = - pytest + -r{toxinidir}/requirements.txt + allure-pytest>=2.8.18 + git+https://github.com/canonical/data-platform-workflows@v24.0.0\#subdirectory=python/pytest_plugins/allure_pytest_collection_report juju>=3,<4 - ops - pytest-operator - pytest-asyncio # Type error problem with newer version of macaroonbakery macaroonbakery==1.3.2 - -r{toxinidir}/requirements.txt + ops + pytest + pytest-asyncio + pytest-operator commands = pytest --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs} From 5ede3536d9dd5b276efad8383090668054dd98d3 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Fri, 7 Nov 2025 09:58:39 +0100 Subject: [PATCH 02/14] chore: integration tests with series fixture --- tests/integration/conftest.py | 44 +++++++-------------------------- tests/integration/helpers.py | 21 ++++++++++++++++ tests/integration/test_proxy.py | 9 ++++--- 3 files changed, 36 insertions(+), 38 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index f1ef059..674b57d 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -4,6 +4,7 @@ """Fixtures for tmate-ssh-server charm integration tests.""" import logging import secrets +import subprocess import typing from pathlib import Path @@ -41,10 +42,16 @@ async def charm_fixture(request: pytest.FixtureRequest, ops_test: OpsTest) -> st return charm +@pytest.fixture(name="series") +def series_fixture(): + """Series for deploying any-charm.""" + return subprocess.check_output(["lsb_release", "-cs"]).strip().decode("utf-8") + + @pytest_asyncio.fixture(scope="module", name="tmate_ssh_server") -async def tmate_ssh_server_fixture(model: Model, charm: str): +async def tmate_ssh_server_fixture(model: Model, charm: str, series: str): """The tmate-ssh-server application fixture.""" - app = await model.deploy(charm) + app = await model.deploy(charm, series=series) await model.wait_for_idle(apps=[app.name], wait_for_active=True) return app @@ -158,36 +165,3 @@ async def proxy_machine_fixture(ops_test: OpsTest, machine: Machine): ) assert retcode == 0, f"Failed to restart squid service, {stdout} {stderr}" return machine - - -@pytest_asyncio.fixture(scope="module", name="machine_ip") -async def machine_ip_fixture(machine: Machine) -> str: - """The machine public IP address.""" - - def get_machine_ip_address() -> typing.Optional[str]: - """Get latest machine IP address. - - Returns: - The latest machine IP address if ready, None otherwise. - """ - latest_machine = machine.latest() - addresses = latest_machine.data["addresses"] - try: - address = next( - iter( - [ - address["value"] - for address in addresses - if address["scope"] != "local-machine" - ] - ) - ) - except StopIteration: - return None - return address - - await wait_for(get_machine_ip_address) - - # mypy doesn't understand that get_machine_ip_address has to be str from wait_for statement - # above. - return typing.cast(str, get_machine_ip_address()) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index ce580b8..c244dec 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -7,6 +7,27 @@ import time import typing +from juju.machine import Machine + + +def get_machine_ip_address(machine: Machine) -> typing.Optional[str]: + """Get latest machine IP address. + + Returns: + The latest machine IP address if ready, None otherwise. + """ + latest_machine = machine.latest() + addresses = latest_machine.data["addresses"] + try: + address = next( + iter( + [address["value"] for address in addresses if address["scope"] != "local-machine"] + ) + ) + except StopIteration: + return None + return address + async def wait_for( func: typing.Callable[[], typing.Union[typing.Awaitable, typing.Any]], diff --git a/tests/integration/test_proxy.py b/tests/integration/test_proxy.py index 6efb512..807c347 100644 --- a/tests/integration/test_proxy.py +++ b/tests/integration/test_proxy.py @@ -9,7 +9,7 @@ from juju.unit import Unit from pytest_operator.plugin import OpsTest -from .helpers import wait_for +from .helpers import get_machine_ip_address, wait_for logger = logging.getLogger(__name__) @@ -19,13 +19,16 @@ async def test_proxy( model: Model, charm: str, proxy_machine: Machine, - machine_ip: str, + series: str, ): """ arrange: given a model configured with squid proxy ip address. act: when tmate charm is deployed. assert: proxy log contains docker access. """ + machine_ip: str | None = await wait_for(get_machine_ip_address(machine=proxy_machine)) + assert machine_ip is not None, "Proxy machine IP address not found." + await model.set_config( { "juju-http-proxy": f"http://{machine_ip}:3218", @@ -34,7 +37,7 @@ async def test_proxy( ) logger.info("Deploying tmate charm.") - app = await model.deploy(charm) + app = await model.deploy(charm, series=series) await model.wait_for_idle(apps=[app.name], wait_for_active=True) unit: Unit = next(iter(app.units)) From ba5a6aa0a42295790c8eedba0cb6688efb6cc3fc Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Mon, 17 Nov 2025 16:33:46 +0800 Subject: [PATCH 03/14] test: fix fixture scoping --- tests/integration/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 674b57d..5e2f430 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -42,7 +42,7 @@ async def charm_fixture(request: pytest.FixtureRequest, ops_test: OpsTest) -> st return charm -@pytest.fixture(name="series") +@pytest.fixture(scope="module", name="series") def series_fixture(): """Series for deploying any-charm.""" return subprocess.check_output(["lsb_release", "-cs"]).strip().decode("utf-8") From 3f8fe3fed2baae84ff6ef188f9d83090b20109b0 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Mon, 17 Nov 2025 16:33:59 +0800 Subject: [PATCH 04/14] test: fix wait for machine address --- tests/integration/test_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_proxy.py b/tests/integration/test_proxy.py index 807c347..9f6de41 100644 --- a/tests/integration/test_proxy.py +++ b/tests/integration/test_proxy.py @@ -26,7 +26,7 @@ async def test_proxy( act: when tmate charm is deployed. assert: proxy log contains docker access. """ - machine_ip: str | None = await wait_for(get_machine_ip_address(machine=proxy_machine)) + machine_ip: str | None = await wait_for(lambda: get_machine_ip_address(machine=proxy_machine)) assert machine_ip is not None, "Proxy machine IP address not found." await model.set_config( From 64b7b1373492e6a09df4ce4d569d2b9cb76c437b Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Mon, 17 Nov 2025 16:38:56 +0800 Subject: [PATCH 05/14] test: lint fix --- tests/integration/helpers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index c244dec..90ad887 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -13,6 +13,9 @@ def get_machine_ip_address(machine: Machine) -> typing.Optional[str]: """Get latest machine IP address. + Args: + machine: The machine to get the IP address for. + Returns: The latest machine IP address if ready, None otherwise. """ From ba4a7df89a7cf6c60ef39255c9962fb88ee0bfd3 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Mon, 17 Nov 2025 16:54:44 +0800 Subject: [PATCH 06/14] fix: update subprocess command for series fixture to include full path and add security comments --- tests/integration/conftest.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 5e2f430..7e23f5d 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -4,7 +4,9 @@ """Fixtures for tmate-ssh-server charm integration tests.""" import logging import secrets -import subprocess + +# Subprocess module is used to check series +import subprocess # nosec B404 import typing from pathlib import Path @@ -45,7 +47,8 @@ async def charm_fixture(request: pytest.FixtureRequest, ops_test: OpsTest) -> st @pytest.fixture(scope="module", name="series") def series_fixture(): """Series for deploying any-charm.""" - return subprocess.check_output(["lsb_release", "-cs"]).strip().decode("utf-8") + # nosec B603 - lsb_release is a system command with no user input + return subprocess.check_output(["/usr/bin/lsb_release", "-cs"]).strip().decode("utf-8") @pytest_asyncio.fixture(scope="module", name="tmate_ssh_server") From 454ae8966554a0de0c18a8d830a22dea710b22be Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Tue, 18 Nov 2025 09:08:31 +0800 Subject: [PATCH 07/14] fix: update series fixture to improve code readability and add security comment --- tests/integration/conftest.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 7e23f5d..585a1ab 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -47,8 +47,12 @@ async def charm_fixture(request: pytest.FixtureRequest, ops_test: OpsTest) -> st @pytest.fixture(scope="module", name="series") def series_fixture(): """Series for deploying any-charm.""" - # nosec B603 - lsb_release is a system command with no user input - return subprocess.check_output(["/usr/bin/lsb_release", "-cs"]).strip().decode("utf-8") + return ( + # lsb_release is a system command with no user input + subprocess.check_output(["/usr/bin/lsb_release", "-cs"]) # nosec B603 + .strip() + .decode("utf-8") + ) @pytest_asyncio.fixture(scope="module", name="tmate_ssh_server") From 718070ea9c54e4f901e226c61d37e395d995bbcc Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Tue, 18 Nov 2025 09:17:20 +0800 Subject: [PATCH 08/14] refactor: update charmcraft and metadata files to streamline platform definitions --- charmcraft.yaml | 10 +++------- metadata.yaml | 3 +-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/charmcraft.yaml b/charmcraft.yaml index bb9fd23..72b9786 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -2,13 +2,9 @@ # See LICENSE file for licensing details. type: charm -bases: - - build-on: - - name: ubuntu - channel: "22.04" - run-on: - - name: ubuntu - channel: "22.04" +platforms: + ubuntu@22.04:amd64: + ubuntu@24.04:amd64: parts: charm: diff --git a/metadata.yaml b/metadata.yaml index cb4b530..2203e18 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -25,8 +25,7 @@ docs: https://discourse.charmhub.io/t/tmate-ssh-server-documentation-overview/12 issues: https://github.com/canonical/tmate-ssh-server-operator/issues source: https://github.com/canonical/tmate-ssh-server-operator summary: Tmate SSH Relay Server -series: - - jammy + tags: - application_development - ops From c720848f37212966cb6175645e463a2bf0b8b879 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Tue, 18 Nov 2025 09:36:58 +0800 Subject: [PATCH 09/14] fix: enhance charm fixture to validate series-specific charm builds --- tests/integration/conftest.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 585a1ab..8f4561e 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -32,18 +32,6 @@ def model_fixture(ops_test: OpsTest) -> Model: return ops_test.model -@pytest_asyncio.fixture(scope="module", name="charm") -async def charm_fixture(request: pytest.FixtureRequest, ops_test: OpsTest) -> str: - """The path to charm.""" - charm = request.config.getoption("--charm-file") - if not charm: - charm = await ops_test.build_charm(".") - else: - charm = f"./{charm}" - - return charm - - @pytest.fixture(scope="module", name="series") def series_fixture(): """Series for deploying any-charm.""" @@ -55,6 +43,21 @@ def series_fixture(): ) +@pytest_asyncio.fixture(scope="module", name="charm") +async def charm_fixture(request: pytest.FixtureRequest, ops_test: OpsTest, series: str) -> str: + """The path to charm.""" + charm = request.config.getoption("--charm-file") + if not charm: + charm = await ops_test.build_charm(".") + else: + charm_dir = Path(f"./{charm}").parent + charm_matching_series = list(charm_dir.rglob(f"*{series}*.charm")) + assert charm_matching_series is not None, f"No build found for series {series}" + return charm_matching_series[0] + + return charm + + @pytest_asyncio.fixture(scope="module", name="tmate_ssh_server") async def tmate_ssh_server_fixture(model: Model, charm: str, series: str): """The tmate-ssh-server application fixture.""" From c22be8906499a921c0048b593095250130f69cc6 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Tue, 18 Nov 2025 09:52:20 +0800 Subject: [PATCH 10/14] fix: update charm fixture signature for improved type hinting --- tests/integration/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 8f4561e..7c0fde1 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -44,7 +44,9 @@ def series_fixture(): @pytest_asyncio.fixture(scope="module", name="charm") -async def charm_fixture(request: pytest.FixtureRequest, ops_test: OpsTest, series: str) -> str: +async def charm_fixture( + request: pytest.FixtureRequest, ops_test: OpsTest, series: str +) -> str | Path: """The path to charm.""" charm = request.config.getoption("--charm-file") if not charm: From 4b227b9b333f3f456973102e9d0f91d5563c62ff Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Tue, 18 Nov 2025 10:29:25 +0800 Subject: [PATCH 11/14] fix: rename series fixture to codename and update deployment logic --- tests/integration/conftest.py | 23 ++++++++++++----------- tests/integration/test_proxy.py | 4 ++-- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 7c0fde1..bc8c0ce 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -32,15 +32,16 @@ def model_fixture(ops_test: OpsTest) -> Model: return ops_test.model -@pytest.fixture(scope="module", name="series") +@pytest.fixture(name="codename", scope="module") +def codename_fixture(): + """Series codename for deploying any-charm.""" + return subprocess.check_output(["/usr/bin/lsb_release", "-cs"]).strip().decode("utf-8") + + +@pytest.fixture(name="series", scope="module") def series_fixture(): - """Series for deploying any-charm.""" - return ( - # lsb_release is a system command with no user input - subprocess.check_output(["/usr/bin/lsb_release", "-cs"]) # nosec B603 - .strip() - .decode("utf-8") - ) + """Series version for deploying any-charm.""" + return subprocess.check_output(["/usr/bin/lsb_release", "-rs"]).strip().decode("utf-8") @pytest_asyncio.fixture(scope="module", name="charm") @@ -54,16 +55,16 @@ async def charm_fixture( else: charm_dir = Path(f"./{charm}").parent charm_matching_series = list(charm_dir.rglob(f"*{series}*.charm")) - assert charm_matching_series is not None, f"No build found for series {series}" + assert len(charm_matching_series), f"No build found for series {series}" return charm_matching_series[0] return charm @pytest_asyncio.fixture(scope="module", name="tmate_ssh_server") -async def tmate_ssh_server_fixture(model: Model, charm: str, series: str): +async def tmate_ssh_server_fixture(model: Model, charm: str, codename: str): """The tmate-ssh-server application fixture.""" - app = await model.deploy(charm, series=series) + app = await model.deploy(charm, series=codename) await model.wait_for_idle(apps=[app.name], wait_for_active=True) return app diff --git a/tests/integration/test_proxy.py b/tests/integration/test_proxy.py index 9f6de41..3a64c58 100644 --- a/tests/integration/test_proxy.py +++ b/tests/integration/test_proxy.py @@ -19,7 +19,7 @@ async def test_proxy( model: Model, charm: str, proxy_machine: Machine, - series: str, + codename: str, ): """ arrange: given a model configured with squid proxy ip address. @@ -37,7 +37,7 @@ async def test_proxy( ) logger.info("Deploying tmate charm.") - app = await model.deploy(charm, series=series) + app = await model.deploy(charm, series=codename) await model.wait_for_idle(apps=[app.name], wait_for_active=True) unit: Unit = next(iter(app.units)) From ed67a796b148f9894e55c8f6b408cebfb9edc86b Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Tue, 18 Nov 2025 10:47:31 +0800 Subject: [PATCH 12/14] fix: enhance codename and series fixtures for improved readability and security --- tests/integration/conftest.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index bc8c0ce..fc0aac4 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -35,13 +35,21 @@ def model_fixture(ops_test: OpsTest) -> Model: @pytest.fixture(name="codename", scope="module") def codename_fixture(): """Series codename for deploying any-charm.""" - return subprocess.check_output(["/usr/bin/lsb_release", "-cs"]).strip().decode("utf-8") + return ( + subprocess.check_output(["/usr/bin/lsb_release", "-cs"]) # nosec B603 + .strip() + .decode("utf-8") + ) @pytest.fixture(name="series", scope="module") def series_fixture(): """Series version for deploying any-charm.""" - return subprocess.check_output(["/usr/bin/lsb_release", "-rs"]).strip().decode("utf-8") + return ( + subprocess.check_output(["/usr/bin/lsb_release", "-rs"]) # nosec B603 + .strip() + .decode("utf-8") + ) @pytest_asyncio.fixture(scope="module", name="charm") @@ -55,7 +63,7 @@ async def charm_fixture( else: charm_dir = Path(f"./{charm}").parent charm_matching_series = list(charm_dir.rglob(f"*{series}*.charm")) - assert len(charm_matching_series), f"No build found for series {series}" + assert charm_matching_series, f"No build found for series {series}" return charm_matching_series[0] return charm From 6c70bd8d7fe8ad6fe42ceb04e5c0b92974ee00a4 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Tue, 18 Nov 2025 11:36:17 +0800 Subject: [PATCH 13/14] debug --- .github/workflows/integration_test.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 3c8f152..da8edaf 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -16,6 +16,8 @@ jobs: self-hosted-runner: true self-hosted-runner-label: "edge" modules: '["test_charm", "test_proxy"]' + tmate-debug: true + tmate-timeout: 90 allure-report: if: always() && !cancelled() needs: integration-tests From 85422fd7349131036094ecafc6dd0693954870e1 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Tue, 18 Nov 2025 13:36:54 +0800 Subject: [PATCH 14/14] fix: update charm fixture to return full path for matching series charm --- .github/workflows/integration_test.yaml | 2 -- tests/integration/conftest.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index da8edaf..3c8f152 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -16,8 +16,6 @@ jobs: self-hosted-runner: true self-hosted-runner-label: "edge" modules: '["test_charm", "test_proxy"]' - tmate-debug: true - tmate-timeout: 90 allure-report: if: always() && !cancelled() needs: integration-tests diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index fc0aac4..a16d18f 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -64,7 +64,7 @@ async def charm_fixture( charm_dir = Path(f"./{charm}").parent charm_matching_series = list(charm_dir.rglob(f"*{series}*.charm")) assert charm_matching_series, f"No build found for series {series}" - return charm_matching_series[0] + return f"./{charm_matching_series[0]}" return charm