diff --git a/test/conftest.py b/test/conftest.py index 39709f7f50f686bfc86f6ceefba65ea38122f9d1..5d45f629227fa70228454a0460e21ac458f00f76 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -2,6 +2,7 @@ import io import json import os import re +import socket import sys import threading from types import SimpleNamespace @@ -27,6 +28,9 @@ from tarantool.error import ( # type: ignore # A constant represents invalid id of raft. # pub const INVALID_ID: u64 = 0; INVALID_RAFT_ID = 0 +BASE_HOST = "127.0.0.1" +BASE_PORT = 3300 +PORT_RANGE = 200 def eprint(*args, **kwargs): @@ -51,6 +55,32 @@ def pytest_addoption(parser: pytest.Parser): ) +@pytest.fixture(scope="session") +def port_range(xdist_worker_number: int) -> tuple[int, int]: + """ + Return pair (base_port, max_port) available for current pytest subprocess. + Ensures that all ports in this range are not in use. + Executes once due scope="session". + + Note: this function has a side-effect. + """ + + assert isinstance(xdist_worker_number, int) + assert xdist_worker_number >= 0 + base_port = BASE_PORT + xdist_worker_number * PORT_RANGE + + max_port = base_port + PORT_RANGE - 1 + assert max_port <= 65535 + + for port in range(base_port, max_port + 1): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind((BASE_HOST, port)) + s.close() + + return (base_port, max_port) + + @pytest.fixture(scope="session") def seed(pytestconfig): """Return a seed for randomized tests. Unless passed via @@ -547,6 +577,9 @@ class Cluster: case _: raise Exception("unreachable") + port = self.base_port + i + assert self.base_port <= port <= self.max_port + instance = Instance( binary_path=self.binary_path, cluster_id=self.id, @@ -554,14 +587,13 @@ class Cluster: replicaset_id=replicaset_id, data_dir=f"{self.data_dir}/i{i}", host=self.base_host, - port=self.base_port + i, + port=port, peers=peers or [f"{self.base_host}:{self.base_port + 1}"], init_replication_factor=init_replication_factor, color=CLUSTER_COLORS[len(self.instances) % len(CLUSTER_COLORS)], failure_domain=failure_domain, ) - assert self.base_port <= instance.port <= self.max_port self.instances.append(instance) if wait_online: @@ -647,26 +679,15 @@ def cluster_ids(xdist_worker_number) -> Iterator[str]: @pytest.fixture def cluster( - binary_path, - tmpdir, - xdist_worker_number, - cluster_ids, + binary_path, tmpdir, cluster_ids, port_range ) -> Generator[Cluster, None, None]: """Return a `Cluster` object capable of deploying test clusters.""" - n = xdist_worker_number - assert isinstance(n, int) - assert n >= 0 - - # Provide each worker a dedicated pool of 200 listening ports - base_port = 3300 + n * 200 - max_port = base_port + 199 - assert max_port <= 65535 - + base_port, max_port = port_range cluster = Cluster( binary_path=binary_path, id=next(cluster_ids), data_dir=tmpdir, - base_host="127.0.0.1", + base_host=BASE_HOST, base_port=base_port, max_port=max_port, )