diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 3068d3f..ca07a10 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -13,33 +13,89 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Configure integration test run.""" +"""Configure slurmd operator integration tests.""" +import logging +import os from pathlib import Path +from typing import Union import pytest from helpers import NHC from pytest_operator.plugin import OpsTest +logger = logging.getLogger(__name__) +SLURMCTLD_DIR = Path(os.getenv("SLURMCTLD_DIR", "../slurmctld-operator")) +SLURMDBD_DIR = Path(os.getenv("SLURMDBD_DIR", "../slurmdbd-operator")) + def pytest_addoption(parser) -> None: parser.addoption( - "--charm-base", action="store", default="ubuntu@22.04", help="Charm base to test." + "--charm-base", + action="store", + default="ubuntu@22.04", + help="Charm base version to use for integration tests.", + ) + parser.addoption( + "--use-local", + action="store_true", + default=False, + help="Use SLURM operators located on localhost rather than pull from Charmhub", ) @pytest.fixture(scope="module") def charm_base(request) -> str: """Get slurmd charm base to use.""" - return request.config.getoption("--charm-base") + return request.config.option.charm_base @pytest.fixture(scope="module") -async def slurmd_charm(ops_test: OpsTest): - """Build slurmd charm to use for integration tests.""" +async def slurmd_charm(ops_test: OpsTest) -> Path: + """Pack slurmd charm to use for integration tests.""" return await ops_test.build_charm(".") +@pytest.fixture(scope="module") +async def slurmctld_charm(request, ops_test: OpsTest) -> Union[str, Path]: + """Pack slurmctld charm to use for integration tests when --use-local is specified. + + Returns: + `str` "slurmctld" if --use-local not specified or if SLURMD_DIR does not exist. + """ + if request.config.option.use_local: + logger.info("Using local slurmctld operator rather than pulling from Charmhub") + if SLURMCTLD_DIR.exists(): + return await ops_test.build_charm(SLURMCTLD_DIR) + else: + logger.warning( + f"{SLURMCTLD_DIR} not found. " + f"Defaulting to latest/edge slurmctld operator from Charmhub" + ) + + return "slurmctld" + + +@pytest.fixture(scope="module") +async def slurmdbd_charm(request, ops_test: OpsTest) -> Union[str, Path]: + """Pack slurmdbd charm to use for integration tests when --use-local is specified. + + Returns: + `str` "slurmdbd" if --use-local not specified or if SLURMDBD_DIR does not exist. + """ + if request.config.option.use_local: + logger.info("Using local slurmdbd operator rather than pulling from Charmhub") + if SLURMDBD_DIR.exists(): + return await ops_test.build_charm(SLURMDBD_DIR) + else: + logger.warning( + f"{SLURMDBD_DIR} not found. " + f"Defaulting to latest/edge slurmdbd operator from Charmhub" + ) + + return "slurmdbd" + + def pytest_sessionfinish(session, exitstatus) -> None: """Clean up repository after test session has completed.""" Path(NHC).unlink(missing_ok=True) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index aab7229..754c1d1 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -15,7 +15,7 @@ """Helpers for the slurmd integration tests.""" import logging -import pathlib +from pathlib import Path from typing import Dict from urllib import request @@ -25,9 +25,9 @@ NHC_URL = f"https://github.com/mej/nhc/releases/download/1.4.3/{NHC}" -def get_slurmd_res() -> Dict[str, pathlib.Path]: +async def get_slurmd_res() -> Dict[str, Path]: """Get slurmd resources needed for charm deployment.""" - if not (nhc := pathlib.Path(NHC)).exists(): + if not (nhc := Path(NHC)).exists(): logger.info(f"Getting resource {NHC} from {NHC_URL}") request.urlretrieve(NHC_URL, nhc) diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index b52438b..34d5b0f 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -13,12 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Test slurmd charm against other SLURM charms in the latest/edge channel.""" +"""Test slurmd charm against other SLURM operators.""" import asyncio import logging -import pathlib -from typing import Any, Coroutine import pytest from helpers import get_slurmd_res @@ -31,37 +29,42 @@ SLURMCTLD = "slurmctld" DATABASE = "mysql" ROUTER = "mysql-router" +UNIT_NAME = f"{SLURMD}/0" @pytest.mark.abort_on_fail @pytest.mark.skip_if_deployed @pytest.mark.order(1) async def test_build_and_deploy( - ops_test: OpsTest, slurmd_charm: Coroutine[Any, Any, pathlib.Path], charm_base: str + ops_test: OpsTest, charm_base: str, slurmd_charm, slurmctld_charm, slurmdbd_charm ) -> None: """Test that the slurmd charm can stabilize against slurmctld, slurmdbd and MySQL.""" logger.info(f"Deploying {SLURMD} against {SLURMCTLD}, {SLURMDBD}, and {DATABASE}") - res_slurmd = get_slurmd_res() + # Pack charms and download NHC resource for slurmd operator. + slurmd_res, slurmd, slurmctld, slurmdbd = await asyncio.gather( + get_slurmd_res(), slurmd_charm, slurmctld_charm, slurmdbd_charm + ) + # Deploy the test Charmed SLURM cloud. await asyncio.gather( ops_test.model.deploy( - str(await slurmd_charm), + str(slurmd), application_name=SLURMD, num_units=1, - resources=res_slurmd, + resources=slurmd_res, base=charm_base, ), ops_test.model.deploy( - SLURMCTLD, + str(slurmctld), application_name=SLURMCTLD, config={"proctrack-type": "proctrack/linuxproc"}, - channel="edge", + channel="edge" if isinstance(slurmctld, str) else None, num_units=1, base=charm_base, ), ops_test.model.deploy( - SLURMDBD, + str(slurmdbd), application_name=SLURMDBD, - channel="edge", + channel="edge" if isinstance(slurmdbd, str) else None, num_units=1, base=charm_base, ), @@ -81,7 +84,7 @@ async def test_build_and_deploy( ), ) # Attach resources to slurmd application. - await ops_test.juju("attach-resource", SLURMD, f"nhc={res_slurmd['nhc']}") + await ops_test.juju("attach-resource", SLURMD, f"nhc={slurmd_res['nhc']}") # Set relations for charmed applications. await ops_test.model.integrate(f"{SLURMD}:{SLURMD}", f"{SLURMCTLD}:{SLURMD}") await ops_test.model.integrate(f"{SLURMDBD}:{SLURMDBD}", f"{SLURMCTLD}:{SLURMDBD}") @@ -90,7 +93,7 @@ async def test_build_and_deploy( # 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=[SLURMD], status="active", timeout=1000) - assert ops_test.model.applications[SLURMD].units[0].workload_status == "active" + assert ops_test.model.units.get(UNIT_NAME).workload_status == "active" @pytest.mark.abort_on_fail @@ -98,8 +101,8 @@ async def test_build_and_deploy( async def test_munge_is_active(ops_test: OpsTest): """Test that munge is active.""" logger.info("Checking that munge is active inside Juju unit") - unit = ops_test.model.applications[SLURMD].units[0] - cmd_res = (await unit.ssh(command="systemctl is-active munge")).strip("\n") + slurmd_unit = ops_test.model.units.get(UNIT_NAME) + cmd_res = (await slurmd_unit.ssh(command="systemctl is-active munge")).strip("\n") assert cmd_res == "active" @@ -108,6 +111,6 @@ async def test_munge_is_active(ops_test: OpsTest): async def test_slurmd_is_active(ops_test: OpsTest): """Test that slurmd is active.""" logger.info("Checking that slurmd is active inside Juju unit") - unit = ops_test.model.applications[SLURMD].units[0] - cmd_res = (await unit.ssh(command="systemctl is-active slurmd")).strip("\n") + slurmd_unit = ops_test.model.units.get(UNIT_NAME) + cmd_res = (await slurmd_unit.ssh(command="systemctl is-active slurmd")).strip("\n") assert cmd_res == "active" diff --git a/tox.ini b/tox.ini index 417c7da..a767bde 100644 --- a/tox.ini +++ b/tox.ini @@ -20,6 +20,8 @@ passenv = PYTHONPATH CHARM_BUILD_DIR MODEL_SETTINGS + SLURMCTLD_DIR + SLURMDBD_DIR [testenv:fmt] description = Apply coding style standards to code @@ -57,8 +59,7 @@ commands = [testenv:integration] description = Run integration tests deps = - juju==3.1.0.1 - pylxd==2.3.1 + juju pytest==7.2.0 pytest-operator==0.26.0 pytest-order==1.1.0