diff --git a/src-docs/runner_manager.md b/src-docs/runner_manager.md index 6cfdf64b5..a848bf4fb 100644 --- a/src-docs/runner_manager.md +++ b/src-docs/runner_manager.md @@ -3,7 +3,7 @@ # module `runner_manager` -Runner Manager manages the runners on LXD and GitHub. +Module for managing reactive runners. **Global Variables** --------------- @@ -17,6 +17,31 @@ Runner Manager manages the runners on LXD and GitHub. - **TIMEOUT_COMMAND** - **UBUNTU_USER** +--- + + + +## function `reconcile` + +```python +reconcile(quantity: int, config: ReactiveRunnerConfig) → int +``` + +Spawn a runner reactively. + + + +**Args:** + + - `quantity`: The number of runners to spawn. + - `config`: The configuration for the reactive runner. + + + +**Raises:** + + - `ReactiveRunnerError`: If the runner fails to spawn. + --- @@ -32,7 +57,7 @@ Manage a group of runners according to configuration. - `runner_bin_path`: The github runner app scripts path. - `cron_path`: The path to runner build image cron job. - + ### method `__init__` @@ -59,7 +84,7 @@ Construct RunnerManager object for creating and managing runners. --- - + ### method `build_runner_image` @@ -79,7 +104,7 @@ Build container image in test mode, else virtual machine image. --- - + ### method `check_runner_bin` @@ -96,7 +121,7 @@ Check if runner binary exists. --- - + ### method `flush` @@ -125,7 +150,7 @@ Remove existing runners. --- - + ### method `get_github_info` @@ -142,7 +167,7 @@ Get information on the runners from GitHub. --- - + ### method `get_latest_runner_bin_url` @@ -173,7 +198,7 @@ The runner binary URL changes when a new version is available. --- - + ### method `has_runner_image` @@ -190,7 +215,7 @@ Check if the runner image exists. --- - + ### method `reconcile` @@ -214,7 +239,7 @@ Bring runners in line with target. --- - + ### method `schedule_build_runner_image` @@ -226,7 +251,7 @@ Install cron job for building runner image. --- - + ### method `update_runner_bin` @@ -253,7 +278,7 @@ Remove the existing runner binary to prevent it from being used. This is done to --- - + ## class `ReactiveRunnerError` Raised when a reactive runner error occurs. @@ -264,17 +289,17 @@ Raised when a reactive runner error occurs. --- - + -## class `ReactiveRunnerManager` -A class to manage the reactive runners. +## class `ReactiveRunnerConfig` +ReactiveRunnerConfig(mq_uri: str, queue_name: str) - + ### method `__init__` ```python -__init__(reactive_config: ReactiveConfig, queue_name: str) +__init__(mq_uri: str, queue_name: str) → None ``` @@ -284,28 +309,4 @@ __init__(reactive_config: ReactiveConfig, queue_name: str) ---- - - - -### method `reconcile` - -```python -reconcile(quantity: int) → int -``` - -Spawn a runner reactively. - - - -**Args:** - - - `queue_name`: The name of the queue. - - - -**Raises:** - - - `ReactiveRunnerError`: If the runner fails to spawn. - diff --git a/src-docs/runner_manager.py.md b/src-docs/runner_manager.py.md index 3ad9cbedb..8cff1dd26 100644 --- a/src-docs/runner_manager.py.md +++ b/src-docs/runner_manager.py.md @@ -23,7 +23,7 @@ Manage a group of runners according to configuration. - `runner_bin_path`: The github runner app scripts path. - `cron_path`: The path to runner build image cron job. - + ### function `__init__` @@ -50,7 +50,7 @@ Construct RunnerManager object for creating and managing runners. --- - + ### function `build_runner_image` @@ -70,7 +70,7 @@ Build container image in test mode, else virtual machine image. --- - + ### function `check_runner_bin` @@ -87,7 +87,7 @@ Check if runner binary exists. --- - + ### function `flush` @@ -116,7 +116,7 @@ Remove existing runners. --- - + ### function `get_github_info` @@ -133,7 +133,7 @@ Get information on the runners from GitHub. --- - + ### function `get_latest_runner_bin_url` @@ -164,7 +164,7 @@ The runner binary URL changes when a new version is available. --- - + ### function `has_runner_image` @@ -181,7 +181,7 @@ Check if the runner image exists. --- - + ### function `reconcile` @@ -205,7 +205,7 @@ Bring runners in line with target. --- - + ### function `schedule_build_runner_image` @@ -217,7 +217,7 @@ Install cron job for building runner image. --- - + ### function `update_runner_bin` diff --git a/src/openstack_cloud/openstack_manager.py b/src/openstack_cloud/openstack_manager.py index 4d88669c3..a378623de 100644 --- a/src/openstack_cloud/openstack_manager.py +++ b/src/openstack_cloud/openstack_manager.py @@ -38,6 +38,7 @@ from openstack.exceptions import OpenStackCloudException, SDKException from paramiko.ssh_exception import NoValidConnectionsError +import reactive.runner_manager as reactive_runner_manager from charm_state import ( Arch, CharmState, @@ -69,7 +70,6 @@ from metrics import runner as runner_metrics from metrics import storage as metrics_storage from metrics.runner import RUNNER_INSTALLED_TS_FILE_NAME -from reactive.runner_manager import ReactiveRunnerManager from repo_policy_compliance_client import RepoPolicyComplianceClient from runner_manager import IssuedMetricEventsStats from runner_manager_type import OpenstackRunnerManagerConfig @@ -587,10 +587,10 @@ def _reconcile_reactive(self, quantity: int) -> int: quantity: Number of intended runners. """ logger.info("Reactive mode is experimental and not yet fully implemented.") - reactive_runner_manager = ReactiveRunnerManager( - reactive_config=self._config.reactive_config, queue_name=self.app_name + config = reactive_runner_manager.ReactiveRunnerConfig( + mq_uri=self._config.reactive_config.mq_uri, queue_name=self.app_name ) - return reactive_runner_manager.reconcile(quantity=quantity) + return reactive_runner_manager.reconcile(quantity=quantity, config=config) def _reconcile_runners(self, quantity: int) -> int: """Reconcile the number of runners. diff --git a/src/reactive/runner_manager.py b/src/reactive/runner_manager.py index f6b7b43b2..5cedecc2c 100644 --- a/src/reactive/runner_manager.py +++ b/src/reactive/runner_manager.py @@ -1,11 +1,13 @@ # Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. + +"""Module for managing reactive runners.""" import logging import shutil import subprocess +from dataclasses import dataclass from pathlib import Path -from charm_state import ReactiveConfig from utilities import secure_run_subprocess logger = logging.getLogger(__name__) @@ -23,91 +25,97 @@ class ReactiveRunnerError(Exception): """Raised when a reactive runner error occurs.""" -class ReactiveRunnerManager: - """A class to manage the reactive runners.""" - - def __init__(self, reactive_config: ReactiveConfig, queue_name: str): - self._reactive_config = reactive_config - self._queue_name = queue_name - - def reconcile(self, quantity: int) -> int: - """Spawn a runner reactively. - - Args: - queue_name: The name of the queue. - - Raises: - ReactiveRunnerError: If the runner fails to spawn. - """ - - actual_quantity = self._determine_current_quantity() - logger.info("Actual quantity of ReactiveRunner processes: %s", actual_quantity) - delta = quantity - actual_quantity - actual_delta = delta - if delta > 0: - logger.info("Will spawn %d new ReactiveRunner processes", delta) - self._setup_log_file() - for _ in range(delta): - try: - self._spawn_runner() - except ReactiveRunnerError: - logger.exception("Failed to spawn a new ReactiveRunner process") - actual_delta -= 1 - elif delta < 0: - logger.info( - "%d ReactiveRunner processes are running. Will skip spawning. Additional processes should terminate after %s.", - actual_quantity, - REACTIVE_RUNNER_TIMEOUT_STR, - ) - else: - logger.info("No changes to number of ReactiveRunners needed.") - - return max(actual_delta, 0) - - def _determine_current_quantity(self): - """Determine the current quantity of reactive runners. - - Returns: - The number of reactive runners. - - Raises: - ReactiveRunnerError: If the number of reactive runners cannot be determined - """ - result = secure_run_subprocess(cmd=PS_COMMAND_LINE_LIST) - if result.returncode != 0: - raise ReactiveRunnerError("Failed to get list of processes") - commands = result.stdout.decode().rstrip().split("\n")[1:] - logger.debug(commands) - actual_quantity = 0 - for command in commands: - if command.startswith(f"{PYTHON_BIN} {REACTIVE_RUNNER_SCRIPT_FILE}"): - actual_quantity += 1 - return actual_quantity - - def _setup_log_file(self) -> None: - """Set up the log file.""" - logfile = Path(REACTIVE_RUNNER_LOG_FILE) - if not logfile.exists(): - logfile.touch() - shutil.chown(logfile, user=UBUNTU_USER, group=UBUNTU_USER) - - def _spawn_runner(self) -> None: - """Spawn a runner.""" - env = {"PYTHONPATH": "src:lib:venv"} - process = subprocess.Popen( - [ - TIMEOUT_COMMAND, - REACTIVE_RUNNER_TIMEOUT_STR, - PYTHON_BIN, - REACTIVE_RUNNER_SCRIPT_FILE, - self._reactive_config.mq_uri, - self._queue_name, - ], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - env=env, - user=UBUNTU_USER, +@dataclass +class ReactiveRunnerConfig: + + mq_uri: str + queue_name: str + + +def reconcile(quantity: int, config: ReactiveRunnerConfig) -> int: + """Spawn a runner reactively. + + Args: + quantity: The number of runners to spawn. + config: The configuration for the reactive runner. + + Raises: + ReactiveRunnerError: If the runner fails to spawn. + """ + + actual_quantity = _determine_current_quantity() + logger.info("Actual quantity of reactive runner processes: %s", actual_quantity) + delta = quantity - actual_quantity + actual_delta = delta + if delta > 0: + logger.info("Will spawn %d new reactive runner processes", delta) + _setup_log_file() + for _ in range(delta): + try: + _spawn_runner(config) + except ReactiveRunnerError: + logger.exception("Failed to spawn a new reactive runner process") + actual_delta -= 1 + elif delta < 0: + logger.info( + "%d reactive runner processes are running. Will skip spawning. Additional processes should terminate after %s.", + actual_quantity, + REACTIVE_RUNNER_TIMEOUT_STR, + ) + else: + logger.info("No changes to number of reactive runner processes needed.") + + return max(actual_delta, 0) + + +def _determine_current_quantity(): + """Determine the current quantity of reactive runners. + + Returns: + The number of reactive runners. + + Raises: + ReactiveRunnerError: If the number of reactive runners cannot be determined + """ + result = secure_run_subprocess(cmd=PS_COMMAND_LINE_LIST) + if result.returncode != 0: + raise ReactiveRunnerError("Failed to get list of processes") + commands = result.stdout.decode().rstrip().split("\n")[1:] + logger.debug(commands) + actual_quantity = 0 + for command in commands: + if command.startswith(f"{PYTHON_BIN} {REACTIVE_RUNNER_SCRIPT_FILE}"): + actual_quantity += 1 + return actual_quantity + + +def _setup_log_file() -> None: + """Set up the log file.""" + logfile = Path(REACTIVE_RUNNER_LOG_FILE) + if not logfile.exists(): + logfile.touch() + shutil.chown(logfile, user=UBUNTU_USER, group=UBUNTU_USER) + + +def _spawn_runner(reactive_runner_config: ReactiveRunnerConfig) -> None: + """Spawn a runner.""" + env = {"PYTHONPATH": "src:lib:venv"} + process = subprocess.Popen( + [ + TIMEOUT_COMMAND, + REACTIVE_RUNNER_TIMEOUT_STR, + PYTHON_BIN, + REACTIVE_RUNNER_SCRIPT_FILE, + reactive_runner_config.mq_uri, + reactive_runner_config.queue_name, + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + env=env, + user=UBUNTU_USER, + ) + logger.debug("Spawned a new reactive runner process with pid %s", process.pid) + if process.returncode not in (0, None): + raise ReactiveRunnerError( + "Failed to spawn a new reactive runner process. Return code: %s" % process.returncode ) - logger.debug("Spawned a new ReactiveRunner process with pid %s", process.pid) - if process.returncode not in (0, None): - raise ReactiveRunnerError("Failed to spawn a new ReactiveRunner process") diff --git a/src/runner_manager.py b/src/runner_manager.py index c94b1be23..5cb7ec0b1 100644 --- a/src/runner_manager.py +++ b/src/runner_manager.py @@ -9,9 +9,7 @@ import secrets import tarfile import time -from dataclasses import dataclass from datetime import datetime, timedelta, timezone -from multiprocessing import Pool from pathlib import Path from typing import Iterator, Optional, Type @@ -20,8 +18,9 @@ import requests.adapters import urllib3 +import reactive.runner_manager as reactive_runner_manager import shared_fs -from charm_state import ReactiveConfig, VirtualMachineResources +from charm_state import VirtualMachineResources from errors import ( GetMetricsStorageError, GithubClientError, @@ -40,7 +39,6 @@ from metrics import runner as runner_metrics from metrics import runner_logs from metrics.runner import RUNNER_INSTALLED_TS_FILE_NAME -from reactive.runner_manager import ReactiveRunnerManager from repo_policy_compliance_client import RepoPolicyComplianceClient from runner import LXD_PROFILE_YAML, CreateRunnerConfig, Runner, RunnerConfig, RunnerStatus from runner_manager_type import FlushMode, RunnerInfo, RunnerManagerClients, RunnerManagerConfig @@ -583,10 +581,10 @@ def _reconcile_reactive(self, quantity: int) -> int: quantity: Number of intended runners. """ logger.info("Reactive mode is experimental and not yet fully implemented.") - reactive_runner_manager = ReactiveRunnerManager( - reactive_config=self.config.reactive_config, queue_name=self.app_name + config = reactive_runner_manager.ReactiveRunnerConfig( + mq_uri=self.config.reactive_config.mq_uri, queue_name=self.app_name ) - return reactive_runner_manager.reconcile(quantity=quantity) + return reactive_runner_manager.reconcile(quantity=quantity, config=config) def _runners_in_pre_job(self) -> bool: """Check there exist runners in the pre-job script stage. diff --git a/tests/unit/reactive/test_runner_manager.py b/tests/unit/reactive/test_runner_manager.py index 3ffc7ef90..4a410ade8 100644 --- a/tests/unit/reactive/test_runner_manager.py +++ b/tests/unit/reactive/test_runner_manager.py @@ -1,6 +1,7 @@ # Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. import random +import secrets import subprocess from pathlib import Path from subprocess import CompletedProcess @@ -13,11 +14,12 @@ PS_COMMAND_LINE_LIST, PYTHON_BIN, REACTIVE_RUNNER_SCRIPT_FILE, - ReactiveRunnerError, - ReactiveRunnerManager, + ReactiveRunnerError, ReactiveRunnerConfig, +reconcile ) from utilities import secure_run_subprocess +EXAMPLE_MQ_URI = "http://example.com" @pytest.fixture(name="log_file_path", autouse=True) def log_file_path_fixture(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: @@ -57,11 +59,11 @@ def test_reconcile_spawns_runners( act: Call reconcile with a quantity of 5. assert: Three runners are spawned. Log file is setup. """ - reactive_config = ReactiveConfig(mq_uri="http://example.com") + queue_name = secrets.token_hex(16) + reactive_config = ReactiveRunnerConfig(mq_uri=EXAMPLE_MQ_URI, queue_name=queue_name) _arrange_reactive_processes(secure_run_subprocess_mock, count=2) - manager = ReactiveRunnerManager(reactive_config, "test-queue") - delta = manager.reconcile(5) + delta = reconcile(5, reactive_config) assert delta == 3 assert subprocess_popen_mock.call_count == 3 @@ -92,11 +94,11 @@ def test_reconcile_does_not_spawn_runners( act: Call reconcile with a quantity of 2. assert: No runners are spawned. """ - reactive_config = ReactiveConfig(mq_uri="http://example.com") + queue_name = secrets.token_hex(16) + reactive_config = ReactiveRunnerConfig(mq_uri=EXAMPLE_MQ_URI, queue_name=queue_name) _arrange_reactive_processes(secure_run_subprocess_mock, count=2) - manager = ReactiveRunnerManager(reactive_config, "test-queue") - delta = manager.reconcile(2) + delta = reconcile(2, reactive_config) assert delta == 0 assert subprocess_popen_mock.call_count == 0 @@ -110,10 +112,10 @@ def test_reconcile_does_not_spawn_runners_for_too_many_processes( act: Call reconcile with a quantity of 1. assert: No runners are spawned and delta is 0. """ - reactive_config = ReactiveConfig(mq_uri="http://example.com") + queue_name = secrets.token_hex(16) + reactive_config = ReactiveRunnerConfig(mq_uri=EXAMPLE_MQ_URI, queue_name=queue_name) _arrange_reactive_processes(secure_run_subprocess_mock, count=2) - manager = ReactiveRunnerManager(reactive_config, "test-queue") - delta = manager.reconcile(1) + delta = reconcile(1, reactive_config) assert delta == 0 assert subprocess_popen_mock.call_count == 0 @@ -127,6 +129,8 @@ def test_reconcile_raises_reactive_runner_error_on_ps_failure( act: Call reconcile with a quantity of 1. assert: A ReactiveRunnerError is raised. """ + queue_name = secrets.token_hex(16) + reactive_config = ReactiveRunnerConfig(mq_uri=EXAMPLE_MQ_URI, queue_name=queue_name) secure_run_subprocess_mock.return_value = CompletedProcess( args=PS_COMMAND_LINE_LIST, returncode=1, @@ -134,10 +138,9 @@ def test_reconcile_raises_reactive_runner_error_on_ps_failure( stderr=b"error", ) - reactive_config = ReactiveConfig(mq_uri="http://example.com") - manager = ReactiveRunnerManager(reactive_config, "test-queue") + reactive_config = ReactiveConfig(mq_uri=EXAMPLE_MQ_URI) with pytest.raises(ReactiveRunnerError) as err: - manager.reconcile(1) + reconcile(1, reactive_config) assert "Failed to get list of processes" in str(err.value) @@ -150,6 +153,8 @@ def test_reconcile_spawn_runner_failed( act: Call reconcile with a quantity of 3. assert: The delta is 2. """ + queue_name = secrets.token_hex(16) + reactive_config = ReactiveRunnerConfig(mq_uri=EXAMPLE_MQ_URI, queue_name=queue_name) subprocess_popen_mock.side_effect = [ MagicMock(returncode=0), MagicMock(return_code=1), @@ -157,8 +162,6 @@ def test_reconcile_spawn_runner_failed( ] _arrange_reactive_processes(secure_run_subprocess_mock, count=0) - reactive_config = ReactiveConfig(mq_uri="http://example.com") - manager = ReactiveRunnerManager(reactive_config, "test-queue") - delta = manager.reconcile(3) + delta = reconcile(3, reactive_config) assert delta == 2 diff --git a/tests/unit/test_openstack_manager.py b/tests/unit/test_openstack_manager.py index 6e3ab57d8..123d60150 100644 --- a/tests/unit/test_openstack_manager.py +++ b/tests/unit/test_openstack_manager.py @@ -17,6 +17,7 @@ from pytest import LogCaptureFixture, MonkeyPatch import metrics.storage +import reactive.runner_manager from charm_state import CharmState, ProxyConfig, ReactiveConfig, RepoPolicyComplianceConfig from errors import OpenStackError, RunnerStartError from github_type import GitHubRunnerStatus, SelfHostedRunner @@ -25,7 +26,6 @@ from metrics.storage import MetricsStorage from openstack_cloud import openstack_manager from openstack_cloud.openstack_manager import MAX_METRICS_FILE_SIZE, METRICS_EXCHANGE_PATH -from reactive.runner_manager import ReactiveRunnerManager from runner_type import RunnerByHealth, RunnerGithubInfo from tests.unit import factories @@ -248,14 +248,15 @@ def pool_map(func, iterable): return os_runner_manager -@pytest.fixture(name="reactive_runner_manager_mock") -def reactive_runner_manager_fixture(monkeypatch: MonkeyPatch, tmp_path: Path) -> MagicMock: +@pytest.fixture(name="reactive_reconcile_mock") +def reactive_reconcile_fixture(monkeypatch: MonkeyPatch, tmp_path: Path) -> MagicMock: """Mock the job class.""" - reactive_runner_manager = MagicMock(spec=ReactiveRunnerManager) - monkeypatch.setattr("openstack_cloud.openstack_manager.ReactiveRunnerManager", MagicMock(return_value=reactive_runner_manager)) - reactive_runner_manager.reconcile.side_effect = lambda quantity: quantity - return reactive_runner_manager - + reconcile_mock = MagicMock(spec=reactive.runner_manager.reconcile) + monkeypatch.setattr( + "openstack_cloud.openstack_manager.reactive_runner_manager.reconcile", reconcile_mock + ) + reconcile_mock.side_effect = lambda quantity, **kwargs: quantity + return reconcile_mock def test__create_connection_error(clouds_yaml: dict, openstack_connect_mock: MagicMock): @@ -1017,7 +1018,9 @@ def test_reconcile_ignores_metrics_for_openstack_online_runners( def test_reconcile_reactive_mode( - openstack_manager_for_reconcile: openstack_manager.OpenstackRunnerManager, reactive_runner_manager_mock: MagicMock, caplog: LogCaptureFixture + openstack_manager_for_reconcile: openstack_manager.OpenstackRunnerManager, + reactive_reconcile_mock: MagicMock, + caplog: LogCaptureFixture, ): """ arrange: Enable reactive mode and mock the job class to return a job. @@ -1031,7 +1034,12 @@ def test_reconcile_reactive_mode( actual_count = openstack_manager_for_reconcile.reconcile(quantity=count) assert actual_count == count - reactive_runner_manager_mock.reconcile.assert_called_with(quantity=count) + reactive_reconcile_mock.assert_called_with( + quantity=count, + config=reactive.runner_manager.ReactiveRunnerConfig( + mq_uri="http://example.com", queue_name=openstack_manager_for_reconcile.app_name + ), + ) def test_repo_policy_config( diff --git a/tests/unit/test_runner_manager.py b/tests/unit/test_runner_manager.py index 88c840b8e..b173ec485 100644 --- a/tests/unit/test_runner_manager.py +++ b/tests/unit/test_runner_manager.py @@ -10,6 +10,7 @@ import pytest from pytest import LogCaptureFixture, MonkeyPatch +import reactive.runner_manager import shared_fs from charm_state import ( Arch, @@ -26,7 +27,6 @@ from metrics.events import Reconciliation, RunnerInstalled, RunnerStart, RunnerStop from metrics.runner import RUNNER_INSTALLED_TS_FILE_NAME from metrics.storage import MetricsStorage -from reactive.runner_manager import ReactiveRunnerManager from runner import Runner, RunnerStatus from runner_manager import BUILD_IMAGE_SCRIPT_FILENAME, RunnerManager, RunnerManagerConfig from runner_type import RunnerByHealth @@ -126,13 +126,13 @@ def runner_metrics_fixture(monkeypatch: MonkeyPatch) -> MagicMock: return runner_metrics_mock -@pytest.fixture(name="reactive_runner_manager_mock") -def reactive_runner_manager_fixture(monkeypatch: MonkeyPatch, tmp_path: Path) -> MagicMock: +@pytest.fixture(name="reactive_reconcile_mock") +def reactive_reconcile_fixture(monkeypatch: MonkeyPatch, tmp_path: Path) -> MagicMock: """Mock the job class.""" - reactive_runner_manager = MagicMock(spec=ReactiveRunnerManager) - monkeypatch.setattr("runner_manager.ReactiveRunnerManager", MagicMock(return_value=reactive_runner_manager)) - reactive_runner_manager.reconcile.side_effect = lambda quantity: quantity - return reactive_runner_manager + reconcile_mock = MagicMock(spec=reactive.runner_manager.reconcile) + monkeypatch.setattr("runner_manager.reactive_runner_manager.reconcile", reconcile_mock) + reconcile_mock.side_effect = lambda quantity, **kwargs: quantity + return reconcile_mock @pytest.mark.parametrize( @@ -520,7 +520,9 @@ def test_reconcile_places_no_timestamp_in_newly_created_runner_if_metrics_disabl def test_reconcile_reactive_mode( - runner_manager: RunnerManager, reactive_runner_manager_mock: MagicMock, caplog: LogCaptureFixture + runner_manager: RunnerManager, + reactive_reconcile_mock: MagicMock, + caplog: LogCaptureFixture, ): """ arrange: Enable reactive mode and mock the job class to return a job. @@ -532,7 +534,12 @@ def test_reconcile_reactive_mode( actual_count = runner_manager.reconcile(count, VirtualMachineResources(2, "7GiB", "10Gib")) assert actual_count == count - reactive_runner_manager_mock.reconcile.assert_called_with(quantity=count) + reactive_reconcile_mock.assert_called_with( + quantity=count, + config=reactive.runner_manager.ReactiveRunnerConfig( + mq_uri="http://example.com", queue_name=runner_manager.app_name + ), + ) def test_schedule_build_runner_image(