diff --git a/.github/workflows/agent-tox.yml b/.github/workflows/agent-tox.yml index 851a47b70..538255655 100644 --- a/.github/workflows/agent-tox.yml +++ b/.github/workflows/agent-tox.yml @@ -109,4 +109,11 @@ jobs: if-no-files-found: error retention-days: 2 - name: Run charm integration tests - run: uvx --with tox-uv tox -e integration + run: uvx --with tox-uv tox -e integration -- --juju-dump-logs logs + - name: Upload logs + if: ${{ !cancelled() }} + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: juju-dump-logs + path: agent/charms/testflinger-agent-host-charm/logs + retention-days: 2 diff --git a/.github/workflows/server-tox.yml b/.github/workflows/server-tox.yml index f09744303..3e8d2628d 100644 --- a/.github/workflows/server-tox.yml +++ b/.github/workflows/server-tox.yml @@ -119,4 +119,11 @@ jobs: if-no-files-found: error retention-days: 2 - name: Run charm integration tests - run: uvx --with tox-uv tox -e integration + run: uvx --with tox-uv tox -e integration -- --juju-dump-logs logs + - name: Upload logs + if: ${{ !cancelled() }} + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: juju-dump-logs + path: server/charm/logs + retention-days: 2 diff --git a/agent/charms/testflinger-agent-host-charm/pyproject.toml b/agent/charms/testflinger-agent-host-charm/pyproject.toml index 3e223872e..186efc6dd 100644 --- a/agent/charms/testflinger-agent-host-charm/pyproject.toml +++ b/agent/charms/testflinger-agent-host-charm/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ [dependency-groups] lint = ["ruff"] unit = ["coverage[toml]", "ops[testing]", "pytest"] -integration = ["jubilant>=1.6.2", "ops[testing]", "pytest", "requests"] +integration = ["jubilant>=1.6.2", "pytest", "pytest-jubilant", "requests"] [tool.coverage.run] branch = true @@ -33,10 +33,7 @@ log_cli_level = "INFO" [tool.ruff] line-length = 79 extend-exclude = ["lib"] -include = [ - "src/**/*.py", - "tests/**/*.py", -] +include = ["src/**/*.py", "tests/**/*.py"] [tool.ruff.lint] select = [ diff --git a/agent/charms/testflinger-agent-host-charm/tests/integration/conftest.py b/agent/charms/testflinger-agent-host-charm/tests/integration/conftest.py index f366b653d..495c1619c 100644 --- a/agent/charms/testflinger-agent-host-charm/tests/integration/conftest.py +++ b/agent/charms/testflinger-agent-host-charm/tests/integration/conftest.py @@ -5,8 +5,6 @@ import json import logging import os -import sys -import time from datetime import datetime, timezone from pathlib import Path @@ -39,20 +37,6 @@ def create_mock_token(juju: jubilant.Juju, app_name: str): ) -@pytest.fixture(scope="module") -def juju(request: pytest.FixtureRequest): - """Create temporary Juju model for running tests.""" - with jubilant.temp_model() as juju: - juju.wait_timeout = 600 - yield juju - - if request.session.testsfailed: - logger.info("Collecting Juju logs...") - time.sleep(0.5) # Wait for Juju to process logs. - log = juju.debug_log(limit=1000) - print(log, end="", file=sys.stderr) - - @pytest.fixture(scope="session") def charm_path(): """Return the path of the charm under test.""" diff --git a/agent/charms/testflinger-agent-host-charm/tests/integration/test_charm_integration.py b/agent/charms/testflinger-agent-host-charm/tests/integration/test_charm_integration.py index 25d634c71..ec9d2cbdc 100644 --- a/agent/charms/testflinger-agent-host-charm/tests/integration/test_charm_integration.py +++ b/agent/charms/testflinger-agent-host-charm/tests/integration/test_charm_integration.py @@ -5,6 +5,7 @@ from pathlib import Path import jubilant +import pytest import yaml from conftest import create_mock_token @@ -27,6 +28,7 @@ APP_NAME = METADATA["name"] +@pytest.mark.juju_setup def test_deploy(charm_path: Path, juju: jubilant.Juju): """Deploy the charm under test.""" juju.deploy(charm_path.resolve(), app=APP_NAME) @@ -154,3 +156,9 @@ def test_supervisord_agent_running(juju: jubilant.Juju): if "RUNNING" in line ] assert len(running_agents) == 2 + + +@pytest.mark.juju_teardown +def test_destroy(juju: jubilant.Juju): + """Tear down the charm under test.""" + juju.remove_application(APP_NAME) diff --git a/agent/charms/testflinger-agent-host-charm/uv.lock b/agent/charms/testflinger-agent-host-charm/uv.lock index 6ef0bc580..713ec445c 100644 --- a/agent/charms/testflinger-agent-host-charm/uv.lock +++ b/agent/charms/testflinger-agent-host-charm/uv.lock @@ -686,6 +686,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] +[[package]] +name = "pytest-jubilant" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jubilant" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/64/e83181af1004f64a3ce11c7b9770c3152832e02d00e9e7b4f01877325c5f/pytest_jubilant-2.0.1.tar.gz", hash = "sha256:960bb63d216ec746f7644b67c276b059fccdaa258ee7644aaa74058db659edac", size = 16311, upload-time = "2026-04-07T02:08:25.874Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/3c/5c0bc4f54f6905aa7515cf764c3f5d6c85711cdc1a928221f711d5b7532c/pytest_jubilant-2.0.1-py3-none-any.whl", hash = "sha256:df498ee79d6db5ec32baa35a121ab75852dd50f61504a12550ba81cd63a7f1ef", size = 13525, upload-time = "2026-04-07T02:08:24.531Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -827,8 +840,8 @@ dependencies = [ [package.dev-dependencies] integration = [ { name = "jubilant" }, - { name = "ops", extra = ["testing"] }, { name = "pytest" }, + { name = "pytest-jubilant" }, { name = "requests" }, ] lint = [ @@ -856,8 +869,8 @@ requires-dist = [ [package.metadata.requires-dev] integration = [ { name = "jubilant", specifier = ">=1.6.2" }, - { name = "ops", extras = ["testing"] }, { name = "pytest" }, + { name = "pytest-jubilant" }, { name = "requests" }, ] lint = [{ name = "ruff" }] diff --git a/server/charm/pyproject.toml b/server/charm/pyproject.toml index 593838a50..1e4604d2f 100644 --- a/server/charm/pyproject.toml +++ b/server/charm/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ [dependency-groups] lint = ["ruff"] unit = ["coverage[toml]", "ops[testing]", "pytest"] -integration = ["jubilant", "ops[testing]", "pytest", "requests"] +integration = ["jubilant", "pytest", "pytest-jubilant", "requests"] [tool.coverage.run] branch = true @@ -29,10 +29,7 @@ log_cli_level = "INFO" [tool.ruff] line-length = 79 extend-exclude = ["lib"] -include = [ - "src/**/*.py", - "tests/**/*.py", -] +include = ["src/**/*.py", "tests/**/*.py"] [tool.ruff.lint] select = [ @@ -63,4 +60,4 @@ ignore = [ ] [tool.ruff.lint.pydocstyle] -convention = "pep257" \ No newline at end of file +convention = "pep257" diff --git a/server/charm/tests/integration/conftest.py b/server/charm/tests/integration/conftest.py index fb81b813e..5cdc4f016 100644 --- a/server/charm/tests/integration/conftest.py +++ b/server/charm/tests/integration/conftest.py @@ -16,30 +16,13 @@ import logging import os -import sys -import time from pathlib import Path -import jubilant import pytest logger = logging.getLogger(__name__) -@pytest.fixture(scope="module") -def juju(request: pytest.FixtureRequest): - """Create temporary Juju model for running tests.""" - with jubilant.temp_model() as juju: - juju.wait_timeout = 600 - yield juju - - if request.session.testsfailed: - logger.info("Collecting Juju logs...") - time.sleep(0.5) # Wait for Juju to process logs. - log = juju.debug_log(limit=1000) - print(log, end="", file=sys.stderr) - - @pytest.fixture(scope="session") def charm_path(): """Return the path of the charm under test.""" diff --git a/server/charm/tests/integration/test_charm.py b/server/charm/tests/integration/test_charm.py index d89048b82..d028ffc1a 100644 --- a/server/charm/tests/integration/test_charm.py +++ b/server/charm/tests/integration/test_charm.py @@ -17,12 +17,14 @@ from pathlib import Path import jubilant +import pytest from .helpers import APP_NAME, METADATA, MONGODB_CHARM, app_is_up, retry DEFAULT_HTTP_PORT = 5000 +@pytest.mark.juju_setup def test_deploy(charm_path: Path, juju: jubilant.Juju): """Deploy the charm under test. @@ -52,3 +54,10 @@ def test_application_is_up(juju: jubilant.Juju): ip = juju.status().apps[APP_NAME].units[f"{APP_NAME}/0"].address base_url = f"http://{ip}:{DEFAULT_HTTP_PORT}" assert app_is_up(base_url) + + +@pytest.mark.juju_teardown +def test_destroy(juju: jubilant.Juju): + """Tear down the charm under test.""" + juju.remove_application(APP_NAME) + juju.remove_application(MONGODB_CHARM) diff --git a/server/charm/tests/integration/test_nginx_ingress.py b/server/charm/tests/integration/test_nginx_ingress.py index a9cc6947b..42bd3daba 100644 --- a/server/charm/tests/integration/test_nginx_ingress.py +++ b/server/charm/tests/integration/test_nginx_ingress.py @@ -19,6 +19,7 @@ from pathlib import Path import jubilant +import pytest import requests from .helpers import ( @@ -35,6 +36,7 @@ INGRESS_NAME = "ingress" +@pytest.mark.juju_setup def test_deploy(charm_path: Path, juju: jubilant.Juju): """Test deploying the charm under test with ingress relation.""" # Deploy the testflinger charm @@ -86,3 +88,11 @@ def test_ingress_is_up(juju: jubilant.Juju): ) base_url = f"http://{DEFAULT_EXTERNAL_HOSTNAME}" assert app_is_up(base_url, session=session) + + +@pytest.mark.juju_teardown +def test_destroy(juju: jubilant.Juju): + """Tear down the charm under test.""" + juju.remove_application(APP_NAME) + juju.remove_application(MONGODB_CHARM) + juju.remove_application(INGRESS_NAME) diff --git a/server/charm/tests/integration/test_traefik_ingress.py b/server/charm/tests/integration/test_traefik_ingress.py index 7b7febe5d..187c87148 100644 --- a/server/charm/tests/integration/test_traefik_ingress.py +++ b/server/charm/tests/integration/test_traefik_ingress.py @@ -18,6 +18,7 @@ from pathlib import Path import jubilant +import pytest import requests from .helpers import ( @@ -38,6 +39,7 @@ INGRESS_NAME = "traefik" +@pytest.mark.juju_setup def test_deploy(charm_path: Path, juju: jubilant.Juju): """Test deploying the charm under test with traefik ingress relation.""" # Deploy the testflinger charm @@ -132,3 +134,11 @@ def test_ingress_is_up_traefik_hostname(juju: jubilant.Juju): result = app_is_up(base_url, session=session) logger.info("Connectivity test: %s", "PASS" if result else "FAIL") assert result + + +@pytest.mark.juju_teardown +def test_destroy(juju: jubilant.Juju): + """Tear down the charm under test.""" + juju.remove_application(APP_NAME) + juju.remove_application(MONGODB_CHARM) + juju.remove_application(INGRESS_NAME) diff --git a/server/charm/uv.lock b/server/charm/uv.lock index cd2a683a4..0d55b31aa 100644 --- a/server/charm/uv.lock +++ b/server/charm/uv.lock @@ -663,6 +663,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] +[[package]] +name = "pytest-jubilant" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jubilant" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/64/e83181af1004f64a3ce11c7b9770c3152832e02d00e9e7b4f01877325c5f/pytest_jubilant-2.0.1.tar.gz", hash = "sha256:960bb63d216ec746f7644b67c276b059fccdaa258ee7644aaa74058db659edac", size = 16311, upload-time = "2026-04-07T02:08:25.874Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/3c/5c0bc4f54f6905aa7515cf764c3f5d6c85711cdc1a928221f711d5b7532c/pytest_jubilant-2.0.1-py3-none-any.whl", hash = "sha256:df498ee79d6db5ec32baa35a121ab75852dd50f61504a12550ba81cd63a7f1ef", size = 13525, upload-time = "2026-04-07T02:08:24.531Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -791,8 +804,8 @@ dependencies = [ [package.dev-dependencies] integration = [ { name = "jubilant" }, - { name = "ops", extra = ["testing"] }, { name = "pytest" }, + { name = "pytest-jubilant" }, { name = "requests" }, ] lint = [ @@ -816,8 +829,8 @@ requires-dist = [ [package.metadata.requires-dev] integration = [ { name = "jubilant" }, - { name = "ops", extras = ["testing"] }, { name = "pytest" }, + { name = "pytest-jubilant" }, { name = "requests" }, ] lint = [{ name = "ruff" }]