diff --git a/bind-operator/pyproject.toml b/bind-operator/pyproject.toml index 820a6d68..d181e145 100644 --- a/bind-operator/pyproject.toml +++ b/bind-operator/pyproject.toml @@ -57,6 +57,9 @@ disable = ["too-many-arguments"] [tool.pytest.ini_options] minversion = "6.0" log_cli_level = "INFO" +markers = [ + "abort_on_fail: abort test execution on first failure in marked test", +] # Linting tools configuration [tool.ruff] diff --git a/bind-operator/tests/conftest.py b/bind-operator/tests/conftest.py index 68f66b86..9711b36d 100644 --- a/bind-operator/tests/conftest.py +++ b/bind-operator/tests/conftest.py @@ -8,7 +8,7 @@ def pytest_addoption(parser): """Parse additional pytest options. Args: - parser: Pytest parser. + parser: the parser """ parser.addoption("--charm-file", action="store", default=None) parser.addoption( @@ -17,3 +17,5 @@ def pytest_addoption(parser): default=False, help="This will skip deployment of the charms. Useful for local testing.", ) + parser.addoption("--model", action="store", default=None) + parser.addoption("--keep-models", action="store_true", default=False) diff --git a/bind-operator/tests/integration/conftest.py b/bind-operator/tests/integration/conftest.py index 1b3cd820..0e8961eb 100644 --- a/bind-operator/tests/integration/conftest.py +++ b/bind-operator/tests/integration/conftest.py @@ -5,36 +5,75 @@ import pathlib import subprocess # nosec B404 +import typing +import jubilant import pytest import pytest_asyncio import yaml -from pytest_operator.plugin import Model, OpsTest -@pytest.fixture(scope="module", name="charmcraft") -def fixture_charmcraft(): - """Provide charmcraft.""" - yield yaml.safe_load(pathlib.Path("./charmcraft.yaml").read_text(encoding="UTF-8")) +@pytest.fixture(scope="module", name="juju") +def juju_fixture( + request: pytest.FixtureRequest, +) -> typing.Generator[jubilant.Juju, None, None]: + """Pytest fixture that wraps jubilant.juju. + Args: + request: fixture request + + Returns: + juju + """ + + def show_debug_log(juju: jubilant.Juju): + """Show debug log. + + Args: + juju: Jubilant.juju + """ + if request.session.testsfailed: + log = juju.debug_log(limit=1000) + print(log, end="") + + use_existing = request.config.getoption("--use-existing", default=False) + if use_existing: + juju = jubilant.Juju() + yield juju + show_debug_log(juju) + return + + model = request.config.getoption("--model", default=None) + if model: + juju = jubilant.Juju(model=model) + yield juju + show_debug_log(juju) + return + + keep_models = request.config.getoption("--keep-models", default=False) + with jubilant.temp_model(keep=keep_models) as juju: + juju.wait_timeout = 10 * 60 + yield juju + show_debug_log(juju) + return -@pytest.fixture(scope="module", name="app_name") -def fixture_app_name(charmcraft): - """Provide app name from the charmcraft.""" - yield charmcraft["name"] + +@pytest.fixture(scope="module", name="metadata") +def fixture_metadata(): + """Provide charm metadata.""" + yield yaml.safe_load(pathlib.Path("./charmcraft.yaml").read_text(encoding="UTF-8")) -@pytest.fixture(scope="module", name="model") -def model_fixture(ops_test: OpsTest) -> Model: - """Juju model API client.""" - assert ops_test.model - return ops_test.model +@pytest.fixture(scope="module", name="app_name") +def fixture_app_name(metadata): + """Provide app name from the metadata.""" + yield metadata["name"] @pytest.fixture(scope="module", name="charm_file") def charm_file_fixture(app_name, pytestconfig: pytest.Config): """Pytest fixture that packs the charm and returns the filename, or --charm-file if set.""" - charm_file = pytestconfig.getoption("--charm-file") + charm_file = pytestconfig.getoption("--charm-file", default=None) if charm_file: yield charm_file return @@ -54,19 +93,18 @@ def charm_file_fixture(app_name, pytestconfig: pytest.Config): @pytest_asyncio.fixture(scope="module", name="app") async def app_fixture( + juju: jubilant.Juju, app_name: str, pytestconfig: pytest.Config, - model: Model, charm_file: str, ): """Build the charm and deploys it.""" use_existing = pytestconfig.getoption("--use-existing", default=False) if use_existing: - yield model.applications[app_name] + yield app_name return - application = await model.deploy(f"./{charm_file}", application_name=app_name, resources={}) - - await model.wait_for_idle(apps=[application.name], status="active") + juju.deploy(charm_file, app_name, resources={}) + juju.wait(lambda status: jubilant.all_active(status, app_name)) - yield application + yield app_name diff --git a/bind-operator/tests/integration/helpers.py b/bind-operator/tests/integration/helpers.py index 262aa90e..5228fe93 100644 --- a/bind-operator/tests/integration/helpers.py +++ b/bind-operator/tests/integration/helpers.py @@ -12,9 +12,7 @@ import tempfile import time -import juju.application -import ops -from pytest_operator.plugin import OpsTest +import jubilant import constants import models @@ -61,31 +59,31 @@ def _generate_random_filename(length: int = 24, extension: str = "") -> str: return random_string -async def run_on_unit(ops_test: OpsTest, unit_name: str, command: str) -> str: +async def run_on_unit(juju: jubilant.Juju, unit_name: str, command: str) -> str: """Run a command on a specific unit. Args: - ops_test: The ops test framework instance + juju: The jubilant Juju instance unit_name: The name of the unit to run the command on command: The command to run Returns: - the command output if it succeeds, otherwise raises an exception. + the command output if it succeeds Raises: ExecutionError: if the command was not successful """ - complete_command = ["exec", "--unit", unit_name, "--", *command.split()] - return_code, stdout, stderr = await ops_test.juju(*complete_command) - if return_code != 0: - raise ExecutionError(f"Command {command} failed with code {return_code}: {stderr}") - return stdout + try: + task = juju.exec(command, unit=unit_name) + return task.stdout + except Exception as e: + raise ExecutionError(f"Command {command} failed: {e}") from e async def push_to_unit( *, - ops_test: OpsTest, - unit: ops.model.Unit, + juju: jubilant.Juju, + unit_name: str, source: str, destination: str, user: str = "root", @@ -95,63 +93,57 @@ async def push_to_unit( """Push a source file to the chosen unit. Args: - ops_test: The ops test framework instance - unit: The unit to push the file to + juju: The jubilant Juju instance + unit_name: The name of the unit source: the content of the file destination: the path of the file on the unit user: the user that owns the file group: the group that owns the file mode: the mode of the file """ - assert unit is not None _, temp_path = tempfile.mkstemp() with open(temp_path, "w", encoding="utf-8") as fd: fd.writelines(source) temp_filename_on_workload = _generate_random_filename() - # unit does have scp_to - await unit.scp_to(source=temp_path, destination=temp_filename_on_workload) # type: ignore + juju.scp(temp_path, f"{unit_name}:{temp_filename_on_workload}") + mv_cmd = f"sudo mv -f /home/ubuntu/{temp_filename_on_workload} {destination}" - await run_on_unit(ops_test, unit.name, mv_cmd) + await run_on_unit(juju, unit_name, mv_cmd) chown_cmd = f"sudo chown {user}:{group} {destination}" - await run_on_unit(ops_test, unit.name, chown_cmd) + await run_on_unit(juju, unit_name, chown_cmd) chmod_cmd = f"sudo chmod {mode} {destination}" - await run_on_unit(ops_test, unit.name, chmod_cmd) + await run_on_unit(juju, unit_name, chmod_cmd) async def dispatch_to_unit( - ops_test: OpsTest, - unit: ops.model.Unit, + juju: jubilant.Juju, + unit_name: str, hook_name: str, ): """Dispatch a hook to the chosen unit. Args: - ops_test: The ops test framework instance - unit: The unit to push the file to + juju: The jubilant Juju instance + unit_name: The name of the unit hook_name: the hook name """ - await ops_test.juju( - "exec", - "--unit", - unit.name, - "--", - f"export JUJU_DISPATCH_PATH=hooks/{hook_name}; ./dispatch", - ) + cmd = f"export JUJU_DISPATCH_PATH=hooks/{hook_name}; ./dispatch" + juju.exec(cmd, unit=unit_name) async def generate_anycharm_relation( - app: juju.application.Application, - ops_test: OpsTest, + app_name: str, + juju: jubilant.Juju, any_charm_name: str, dns_entries: list[models.DnsEntry], machine: str | None, ): - """Deploy any-charm with a wanted DNS entries config and integrate it to the bind app. + """Deploy any-charm with wanted DNS entries config and integrate to bind app. Args: - app: Deployed bind-operator app - ops_test: The ops test framework instance + app_name: Deployed bind-operator app name + juju: The jubilant Juju instance any_charm_name: Name of the to be deployed any-charm dns_entries: List of DNS entries for any-charm machine: The machine to deploy the any-charm onto @@ -167,65 +159,66 @@ async def generate_anycharm_relation( "dns_record.py": dns_record_content, } + config = { + "src-overwrite": json.dumps(any_charm_src_overwrite), + "python-packages": "pydantic==2.7.1\n", + } + # We deploy https://charmhub.io/any-charm and inject the any_charm.py behavior # See https://github.com/canonical/any-charm on how to use any-charm - assert ops_test.model - args = { - "application_name": any_app_name, - "channel": "beta", - "config": { - "src-overwrite": json.dumps(any_charm_src_overwrite), - "python-packages": "pydantic==2.7.1\n", - }, - } if machine is not None: - args["to"] = machine - any_charm = await ops_test.model.deploy("any-charm", storage=None, **args) - await ops_test.model.wait_for_idle(apps=[any_charm.name]) - await ops_test.model.add_relation( - f"{any_charm.name}:require-dns-record", f"{app.name}:dns-record" - ) - await change_anycharm_relation(ops_test, any_charm.units[0], dns_entries) + juju.deploy("any-charm", any_app_name, channel="beta", config=config, to=machine) + else: + juju.deploy("any-charm", any_app_name, channel="beta", config=config) + + juju.wait(jubilant.all_agents_idle) + + juju.integrate(f"{any_app_name}:require-dns-record", f"{app_name}:dns-record") + + # Get unit name and change relation data + any_units = juju.status().get_units(any_app_name) + any_unit_name = list(any_units.keys())[0] + await change_anycharm_relation(juju, any_unit_name, dns_entries) async def change_anycharm_relation( - ops_test: OpsTest, - anyapp_unit: ops.model.Unit, + juju: jubilant.Juju, + anyapp_unit_name: str, dns_entries: list[models.DnsEntry], ): - """Change the relation of a anyapp_unit with the bind operator. + """Change the relation of an anyapp_unit with the bind operator. Args: - ops_test: The ops test framework instance - anyapp_unit: anyapp unit who's relation will change + juju: The jubilant Juju instance + anyapp_unit_name: anyapp unit name whose relation will change dns_entries: List of DNS entries for any-charm """ await push_to_unit( - ops_test=ops_test, - unit=anyapp_unit, + juju=juju, + unit_name=anyapp_unit_name, source=json.dumps([e.model_dump(mode="json") for e in dns_entries]), destination="/srv/dns_entries.json", ) # fire reload-data event - assert ops_test.model + model_name = juju.model or juju.status().model.name cmd = ( "JUJU_DISPATCH_PATH=hooks/reload-data " - f"JUJU_MODEL_NAME={ops_test.model.name} " - f"JUJU_UNIT_NAME={anyapp_unit.name} ./dispatch" + f"JUJU_MODEL_NAME={model_name} " + f"JUJU_UNIT_NAME={anyapp_unit_name} ./dispatch" ) - await run_on_unit(ops_test, anyapp_unit.name, cmd) - await ops_test.model.wait_for_idle() + await run_on_unit(juju, anyapp_unit_name, cmd) + juju.wait(jubilant.all_agents_idle) async def dig_query( - ops_test: OpsTest, unit: ops.model.Unit, cmd: str, retry: bool = False, wait: int = 5 + juju: jubilant.Juju, unit_name: str, cmd: str, retry: bool = False, wait: int = 5 ) -> str: """Query a DnsEntry with dig. Args: - ops_test: The ops test framework instance - unit: Unit to be used to launch the command + juju: The jubilant Juju instance + unit_name: Unit name to be used to launch the command cmd: Dig command to perform retry: If the dig request should be retried wait: duration in seconds to wait between retries @@ -234,7 +227,7 @@ async def dig_query( """ result: str = "" for _ in range(5): - result = (await run_on_unit(ops_test, unit.name, f"dig {cmd}")).strip() + result = (await run_on_unit(juju, unit_name, f"dig {cmd}")).strip() if (result.strip() != "" and "timed out" not in result) or not retry: break time.sleep(wait) @@ -242,26 +235,29 @@ async def dig_query( return result -async def get_active_unit( - app: juju.application.Application, ops_test: OpsTest -) -> ops.model.Unit | None: - """Get the current active unit if it exists. +async def get_active_unit(app_name: str, juju: jubilant.Juju) -> str | None: + """Get the current active unit name if it exists. Args: - app: Application to search for an active unit - ops_test: The ops test framework instance + app_name: Application name to search for an active unit + juju: The jubilant Juju instance Returns: - The current active unit if it exists, None otherwise + The current active unit name if it exists, None otherwise """ - for unit in app.units: - # We take `[1]` because `[0]` is the return code of the process - data = json.loads((await ops_test.juju("show-unit", unit.name, "--format", "json"))[1]) - relations = data[unit.name]["relation-info"] + units = juju.status().get_units(app_name) + for unit_name in units: + # Use juju CLI to get detailed unit info + output = juju.cli("show-unit", unit_name, "--format", "json") + data = json.loads(output) + relations = data[unit_name]["relation-info"] + + peer_relation = None for relation in relations: if relation["endpoint"] == "bind-peers": peer_relation = relation break + if peer_relation is None: continue if "active-unit" not in peer_relation["application-data"]: @@ -270,75 +266,77 @@ async def get_active_unit( peer_relation["local-unit"]["data"]["ingress-address"] == peer_relation["application-data"]["active-unit"] ): - return unit + return unit_name return None -async def check_if_active_unit_exists( - app: juju.application.Application, ops_test: OpsTest -) -> bool: +async def check_if_active_unit_exists(app_name: str, juju: jubilant.Juju) -> bool: """Check if an active unit exists and is reachable. Args: - app: Application to search for an active unit - ops_test: The ops test framework instance + app_name: Application name to search for an active unit + juju: The jubilant Juju instance Returns: - The current active unit if it exists, None otherwise + True if active unit exists and is reachable """ - unit = app.units[0] - # We take `[1]` because `[0]` is the return code of the process - data = json.loads((await ops_test.juju("show-unit", unit.name, "--format", "json"))[1]) - relations = data[unit.name]["relation-info"] + units = juju.status().get_units(app_name) + if not units: + return False + + unit_name = list(units.keys())[0] + output = juju.cli("show-unit", unit_name, "--format", "json") + data = json.loads(output) + relations = data[unit_name]["relation-info"] + + peer_relation = None for relation in relations: if relation["endpoint"] == "bind-peers": peer_relation = relation break + if peer_relation is None: return False if "active-unit" not in peer_relation["application-data"]: return False + active_unit = peer_relation["application-data"]["active-unit"] if not active_unit: return False status = await dig_query( - ops_test, - unit, + juju, + unit_name, f"@{active_unit} status.{constants.ZONE_SERVICE_NAME} TXT +short", retry=True, wait=5, ) - if status != '"ok"': - return False - return True + return status == '"ok"' -async def force_reload_bind(ops_test: OpsTest, unit: ops.model.Unit): +async def force_reload_bind(juju: jubilant.Juju, unit_name: str): """Force reload bind. Args: - ops_test: The ops test framework instance - unit: the bind unit to force reload + juju: The jubilant Juju instance + unit_name: the bind unit name to force reload """ restart_cmd = f"sudo snap restart --reload {constants.DNS_SNAP_NAME}" - await run_on_unit(ops_test, unit.name, restart_cmd) + await run_on_unit(juju, unit_name, restart_cmd) -async def get_unit_ips(ops_test: OpsTest, unit: ops.model.Unit) -> list[str]: +async def get_unit_ips(juju: jubilant.Juju, app_name: str) -> list[str]: """Retrieve unit ip addresses. Args: - ops_test: The ops test framework instance - unit: the bind unit to force reload + juju: The jubilant Juju instance + app_name: Application name Returns: - list of units ip addresses. + list of unit ip addresses. """ - _, status, _ = await ops_test.juju("status", "--format", "json") - status = json.loads(status) - units = status["applications"][unit.name.split("/")[0]]["units"] + units = juju.status().get_units(app_name) ip_list = [] - for key in sorted(units.keys(), key=lambda n: int(n.split("/")[-1])): - ip_list.append(units[key]["public-address"]) + for unit_name in sorted(units.keys(), key=lambda n: int(n.split("/")[-1])): + ip_list.append(units[unit_name].public_address) return ip_list diff --git a/bind-operator/tests/integration/test_charm.py b/bind-operator/tests/integration/test_charm.py index 6b39550a..24900eb0 100644 --- a/bind-operator/tests/integration/test_charm.py +++ b/bind-operator/tests/integration/test_charm.py @@ -8,10 +8,8 @@ import time import dns.resolver -import juju.application -import ops +import jubilant import pytest -from pytest_operator.plugin import Model, OpsTest import constants import models @@ -22,30 +20,31 @@ @pytest.mark.asyncio @pytest.mark.abort_on_fail -async def test_lifecycle(app: juju.application.Application, ops_test: OpsTest): +async def test_lifecycle(app: str, juju: jubilant.Juju): """ arrange: build and deploy the charm. act: nothing. assert: that the charm ends up in an active state. """ - unit = app.units[0] + units = juju.status().get_units(app) + unit_name = list(units.keys())[0] + unit_status = units[unit_name] - assert unit.workload_status == ops.model.ActiveStatus.name + assert unit_status.workload_status.current == "active" status = await tests.integration.helpers.dig_query( - ops_test, - unit, + juju, + unit_name, f"@127.0.0.1 status.{constants.ZONE_SERVICE_NAME} TXT +short", retry=True, wait=5, ) assert status == '"ok"' - await tests.integration.helpers.dispatch_to_unit(ops_test, unit, "stop") + await tests.integration.helpers.dispatch_to_unit(juju, unit_name, "stop") time.sleep(5) - _, service_status, _ = await ops_test.juju( - "exec", "--unit", unit.name, "snap services charmed-bind" - ) + + service_status = juju.exec("snap services charmed-bind", unit=unit_name).stdout logger.info(service_status) assert "inactive" in service_status @@ -55,24 +54,24 @@ async def test_lifecycle(app: juju.application.Application, ops_test: OpsTest): logger.info(service_status) assert "inactive" in service_status - await tests.integration.helpers.dispatch_to_unit(ops_test, unit, "start") + await tests.integration.helpers.dispatch_to_unit(juju, unit_name, "start") time.sleep(5) - _, service_status, _ = await ops_test.juju( - "exec", "--unit", unit.name, "snap services charmed-bind" - ) + + service_status = juju.exec("snap services charmed-bind", unit=unit_name).stdout logger.info(service_status) assert "active" in service_status @pytest.mark.asyncio @pytest.mark.abort_on_fail -async def test_basic_dns_config(app: juju.application.Application, ops_test: OpsTest): +async def test_basic_dns_config(app: str, juju: jubilant.Juju): """ arrange: build, deploy the charm and change its configuration. act: request the test domain. assert: the output of the dig command is the expected one """ - unit = app.units[0] + units = juju.status().get_units(app) + unit_name = list(units.keys())[0] test_zone_def = f"""zone "dns.test" IN {{ type primary; @@ -82,11 +81,11 @@ async def test_basic_dns_config(app: juju.application.Application, ops_test: Ops """ # We need to stop the dispatch-reload-bind timer for this test stop_timer_cmd = "sudo systemctl stop dispatch-reload-bind.timer" - await tests.integration.helpers.run_on_unit(ops_test, unit.name, stop_timer_cmd) + await tests.integration.helpers.run_on_unit(juju, unit_name, stop_timer_cmd) await tests.integration.helpers.push_to_unit( - ops_test=ops_test, - unit=unit, + juju=juju, + unit_name=unit_name, source=test_zone_def, destination=f"{constants.DNS_CONFIG_DIR}/named.conf.local", ) @@ -104,24 +103,24 @@ async def test_basic_dns_config(app: juju.application.Application, ops_test: Ops @ IN TXT "this-is-a-test" """ await tests.integration.helpers.push_to_unit( - ops_test=ops_test, - unit=unit, + juju=juju, + unit_name=unit_name, source=test_zone, destination=f"{constants.DNS_CONFIG_DIR}/db.dns.test", ) restart_cmd = f"sudo snap restart --reload {constants.DNS_SNAP_NAME}" - await tests.integration.helpers.run_on_unit(ops_test, unit.name, restart_cmd) + await tests.integration.helpers.run_on_unit(juju, unit_name, restart_cmd) assert ( await tests.integration.helpers.run_on_unit( - ops_test, unit.name, "dig @127.0.0.1 dns.test TXT +short" + juju, unit_name, "dig @127.0.0.1 dns.test TXT +short" ) ).strip() == '"this-is-a-test"' # Restart the timer for the subsequent tests start_timer_cmd = "sudo systemctl start dispatch-reload-bind.timer" - await tests.integration.helpers.run_on_unit(ops_test, unit.name, start_timer_cmd) + await tests.integration.helpers.run_on_unit(juju, unit_name, start_timer_cmd) @pytest.mark.parametrize( @@ -156,7 +155,7 @@ async def test_basic_dns_config(app: juju.application.Application, ops_test: Ops ), ], ), - ops.model.ActiveStatus, + "active", ), ( ( @@ -181,7 +180,7 @@ async def test_basic_dns_config(app: juju.application.Application, ops_test: Ops ), ], ), - ops.model.BlockedStatus, + "blocked", ), ( ( @@ -216,17 +215,17 @@ async def test_basic_dns_config(app: juju.application.Application, ops_test: Ops ), ], ), - ops.model.ActiveStatus, + "active", ), ), ) @pytest.mark.asyncio @pytest.mark.abort_on_fail +# pylint: disable=too-many-locals async def test_dns_record_relation( - app: juju.application.Application, - ops_test: OpsTest, - model: Model, - status: ops.model.StatusBase, + app: str, + juju: jubilant.Juju, + status: str, integration_datasets: tuple[list[models.DnsEntry]], ): """ @@ -235,36 +234,43 @@ async def test_dns_record_relation( assert: bind-operator should have the correct status and respond to dig queries """ # Remove previously deployed instances of any-app - for any_app_number in range(10): + apps = juju.status().apps + for any_app_number in range(5): anyapp_name = f"anyapp-t{any_app_number}" - if anyapp_name in model.applications: - await model.remove_application(anyapp_name, block_until_done=True) + if anyapp_name in apps: + juju.remove_application(anyapp_name) + juju.wait(jubilant.all_agents_idle) + # Start by deploying the any-app instances and integrate them with the bind charm any_app_number = 0 for integration_data in integration_datasets: anyapp_name = f"anyapp-t{any_app_number}" await tests.integration.helpers.generate_anycharm_relation( app, - ops_test, + juju, anyapp_name, integration_data, None, ) any_app_number += 1 - await model.wait_for_idle(idle_period=30) - await tests.integration.helpers.force_reload_bind(ops_test, app.units[0]) - await model.wait_for_idle(idle_period=30) + juju.wait(jubilant.all_agents_idle, timeout=30) + + units = juju.status().get_units(app) + unit_name = list(units.keys())[0] + await tests.integration.helpers.force_reload_bind(juju, unit_name) + juju.wait(jubilant.all_agents_idle, timeout=30) # Test the status of the bind-operator instance - assert app.units[0].workload_status == status.name + unit_status = juju.status().get_units(app)[unit_name] + assert unit_status.workload_status.current == status # Test if the records give the correct results # Do that only if we have an active status - if status == ops.model.ActiveStatus: + if status == "active": for integration_data in integration_datasets: for entry in integration_data: - ips = await tests.integration.helpers.get_unit_ips(ops_test, app.units[0]) + ips = await tests.integration.helpers.get_unit_ips(juju, app) logger.info(ips) for ip in ips: # Create a DNS resolver diff --git a/bind-operator/tests/integration/test_multi_units.py b/bind-operator/tests/integration/test_multi_units.py index 875367b1..7d85d089 100644 --- a/bind-operator/tests/integration/test_multi_units.py +++ b/bind-operator/tests/integration/test_multi_units.py @@ -6,9 +6,8 @@ import logging -import juju.application +import jubilant import pytest -from pytest_operator.plugin import Model, OpsTest import models import tests.integration.helpers @@ -19,9 +18,8 @@ @pytest.mark.asyncio @pytest.mark.abort_on_fail async def test_multi_units( - app: juju.application.Application, - ops_test: OpsTest, - model: Model, + app: str, + juju: jubilant.Juju, ): """ arrange: given deployed bind-operator @@ -29,15 +27,17 @@ async def test_multi_units( assert: there always is an active unit """ # Remove previously deployed instances of any-app + apps = juju.status().apps for any_app_number in range(10): anyapp_name = f"anyapp-t{any_app_number}" - if anyapp_name in model.applications: - await model.remove_application(anyapp_name, block_until_done=True) + if anyapp_name in apps: + juju.remove_application(anyapp_name) + juju.wait(jubilant.all_agents_idle) # Start by deploying the any-app instance with the domain to check await tests.integration.helpers.generate_anycharm_relation( app, - ops_test, + juju, "anyapp-t1", [ models.DnsEntry( @@ -51,17 +51,18 @@ async def test_multi_units( ], None, ) - await model.wait_for_idle() + juju.wait(jubilant.all_agents_idle) # Start by testing that everything is fine - assert await tests.integration.helpers.check_if_active_unit_exists(app, ops_test) - for unit in app.units: - await tests.integration.helpers.force_reload_bind(ops_test, unit) - await model.wait_for_idle() + assert await tests.integration.helpers.check_if_active_unit_exists(app, juju) + units = juju.status().get_units(app) + for unit_name in units.keys(): + await tests.integration.helpers.force_reload_bind(juju, unit_name) + juju.wait(jubilant.all_agents_idle) assert ( await tests.integration.helpers.dig_query( - ops_test, - unit, + juju, + unit_name, "@127.0.0.1 admin.dns.test A +short", retry=True, wait=5, @@ -70,18 +71,17 @@ async def test_multi_units( ), "Initial test failed" # add a unit and verify that everything goes well - assert ops_test.model is not None - add_unit_cmd = f"add-unit {app.name} --model={ops_test.model.info.name}" - await ops_test.juju(*(add_unit_cmd.split(" "))) - await model.wait_for_idle() - assert await tests.integration.helpers.check_if_active_unit_exists(app, ops_test) - for unit in app.units: - await tests.integration.helpers.force_reload_bind(ops_test, unit) - await model.wait_for_idle() + juju.add_unit(app) + juju.wait(jubilant.all_agents_idle) + assert await tests.integration.helpers.check_if_active_unit_exists(app, juju) + units = juju.status().get_units(app) + for unit_name in units.keys(): + await tests.integration.helpers.force_reload_bind(juju, unit_name) + juju.wait(jubilant.all_agents_idle) assert ( await tests.integration.helpers.dig_query( - ops_test, - unit, + juju, + unit_name, "@127.0.0.1 admin.dns.test A +short", retry=True, wait=5, @@ -91,10 +91,11 @@ async def test_multi_units( # Change the domain requested by any-app anyapp_name = "anyapp-t1" - anyapp = model.applications[anyapp_name] + anyapp_units = juju.status().get_units(anyapp_name) + anyapp_unit_name = list(anyapp_units.keys())[0] await tests.integration.helpers.change_anycharm_relation( - ops_test, - anyapp.units[0], + juju, + anyapp_unit_name, [ models.DnsEntry( domain="dns.test", @@ -106,14 +107,15 @@ async def test_multi_units( ), ], ) - await model.wait_for_idle() - for unit in app.units: - await tests.integration.helpers.force_reload_bind(ops_test, unit) - await model.wait_for_idle() + juju.wait(jubilant.all_agents_idle) + units = juju.status().get_units(app) + for unit_name in units.keys(): + await tests.integration.helpers.force_reload_bind(juju, unit_name) + juju.wait(jubilant.all_agents_idle) assert ( await tests.integration.helpers.dig_query( - ops_test, - unit, + juju, + unit_name, "@127.0.0.1 admin.dns.test A +short", retry=True, wait=5, @@ -122,21 +124,19 @@ async def test_multi_units( ), "Failed after changing DNS request" # remove the active unit and check that we're still all right - active_unit = await tests.integration.helpers.get_active_unit(app, ops_test) - assert active_unit is not None - remove_unit_cmd = ( - f"remove-unit {active_unit.name} --model={ops_test.model.info.name} --no-prompt" - ) - await ops_test.juju(*(remove_unit_cmd.split(" "))) - await model.wait_for_idle() - assert await tests.integration.helpers.check_if_active_unit_exists(app, ops_test) - for unit in app.units: - await tests.integration.helpers.force_reload_bind(ops_test, unit) - await model.wait_for_idle() + active_unit_name = await tests.integration.helpers.get_active_unit(app, juju) + assert active_unit_name is not None + juju.remove_unit(active_unit_name, force=True) + juju.wait(jubilant.all_agents_idle) + assert await tests.integration.helpers.check_if_active_unit_exists(app, juju) + units = juju.status().get_units(app) + for unit_name in units.keys(): + await tests.integration.helpers.force_reload_bind(juju, unit_name) + juju.wait(jubilant.all_agents_idle) assert ( await tests.integration.helpers.dig_query( - ops_test, - unit, + juju, + unit_name, "@127.0.0.1 admin.dns.test A +short", retry=True, wait=5, diff --git a/bind-operator/tests/integration/test_scale.py b/bind-operator/tests/integration/test_scale.py index 34034fe0..d4f6d1b2 100644 --- a/bind-operator/tests/integration/test_scale.py +++ b/bind-operator/tests/integration/test_scale.py @@ -8,13 +8,11 @@ # pylint: disable=too-many-arguments import asyncio -import json import logging import time -import juju.application +import jubilant import pytest -from pytest_operator.plugin import Model, OpsTest import models import tests.integration.helpers @@ -24,9 +22,8 @@ async def deploy_any_charm( *, - app: juju.application.Application, - ops_test: OpsTest, - model: Model, + app_name: str, + juju: jubilant.Juju, any_app_number: int, machines: list[str] | None, entries: list[models.DnsEntry], @@ -34,9 +31,8 @@ async def deploy_any_charm( """Deploy any charm and integrate it to the bind-operator. Args: - app: Deployed bind-operator app - ops_test: The ops test framework instance - model: The ops model instance + app_name: Deployed bind-operator app name + juju: The jubilant Juju instance any_app_number: Number of the to be deployed any-charm machines: The machines to deploy the any-charm onto entries: List of DNS entries for any-charm @@ -47,11 +43,12 @@ async def deploy_any_charm( else: machine = None logger.info("Deploying %s on %s", anyapp_name, machine) - if anyapp_name in model.applications: + apps = juju.status().apps + if anyapp_name in apps: return await tests.integration.helpers.generate_anycharm_relation( - app, - ops_test, + app_name, + juju, anyapp_name, entries, machine=machine, @@ -62,9 +59,8 @@ async def deploy_any_charm( @pytest.mark.abort_on_fail @pytest.mark.skip(reason="Scaling test") async def test_lots_of_applications( - app: juju.application.Application, - ops_test: OpsTest, - model: Model, + app: str, + juju: jubilant.Juju, ): """ arrange: build and deploy the charm. @@ -73,25 +69,19 @@ async def test_lots_of_applications( """ batch_number = 10 machines_number = 20 - status = await ops_test.juju("status", "--format", "json") - data = json.loads(status[1]) + status = juju.status() machines_available = ( - sum( - 1 - for machine in data["machines"].values() - if machine["machine-status"]["current"] == "running" - ) + sum(1 for machine in status.machines.values() if machine.juju_status.current == "running") - 1 ) while machines_available < machines_number: - await ops_test.juju("add-machine") - status = await ops_test.juju("status", "--format", "json") - data = json.loads(status[1]) + juju.cli("add-machine") + status = juju.status() machines_available = ( sum( 1 - for machine in data["machines"].values() - if machine["machine-status"]["current"] == "running" + for machine in status.machines.values() + if machine.juju_status.current == "running" ) - 1 ) @@ -100,19 +90,17 @@ async def test_lots_of_applications( for i in range(int(2000 / batch_number)): - status = await ops_test.juju("status", "--format", "json") - status_data = json.loads(status[1]) + status = juju.status() # we collect the machines but leave out machine "0" # that should be in use by the bind-operator - machines = [x for x in status_data["machines"].keys() if x != "0"] + machines = [x for x in status.machines.keys() if x != "0"] print("Available machines:", machines) await asyncio.gather( *[ deploy_any_charm( - app=app, - ops_test=ops_test, - model=model, + app_name=app, + juju=juju, any_app_number=i * batch_number + x, machines=machines, entries=[ @@ -139,9 +127,8 @@ async def test_lots_of_applications( @pytest.mark.abort_on_fail @pytest.mark.skip(reason="Scaling test") async def test_lots_of_record_requests( - app: juju.application.Application, - ops_test: OpsTest, - model: Model, + app: str, + juju: jubilant.Juju, ): """ arrange: build and deploy the charm. @@ -168,9 +155,8 @@ async def test_lots_of_record_requests( ) await deploy_any_charm( - app=app, - ops_test=ops_test, - model=model, + app_name=app, + juju=juju, any_app_number=any_app_number, machines=None, entries=entries, diff --git a/bind-operator/tox.ini b/bind-operator/tox.ini index 94b3e7e0..7473275e 100644 --- a/bind-operator/tox.ini +++ b/bind-operator/tox.ini @@ -50,7 +50,7 @@ deps = pyproject-flake8 pytest pytest-asyncio - pytest-operator + jubilant requests types-PyYAML types-requests @@ -106,7 +106,7 @@ deps = juju pytest pytest-asyncio - pytest-operator + jubilant -r{toxinidir}/requirements.txt commands = pytest -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs}