diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e9eb660..98a3583 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -53,7 +53,13 @@ jobs: run: tox -e unit integration-test: - name: Integration tests (LXD) + strategy: + fail-fast: true + matrix: + bases: + - ubuntu@20.04 + - ubuntu@22.04 + name: Integration tests (LXD) | ${{ matrix.bases }} runs-on: ubuntu-latest needs: - inclusive-naming-check @@ -68,4 +74,4 @@ jobs: provider: lxd juju-channel: 3.1/stable - name: Run tests - run: tox -e integration + run: tox run -e integration -- --charm-base=${{ matrix.bases }} diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index a67a827..da6ce4e 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -17,12 +17,25 @@ import pathlib +import pytest +from _pytest.config.argparsing import Parser from helpers import ETCD, NHC, VERSION -from pytest import fixture from pytest_operator.plugin import OpsTest -@fixture(scope="module") +def pytest_addoption(parser: Parser) -> None: + parser.addoption( + "--charm-base", action="store", default="ubuntu@22.04", help="Charm base to test." + ) + + +@pytest.fixture(scope="module") +def charm_base(request) -> str: + """Get slurmdbd charm base to use.""" + return request.config.getoption("--charm-base") + + +@pytest.fixture(scope="module") async def slurmrestd_charm(ops_test: OpsTest): """Slurmrestd charm used for integration testing.""" charm = await ops_test.build_charm(".") diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 9b21ee6..07beddd 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -36,10 +36,10 @@ def get_slurmctld_res() -> Dict[str, pathlib.Path]: """Get slurmctld resources needed for charm deployment.""" if not (version := pathlib.Path(VERSION)).exists(): - logger.info(f"Setting resource {VERSION} to value {VERSION_NUM}...") + logger.info(f"Setting resource {VERSION} to value {VERSION_NUM}") version.write_text(VERSION_NUM) if not (etcd := pathlib.Path(ETCD)).exists(): - logger.info(f"Getting resource {ETCD} from {ETCD_URL}...") + logger.info(f"Getting resource {ETCD} from {ETCD_URL}") request.urlretrieve(ETCD_URL, etcd) return {"etcd": etcd} @@ -48,7 +48,7 @@ def get_slurmctld_res() -> Dict[str, pathlib.Path]: def get_slurmd_res() -> Dict[str, pathlib.Path]: """Get slurmd resources needed for charm deployment.""" if not (nhc := pathlib.Path(NHC)).exists(): - logger.info(f"Getting resource {NHC} from {NHC_URL}...") + logger.info(f"Getting resource {NHC} from {NHC_URL}") request.urlretrieve(NHC_URL, nhc) return {"nhc": nhc} diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index 06e1a8c..71b536b 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -27,7 +27,6 @@ logger = logging.getLogger(__name__) -SERIES = ["focal"] SLURMCTLD = "slurmctld" SLURMD = "slurmd" SLURMDBD = "slurmdbd" @@ -38,14 +37,13 @@ @pytest.mark.abort_on_fail @pytest.mark.skip_if_deployed -@pytest.mark.parametrize("series", SERIES) +@pytest.mark.order(1) async def test_build_and_deploy( - ops_test: OpsTest, slurmrestd_charm: Coroutine[Any, Any, pathlib.Path], series: str + ops_test: OpsTest, slurmrestd_charm: Coroutine[Any, Any, pathlib.Path], charm_base: str ) -> None: """Deploy minimal working slurmrestd charm.""" res_slurmd = get_slurmd_res() res_slurmctld = get_slurmctld_res() - await asyncio.gather( # Fetch from charmhub slurmctld ops_test.model.deploy( @@ -54,7 +52,7 @@ async def test_build_and_deploy( channel="edge", num_units=1, resources=res_slurmctld, - series=series, + base=charm_base, ), ops_test.model.deploy( SLURMD, @@ -62,45 +60,45 @@ async def test_build_and_deploy( channel="edge", num_units=1, resources=res_slurmd, - series=series, + base=charm_base, ), ops_test.model.deploy( SLURMDBD, application_name=SLURMDBD, channel="edge", num_units=1, - series=series, + base=charm_base, ), ops_test.model.deploy( str(await slurmrestd_charm), application_name=SLURMRESTD, num_units=1, - series=series, + base=charm_base, ), ops_test.model.deploy( ROUTER, application_name=f"{SLURMDBD}-{ROUTER}", channel="dpe/edge", - num_units=1, - series=series, + num_units=0, + base=charm_base, ), ops_test.model.deploy( DATABASE, application_name=DATABASE, - channel="edge", + channel="8.0/edge", num_units=1, - series="jammy", + base="ubuntu@22.04", ), ) # Attach resources to charms. await ops_test.juju("attach-resource", SLURMCTLD, f"etcd={res_slurmctld['etcd']}") await ops_test.juju("attach-resource", SLURMD, f"nhc={res_slurmd['nhc']}") # Set relations for charmed applications. - await ops_test.model.relate(f"{SLURMCTLD}:{SLURMDBD}", f"{SLURMDBD}:{SLURMDBD}") - await ops_test.model.relate(f"{SLURMDBD}-{ROUTER}:backend-database", f"{DATABASE}:database") - await ops_test.model.relate(f"{SLURMDBD}:database", f"{SLURMDBD}-{ROUTER}:database") - await ops_test.model.relate(f"{SLURMRESTD}:slurmrestd", f"{SLURMCTLD}:slurmrestd") - await ops_test.model.add_relation(f"{SLURMD}:{SLURMD}", f"{SLURMCTLD}:{SLURMD}") + await ops_test.model.integrate(f"{SLURMCTLD}:{SLURMDBD}", f"{SLURMDBD}:{SLURMDBD}") + await ops_test.model.integrate(f"{SLURMDBD}-{ROUTER}:backend-database", f"{DATABASE}:database") + await ops_test.model.integrate(f"{SLURMDBD}:database", f"{SLURMDBD}-{ROUTER}:database") + await ops_test.model.integrate(f"{SLURMRESTD}:slurmrestd", f"{SLURMCTLD}:slurmrestd") + await ops_test.model.integrate(f"{SLURMD}:{SLURMD}", f"{SLURMCTLD}:{SLURMD}") # Reduce the update status frequency to accelerate the triggering of deferred events. async with ops_test.fast_forward(): await ops_test.model.wait_for_idle(apps=[SLURMRESTD], status="active", timeout=1000) @@ -108,6 +106,7 @@ async def test_build_and_deploy( @pytest.mark.abort_on_fail +@pytest.mark.order(2) @tenacity.retry( wait=tenacity.wait.wait_exponential(multiplier=2, min=1, max=30), stop=tenacity.stop_after_attempt(3), @@ -115,7 +114,7 @@ async def test_build_and_deploy( ) async def test_munge_is_active(ops_test: OpsTest) -> None: """Test that munge is active.""" - logger.info("Checking that munge is active inside Juju unit...") + logger.info("Checking that munge is active inside Juju unit") slurmctld_unit = ops_test.model.applications[SLURMCTLD].units[0] res = (await slurmctld_unit.ssh("systemctl is-active munge")).strip("\n") assert res == "active" @@ -125,6 +124,7 @@ async def test_munge_is_active(ops_test: OpsTest) -> None: # systemd service failing. Error is "unable to get address" and "Temporary failure in # name resolution". @pytest.mark.xfail +@pytest.mark.order(3) @tenacity.retry( wait=tenacity.wait.wait_exponential(multiplier=2, min=1, max=30), stop=tenacity.stop_after_attempt(3), @@ -132,7 +132,7 @@ async def test_munge_is_active(ops_test: OpsTest) -> None: ) async def test_slurmrestd_is_active(ops_test: OpsTest) -> None: """Test that slurmrestd is active.""" - logger.info("Checking that slurmrestd is active inside Juju unit...") + logger.info("Checking that slurmrestd is active inside Juju unit") unit = ops_test.model.applications[SLURMRESTD].units[0] cmd_res = (await unit.ssh("systemctl is-active slurmrestd")).strip("\n") assert cmd_res == "active" diff --git a/tox.ini b/tox.ini index fee5cf1..7bee544 100644 --- a/tox.ini +++ b/tox.ini @@ -58,14 +58,14 @@ description = Run integration tests deps = juju==3.1.0.1 pytest==7.2.0 - pytest-operator==0.22.0 - tenacity==8.1.0 + pytest-operator==0.26.0 + pytest-order==1.1.0 + tenacity==8.2.2 + -r{toxinidir}/requirements.txt commands = pytest -v \ - -s \ - --tb native \ - --ignore={[vars]tst_path}unit \ - --log-cli-level=INFO \ - --model controller \ - --keep-models \ - {posargs} + -s \ + --tb native \ + --log-cli-level=INFO \ + {[vars]tst_path}integration \ + {posargs}