diff --git a/goth/address.py b/goth/address.py index 6beabebf0..307357e51 100644 --- a/goth/address.py +++ b/goth/address.py @@ -95,12 +95,29 @@ def _with_default(self, mapping: Mapping[str, object]): default={"port": YAGNA_REST_PORT, "protocol": YAGNA_REST_PROTOCOL}, ) -# Range of ports on the host which can be mapped to yagna daemons' REST API port +# Range of ports on the host to which yagna daemon ports in containers can be mapped. +# Port $YAGNA_REST_PORT on N-th started yagna container is mapped to port +# $HOST_REST_PORT_START + (N-1) on the host. + # NOTE: This variable is used in `nginx.conf` file in the proxy container: HOST_REST_PORT_START = 6001 # NOTE: This variable is used in `nginx.conf` file in the proxy container: HOST_REST_PORT_END = 6100 +# Ports in the range $HOST_REST_PORT_START .. $HOST_REST_PORT_END are also used +# by the nginx-proxy container. A request made to port $HOST_REST_PORT_START + (N-1) +# on nginx-proxy container is forwarded to the MITM proxy (that runs on host) +# which then forwards the request to $YAGNA_REST_PORT on the N-th yagna container. + +# To enable access to those nginx-proxy ports from outside of the Docker network +# on non-Linux systems, we map ports $HOST_REST_PORT_START .. $HOST_REST_PORT_END on +# the nginx-proxy to ports ($HOST_REST_PORT_START + O) .. ($HOST_REST_PORT_END + O) +# on the host, where the offset O is the value of the variable HOST_NGINX_PORT_OFFSET +# defined below. +# NOTE: this value has to be consistent with a port mapping definition for the +# `nginx-proxy` container in `docker-compose.yml`. +HOST_NGINX_PORT_OFFSET = 10000 + # Port used by the mitmproxy instance # NOTE: This variable is used in `nginx.conf` file in the proxy container: diff --git a/goth/api_monitor/router_addon.py b/goth/api_monitor/router_addon.py index 20e733123..36804d75a 100644 --- a/goth/api_monitor/router_addon.py +++ b/goth/api_monitor/router_addon.py @@ -55,20 +55,19 @@ def request(self, flow: HTTPFlow) -> None: node_name = self._node_names[remote_addr] if server_port == YAGNA_REST_PORT: - # It's a provider agent calling a yagna daemon - # Since we assume that the provider agent runs on the same container - # as the provider daemon, we route this request to that container's - # host-mapped daemon port + # This should be a request from an agent running in a yagna container + # calling that container's daemon. We route this request to that + # container's host-mapped daemon port. req.host = "127.0.0.1" req.port = self._ports[remote_addr][server_port] req.headers[CALLER_HEADER] = f"{node_name}:agent" req.headers[CALLEE_HEADER] = f"{node_name}:daemon" elif HOST_REST_PORT_START <= server_port <= HOST_REST_PORT_END: - # It's a requestor agent calling a yagna daemon. - # We use localhost as the address together with the original port, - # since each daemon has its API port mapped to a port on the host - # chosen from the specified range. + # This should be a request from an agent running on the Docker host + # calling a yagna daemon in a container. We use localhost as the address + # together with the original port, since each daemon has its API port + # mapped to a port on the host chosen from the specified range. req.host = "127.0.0.1" req.port = server_port req.headers[CALLER_HEADER] = f"{node_name}:agent" diff --git a/goth/configuration.py b/goth/configuration.py index ad7b0a280..a567fdea5 100644 --- a/goth/configuration.py +++ b/goth/configuration.py @@ -82,6 +82,7 @@ def add_node( subnet="goth", volumes=volumes, payment_id=self._id_pool.get_id(), + use_proxy=use_proxy, **kwargs, ) diff --git a/goth/default-assets/docker/docker-compose.yml b/goth/default-assets/docker/docker-compose.yml index d8378fcc0..6999292b0 100644 --- a/goth/default-assets/docker/docker-compose.yml +++ b/goth/default-assets/docker/docker-compose.yml @@ -13,7 +13,16 @@ services: # harness on the host machine image: proxy-nginx ports: - - "6000:6000" + # Requests to ports 6001-6100 in proxy-nginx are forwarded + # to the MITM proxy started by the test runner, and further + # to yagna API port (usually 6000) in yagna containers: + # request to port 6001 is forwarded to (yagna API port in) + # the first yagna container, request to port 6002 -- to + # the second one, and so on. + # To make these ports available from Docker host (on some + # systems, Docker network may be unreachable from the host) + # we map them to ports 16001-16100 on the host. + - "16001-16100:6001-6100" ethereum: image: docker.pkg.github.com/golemfactory/gnt2/gnt2-docker-yagna:483c6f241edd diff --git a/goth/default-assets/goth-config.yml b/goth/default-assets/goth-config.yml index 241005ea7..f5f0ad839 100644 --- a/goth/default-assets/goth-config.yml +++ b/goth/default-assets/goth-config.yml @@ -52,6 +52,7 @@ nodes: - name: "requestor" type: "Requestor" + use-proxy: True - name: "provider-1" type: "VM-Wasm-Provider" diff --git a/goth/node.py b/goth/node.py index cd13c4965..44597cb9e 100644 --- a/goth/node.py +++ b/goth/node.py @@ -4,6 +4,7 @@ from goth.address import ( ACTIVITY_API_URL, + MARKET_API_URL, PAYMENT_API_URL, ROUTER_HOST, ROUTER_PORT, @@ -45,7 +46,12 @@ def node_environment( if rest_api_url_base: agent_env = { + # Setting URLs for all three APIs has the same effect as setting + # YAGNA_API_URL to YAGNA_REST_URL.substitute(base=rest_api_url_base). + # We set all three separately so it's easier to selectively disable + # proxy for certain APIs (if a need to do so arises). "YAGNA_ACTIVITY_URL": ACTIVITY_API_URL.substitute(base=rest_api_url_base), + "YAGNA_MARKET_URL": MARKET_API_URL.substitute(base=rest_api_url_base), "YAGNA_PAYMENT_URL": PAYMENT_API_URL.substitute(base=rest_api_url_base), } node_env.update(agent_env) diff --git a/goth/runner/__init__.py b/goth/runner/__init__.py index d1160cf6e..45f393866 100644 --- a/goth/runner/__init__.py +++ b/goth/runner/__init__.py @@ -42,6 +42,14 @@ ProbeType = TypeVar("ProbeType", bound=Probe) +PROXY_NGINX_SERVICE_NAME = "proxy-nginx" +"""Name of the nginx proxy service in the Docker network. + +Must match the service name in the config file used by the runner's +compose network manager. +""" + + class Runner: """Manages the nodes and runs the scenario on them.""" @@ -75,10 +83,13 @@ class Runner: _exit_stack: AsyncExitStack """A stack of `AsyncContextManager` instances to be closed on runner shutdown.""" + _nginx_service_address: Optional[str] + """The IP address of the nginx service in the Docker network.""" + _topology: List[YagnaContainerConfig] """A list of configuration objects for the containers to be instantiated.""" - _web_server: WebServer + _web_server: Optional[WebServer] """A built-in web server.""" def __init__( @@ -107,6 +118,7 @@ def __init__( config=compose_config, docker_client=docker.from_env(), ) + self._nginx_service_address = None self._web_server = ( WebServer(web_root_path, web_server_port) if web_root_path else None ) @@ -234,6 +246,13 @@ def host_address(self) -> str: else: return "host.docker.internal" + @property + def nginx_container_address(self) -> str: + """Return the IP address of the proxy-nginx service in the Docker network.""" + if not self._nginx_service_address: + raise RuntimeError("Docker network not started") + return self._nginx_service_address + @property def web_server_port(self) -> Optional[int]: """Return the port of the build-in web server.""" @@ -275,9 +294,17 @@ async def _enter(self) -> None: self._exit_stack.enter_context(configure_logging_for_test(self.log_dir)) logger.info(colors.yellow("Running test: %s"), self.test_name) - await self._exit_stack.enter_async_context( + container_info = await self._exit_stack.enter_async_context( run_compose_network(self._compose_manager, self.log_dir) ) + for info in container_info.values(): + if PROXY_NGINX_SERVICE_NAME in info.aliases: + self._nginx_service_address = info.address + break + else: + raise RuntimeError( + f"Service {PROXY_NGINX_SERVICE_NAME} not found in the Docker network" + ) self._create_probes(self.log_dir) diff --git a/goth/runner/container/compose.py b/goth/runner/container/compose.py index e14d4c8e6..7824023f6 100644 --- a/goth/runner/container/compose.py +++ b/goth/runner/container/compose.py @@ -5,7 +5,7 @@ import logging import os from pathlib import Path -from typing import ClassVar, Dict, Optional +from typing import AsyncIterator, ClassVar, Dict, List, Optional from docker import DockerClient import yaml @@ -16,7 +16,7 @@ build_yagna_image, YagnaBuildEnvironment, ) -from goth.runner.container.utils import get_container_address +from goth.runner.container.utils import get_container_network_info from goth.runner.exceptions import ContainerNotFoundError from goth.runner.log import LogConfig from goth.runner.log_monitor import LogEventMonitor @@ -47,6 +47,20 @@ class ComposeConfig: """ +@dataclass +class ContainerInfo: + """Info on a Docker container started by Docker compose.""" + + address: str + """The container's IP address in the Docker network""" + + aliases: List[str] + """Container aliases in the Docker network""" + + image: str + """The container's image name""" + + class ComposeNetworkManager: """Class which manages a docker-compose network. @@ -82,9 +96,12 @@ def __init__( self._log_monitors = {} self._network_gateway_address = "" - async def start_network(self, log_dir: Path, force_build: bool = False) -> None: + async def start_network( + self, log_dir: Path, force_build: bool = False + ) -> Dict[str, ContainerInfo]: """Start the compose network based on this manager's compose file. + Returns information on containers started in the compose network. This step may include (re)building the network's docker images. """ # Stop the network in case it's already running (e.g. from a previous test) @@ -106,7 +123,12 @@ async def start_network(self, log_dir: Path, force_build: bool = False) -> None: self._start_log_monitors(log_dir) await self._wait_for_containers() - self._log_running_containers() + container_infos = self._get_running_containers() + for name, info in container_infos.items(): + logger.info( + "[%-25s] IP address: %-15s image: %s", name, info.address, info.image + ) + return container_infos async def _wait_for_containers(self) -> None: logger.info("Waiting for compose containers to be ready") @@ -162,14 +184,15 @@ def network_gateway_address(self) -> str: return self._network_gateway_address - def _log_running_containers(self): + def _get_running_containers(self) -> Dict[str, ContainerInfo]: + info = {} for container in self._docker_client.containers.list(): - logger.info( - "[%-25s] IP address: %-15s image: %s", - container.name, - get_container_address(self._docker_client, container.name), - container.image.tags[0], + address, aliases = get_container_network_info( + self._docker_client, container.name ) + image = container.image.tags[0] + info[container.name] = ContainerInfo(address, aliases, image) + return info def _start_log_monitors(self, log_dir: Path) -> None: for service_name in self._get_compose_services(): @@ -195,15 +218,18 @@ def _start_log_monitors(self, log_dir: Path) -> None: @contextlib.asynccontextmanager async def run_compose_network( compose_manager: ComposeNetworkManager, log_dir: Path, force_build: bool = False -) -> None: - """Implement AsyncContextManager for starting/stopping docker compose network.""" +) -> AsyncIterator[Dict[str, ContainerInfo]]: + """Implement AsyncContextManager for starting/stopping docker compose network. + + Yields information on containers started in the compose network. + """ try: logger.debug( "Starting compose network. log_dir=%s, force_build=%s", log_dir, force_build ) - await compose_manager.start_network(log_dir, force_build) - yield + containers = await compose_manager.start_network(log_dir, force_build) + yield containers finally: logger.debug("Stopping compose network") await compose_manager.stop_network() diff --git a/goth/runner/container/utils.py b/goth/runner/container/utils.py index c040a6624..5a8d9ea2e 100755 --- a/goth/runner/container/utils.py +++ b/goth/runner/container/utils.py @@ -1,6 +1,6 @@ """Utilities related to Docker containers.""" from pathlib import Path -from typing import Dict +from typing import Dict, List, Tuple from docker import DockerClient @@ -8,12 +8,12 @@ from goth.runner.exceptions import ContainerNotFoundError -def get_container_address( +def get_container_network_info( client: DockerClient, container_name: str, network_name: str = DockerContainer.DEFAULT_NETWORK, -) -> str: - """Get the IP address of a container in a given network. +) -> Tuple[str, List[str]]: + """Get the IP address and the aliases of a container in a given network. The name of the container does not have to be exact, it may be a substring. In case of more than one container name matching the given string, the first @@ -29,7 +29,25 @@ def get_container_address( container = matching_containers[0] container_networks = container.attrs["NetworkSettings"]["Networks"] - return container_networks[network_name]["IPAddress"] + network = container_networks[network_name] + return network["IPAddress"], network["Aliases"] + + +def get_container_address( + client: DockerClient, + container_name: str, + network_name: str = DockerContainer.DEFAULT_NETWORK, +) -> str: + """Get the IP address and the aliases of a container in a given network. + + The name of the container does not have to be exact, it may be a substring. + In case of more than one container name matching the given string, the first + container is returned, as listed by the Docker daemon. + + Raises `ContainerNotFoundError` if no matching container is found. + Raises `KeyError` if the container is not connected to the specified network. + """ + return get_container_network_info(client, container_name, network_name)[0] def get_volumes_spec( diff --git a/goth/runner/container/yagna.py b/goth/runner/container/yagna.py index 2c4f2f085..eea203087 100644 --- a/goth/runner/container/yagna.py +++ b/goth/runner/container/yagna.py @@ -38,6 +38,14 @@ class YagnaContainerConfig(DockerContainerConfig): payment_id: Optional[payment.PaymentId] """Custom key and payment accounts to be imported into yagna ID service.""" + use_proxy: bool + """Whether the probe will route REST API calls through MITM proxy. + + This setting is used when initializing the `api` component of the probe + instantiated from this config, as well as to define the environment in which + commands run by `Probe.run_command_on_host()` are run. + """ + def __init__( self, name: str, @@ -47,6 +55,7 @@ def __init__( environment: Optional[Dict[str, str]] = None, privileged_mode: bool = False, payment_id: Optional[payment.PaymentId] = None, + use_proxy: bool = False, **probe_properties, ): super().__init__(name, volumes or {}, log_config, privileged_mode) @@ -54,6 +63,7 @@ def __init__( self.probe_properties = probe_properties or {} self.environment = environment or {} self.payment_id = payment_id + self.use_proxy = use_proxy class YagnaContainer(DockerContainer): diff --git a/goth/runner/probe/__init__.py b/goth/runner/probe/__init__.py index f4306216b..befae5de0 100644 --- a/goth/runner/probe/__init__.py +++ b/goth/runner/probe/__init__.py @@ -13,6 +13,7 @@ Dict, Iterator, List, + Mapping, Optional, Tuple, TYPE_CHECKING, @@ -21,7 +22,9 @@ from docker import DockerClient from goth.address import ( + HOST_NGINX_PORT_OFFSET, YAGNA_BUS_URL, + YAGNA_REST_PORT, YAGNA_REST_URL, ) @@ -143,6 +146,16 @@ def name(self) -> str: """Name of the container.""" return self.container.name + @property + def host_rest_port(self) -> int: + """Host port to which yagna API port on this probe's container is mapped to.""" + return self.container.ports[YAGNA_REST_PORT] + + @property + def nginx_rest_port(self) -> int: + """Host port to which the nginx port assigned to this probe is mapped to.""" + return self.host_rest_port + HOST_NGINX_PORT_OFFSET + def _setup_gftp_proxy(self, config: YagnaContainerConfig) -> YagnaContainerConfig: """Create a proxy script and a dir for exchanging files with the container.""" @@ -223,7 +236,21 @@ async def _start_container(self) -> None: self.ip_address = get_container_address( self._docker_client, self.container.name ) - self._logger.info("IP address: %s", self.ip_address) + nginx_ip_address = self.runner.nginx_container_address + self._logger.info( + "Yagna API host:port in Docker network: " + "%s:%s (direct), %s:%s (through proxy)", + self.ip_address, + YAGNA_REST_PORT, + nginx_ip_address, + self.host_rest_port, + ) + self._logger.info( + "Yagna API host:port via localhost: " + "127.0.0.1:%s (direct), 127.0.0.1:%s (through proxy)", + self.host_rest_port, + self.nginx_rest_port, + ) async def create_app_key(self, key_name: str = "test_key") -> str: """Attempt to create a new app key on the Yagna daemon. @@ -262,6 +289,25 @@ async def create_app_key(self, key_name: str = "test_key") -> str: key = app_key.key return key + def get_yagna_api_url(self) -> str: + """Return the URL through which this probe's daemon can be reached. + + This URL can be used to access yagna APIs from outside of the probe's + container, for example, from a requestor script running on host. + + The URL may point directly to the IP address of this probe in the Docker + network, or a port on localhost on which the MITM proxy listens, depending + on the `use_proxy` setting in the probe's configuration. + """ + + # Port on the host to which yagna API port in the container is mapped + host_port = ( + self.nginx_rest_port + if self._yagna_config.use_proxy + else self.host_rest_port + ) + return YAGNA_REST_URL.substitute(host="127.0.0.1", port=host_port) + def get_agent_env_vars(self, expand_path: bool = True) -> Dict[str, str]: """Get env vars needed to talk to the daemon in this probe's container. @@ -284,7 +330,7 @@ def get_agent_env_vars(self, expand_path: bool = True) -> Dict[str, str]: return { "YAGNA_APPKEY": self.app_key, - "YAGNA_API_URL": YAGNA_REST_URL.substitute(host=self.ip_address), + "YAGNA_API_URL": self.get_yagna_api_url(), "GSB_URL": YAGNA_BUS_URL.substitute(host=self.ip_address), "PATH": f"{self._gftp_script_dir}:{path}", } @@ -293,7 +339,7 @@ def get_agent_env_vars(self, expand_path: bool = True) -> Dict[str, str]: async def run_command_on_host( self, command: str, - env: Optional[Dict[str, str]] = None, + env: Optional[Mapping[str, str]] = None, command_timeout: float = 300, ) -> AsyncIterator[Tuple[asyncio.Task, PatternMatchingEventMonitor]]: """Run `command` on host in given `env` and with optional `timeout`. @@ -314,7 +360,9 @@ async def run_command_on_host( cmd_env = {**env} if env is not None else {} cmd_env.update(self.get_agent_env_vars()) - cmd_monitor = PatternMatchingEventMonitor(name="command output") + cmd_monitor: PatternMatchingEventMonitor = PatternMatchingEventMonitor( + name="command output" + ) cmd_monitor.start() try: diff --git a/goth/runner/probe/rest_client.py b/goth/runner/probe/rest_client.py index 7ccfe72a2..05a803405 100644 --- a/goth/runner/probe/rest_client.py +++ b/goth/runner/probe/rest_client.py @@ -14,8 +14,6 @@ ACTIVITY_API_URL, MARKET_API_URL, PAYMENT_API_URL, - YAGNA_REST_PORT, - YAGNA_REST_URL, ) from goth.runner.probe.component import ProbeComponent @@ -80,13 +78,7 @@ class RestApiComponent(ProbeComponent): def __init__(self, probe: "Probe"): super().__init__(probe) - - # We reach the daemon through MITM proxy running on localhost using the - # container's unique port mapping - host_port = probe.container.ports[YAGNA_REST_PORT] - proxy_ip = "127.0.0.1" - base_hostname = YAGNA_REST_URL.substitute(host=proxy_ip, port=host_port) - + base_hostname = probe.get_yagna_api_url() self._init_activity_api(base_hostname) self._init_payment_api(base_hostname) self._init_market_api(base_hostname) diff --git a/test/unit/runner/container/test_utils.py b/test/unit/runner/container/test_utils.py index 2a830648a..7116efc9c 100644 --- a/test/unit/runner/container/test_utils.py +++ b/test/unit/runner/container/test_utils.py @@ -20,7 +20,10 @@ def mock_container(): mock_container.attrs = { "NetworkSettings": { "Networks": { - DockerContainer.DEFAULT_NETWORK: {"IPAddress": TEST_IP_ADDRESS} + DockerContainer.DEFAULT_NETWORK: { + "IPAddress": TEST_IP_ADDRESS, + "Aliases": [], + } } } } diff --git a/test/unit/runner/probe/test_probe.py b/test/unit/runner/probe/test_probe.py new file mode 100644 index 000000000..0986653b8 --- /dev/null +++ b/test/unit/runner/probe/test_probe.py @@ -0,0 +1,37 @@ +"""Unit tests for the goth.runner.probe.Probe class.""" +import pytest +from unittest.mock import MagicMock + +from goth.address import YAGNA_REST_PORT, YAGNA_REST_URL, HOST_NGINX_PORT_OFFSET +import goth.runner.container.yagna +from goth.runner.probe import Probe + + +@pytest.mark.parametrize("use_proxy", [False, True]) +@pytest.mark.asyncio +async def test_get_yagna_api_url(monkeypatch, use_proxy: bool): + """Test if get_yagna_api_url() returns correct URL for given use_proxy setting.""" + + monkeypatch.setattr(goth.runner.probe, "YagnaContainer", MagicMock) + + host_mapped_port = 6789 + host_mapped_nginx_port = HOST_NGINX_PORT_OFFSET + host_mapped_port + + probe = Probe( + runner=MagicMock(), + client=MagicMock(), + config=MagicMock(use_proxy=use_proxy), + log_config=MagicMock(), + ) + probe.container.ports = {YAGNA_REST_PORT: host_mapped_port} + + if use_proxy: + expected_url = YAGNA_REST_URL.substitute( + host="127.0.0.1", port=host_mapped_nginx_port + ) + else: + expected_url = YAGNA_REST_URL.substitute( + host="127.0.0.1", port=host_mapped_port + ) + + assert probe.get_yagna_api_url() == expected_url diff --git a/test/unit/runner/probe/test_run_command_on_host.py b/test/unit/runner/probe/test_run_command_on_host.py index 5874f0580..5485d2aa8 100644 --- a/test/unit/runner/probe/test_run_command_on_host.py +++ b/test/unit/runner/probe/test_run_command_on_host.py @@ -3,7 +3,7 @@ import pytest from unittest.mock import MagicMock -from goth.address import YAGNA_BUS_URL, YAGNA_REST_URL +from goth.address import YAGNA_BUS_URL, YAGNA_REST_URL, YAGNA_REST_PORT import goth.runner.container.yagna from goth.runner.probe import RequestorProbe @@ -14,14 +14,12 @@ async def test_run_command_on_host(monkeypatch): runner = MagicMock() docker_client = MagicMock() - container_config = MagicMock() + container_config = MagicMock(use_proxy=False) log_config = MagicMock() + container_rest_port = 6006 monkeypatch.setattr(goth.runner.probe, "YagnaContainer", MagicMock(spec="ports")) monkeypatch.setattr(goth.runner.probe, "Cli", MagicMock(spec="yagna")) - monkeypatch.setattr( - goth.runner.probe, "get_container_address", lambda *_args: "1.2.3.4" - ) monkeypatch.setattr(RequestorProbe, "app_key", "0xcafebabe") probe = RequestorProbe( @@ -30,6 +28,7 @@ async def test_run_command_on_host(monkeypatch): config=container_config, log_config=log_config, ) + probe.container.ports = {YAGNA_REST_PORT: container_rest_port} async def func(lines): # The monitor should guarantee that we don't skip any events @@ -50,7 +49,9 @@ async def func(lines): result = await assertion.wait_for_result(timeout=1) assert result["YAGNA_APPKEY"] == probe.app_key - assert result["YAGNA_API_URL"] == YAGNA_REST_URL.substitute(host=None) + assert result["YAGNA_API_URL"] == YAGNA_REST_URL.substitute( + host="127.0.0.1", port=container_rest_port + ) assert result["GSB_URL"] == YAGNA_BUS_URL.substitute(host=None) # Let's make sure that another command can be run without problems diff --git a/test/unit/runner/test_shutdown.py b/test/unit/runner/test_shutdown.py index 6f878bcb2..f2573ba00 100644 --- a/test/unit/runner/test_shutdown.py +++ b/test/unit/runner/test_shutdown.py @@ -8,8 +8,8 @@ import docker import pytest -from goth.runner import TestFailure, Runner -from goth.runner.container.compose import ComposeNetworkManager +from goth.runner import TestFailure, Runner, PROXY_NGINX_SERVICE_NAME +from goth.runner.container.compose import ComposeNetworkManager, ContainerInfo import goth.runner.container.utils from goth.runner.container.yagna import YagnaContainerConfig from goth.runner.probe import Probe @@ -71,7 +71,7 @@ def mock_runner(test_failure_callback=None, cancellation_callback=None): def mock_function(monkeypatch): """Return a function that performs monkey-patching of functions or coroutines.""" - def _mock_function(class_, method, fails=0): + def _mock_function(class_, method, fails=0, result=None): call = f"{class_.__name__}.{method}()" mock_ = mock.MagicMock() @@ -84,9 +84,10 @@ def _func(*args): mock_.failed = True raise MockError(mock_) print(f"{call} succeeds") + return result async def _coro(*args): - _func(*args) + return _func(*args) if asyncio.iscoroutinefunction(class_.__dict__[method]): mock_.side_effect = _coro @@ -100,6 +101,13 @@ async def _coro(*args): return _mock_function +MOCK_CONTAINER_INFO = { + "whatever": ContainerInfo( + "doesn't really matter", PROXY_NGINX_SERVICE_NAME, "who cares?" + ) +} + + @pytest.mark.parametrize( "manager_start_fails, webserver_start_fails, " "probe_init_fails, probe_start_fails, proxy_start_fails, " @@ -144,7 +152,10 @@ async def test_runner_startup_shutdown( """Test if runner components are started and shut down correctly.""" manager_start_network = mock_function( - ComposeNetworkManager, "start_network", manager_start_fails + ComposeNetworkManager, + "start_network", + manager_start_fails, + result=MOCK_CONTAINER_INFO, ) manager_stop_network = mock_function( ComposeNetworkManager, "stop_network", manager_stop_fails @@ -200,20 +211,27 @@ async def test_runner_startup_shutdown( assert "Starting probes failed: MockError" in caplog.text +_FUNCTIONS_TO_MOCK = ( + ( + ComposeNetworkManager, + ("start_network", "stop_network"), + (MOCK_CONTAINER_INFO, None), + ), + (WebServer, ("start", "stop"), (None, None)), + (Probe, ("remove", "start", "stop"), (None, None, None)), + (Proxy, ("start", "stop"), (None, None)), + (Runner, ("check_assertion_errors",), (None,)), +) + + @pytest.mark.parametrize("have_test_failure", [False, True]) @pytest.mark.asyncio async def test_runner_test_failure(mock_function, have_test_failure): """Test if test failure callback is called if a TestFailure is raised.""" - for class_, funcs in ( - (ComposeNetworkManager, ("start_network", "stop_network")), - (WebServer, ("start", "stop")), - (Probe, ("remove", "start", "stop")), - (Proxy, ("start", "stop")), - (Runner, ("check_assertion_errors",)), - ): - for func in funcs: - mock_function(class_, func) + for class_, funcs, results in _FUNCTIONS_TO_MOCK: + for func, result in zip(funcs, results): + mock_function(class_, func, result=result) test_failure_callback = mock.MagicMock() runner = mock_runner(test_failure_callback=test_failure_callback) @@ -230,15 +248,9 @@ async def test_runner_test_failure(mock_function, have_test_failure): async def test_runner_cancelled(mock_function, cancel): """Test that cancellation callback is called if a runner is cancelled.""" - for class_, funcs in ( - (ComposeNetworkManager, ("start_network", "stop_network")), - (WebServer, ("start", "stop")), - (Probe, ("remove", "start", "stop")), - (Proxy, ("start", "stop")), - (Runner, ("check_assertion_errors",)), - ): - for func in funcs: - mock_function(class_, func) + for class_, funcs, results in _FUNCTIONS_TO_MOCK: + for func, result in zip(funcs, results): + mock_function(class_, func, result=result) cancellation_callback = mock.MagicMock() runner = mock_runner(cancellation_callback=cancellation_callback)