Skip to content
This repository was archived by the owner on Aug 13, 2025. It is now read-only.

Commit 0b42f21

Browse files
committed
Assign unique ports to pytest modules
This is basically a pytest re-implementation of the get_ports.sh script. The main difference is that ports are assigned on a module basis, rather than a directory basis. Module is the new atomic unit for parallel execution, therefore it needs to have unique ports to avoid collisions. Each module gets its ports through the env fixture which is updated with ports and other module-specific variables.
1 parent 78d2865 commit 0b42f21

File tree

1 file changed

+84
-0
lines changed

1 file changed

+84
-0
lines changed

bin/tests/system/conftest.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
# ======================= LEGACY=COMPATIBLE FIXTURES =========================
1717
# The following fixtures are designed to work with both pytest system test
1818
# runner and the legacy system test framework.
19+
#
20+
# FUTURE: Rewrite the individual port fixtures to re-use the `ports` fixture.
1921

2022

2123
@pytest.fixture(scope="module")
@@ -53,12 +55,19 @@ def control_port():
5355
from pathlib import Path
5456
import re
5557
import subprocess
58+
import time
59+
60+
# Silence warnings caused by passing a pytest fixture to another fixture.
61+
# pylint: disable=redefined-outer-name
5662

5763
# ----------------------- Globals definition -----------------------------
5864

5965
XDIST_WORKER = os.environ.get("PYTEST_XDIST_WORKER", "")
6066
FILE_DIR = os.path.abspath(Path(__file__).parent)
6167
ENV_RE = re.compile("([^=]+)=(.*)")
68+
PORT_MIN = 5001
69+
PORT_MAX = 32767
70+
PORTS_PER_TEST = 20
6271

6372
# ---------------------- Module initialization ---------------------------
6473

@@ -119,3 +128,78 @@ def pytest_configure():
119128
logging.error("failed to compile test files: %s", exc)
120129
raise exc
121130
logging.debug(proc.stdout)
131+
132+
# --------------------------- Fixtures -----------------------------------
133+
134+
@pytest.fixture(scope="session")
135+
def modules():
136+
"""Sorted list of all modules. Used to determine port distribution."""
137+
mods = []
138+
for dirpath, _dirs, files in os.walk(os.getcwd()):
139+
for file in files:
140+
if file.startswith("tests_") and file.endswith(".py"):
141+
mod = f"{dirpath}/{file}"
142+
mods.append(mod)
143+
return sorted(mods)
144+
145+
@pytest.fixture(scope="session")
146+
def module_base_ports(modules):
147+
"""
148+
Dictionary containing assigned base port for every module.
149+
150+
Note that this is a session-wide fixture. The port numbers are
151+
deterministically assigned before any testing starts. This fixture MUST
152+
return the same value when called again during the same test session.
153+
When running tests in parallel, this is exactly what happens - every
154+
worker thread will call this fixture to determine test ports.
155+
"""
156+
port_min = PORT_MIN
157+
port_max = PORT_MAX - len(modules) * PORTS_PER_TEST
158+
if port_max < port_min:
159+
raise RuntimeError(
160+
"not enough ports to assign unique port set to each module"
161+
)
162+
163+
# Rotate the base port value over time to detect possible test issues
164+
# with using random ports. This introduces a very slight race condition
165+
# risk. If this value changes between pytest invocation and spawning
166+
# worker threads, multiple tests may have same port values assigned. If
167+
# these tests are then executed simultaneously, the test results will
168+
# be misleading.
169+
base_port = int(time.time() // 3600) % (port_max - port_min)
170+
171+
return {mod: base_port + i * PORTS_PER_TEST for i, mod in enumerate(modules)}
172+
173+
@pytest.fixture(scope="module")
174+
def base_port(request, module_base_ports):
175+
"""Start of the port range assigned to a particular test module."""
176+
port = module_base_ports[request.fspath]
177+
return port
178+
179+
@pytest.fixture(scope="module")
180+
def ports(base_port):
181+
"""Dictionary containing port names and their assigned values."""
182+
return {
183+
"PORT": str(base_port),
184+
"TLSPORT": str(base_port + 1),
185+
"HTTPPORT": str(base_port + 2),
186+
"HTTPSPORT": str(base_port + 3),
187+
"EXTRAPORT1": str(base_port + 4),
188+
"EXTRAPORT2": str(base_port + 5),
189+
"EXTRAPORT3": str(base_port + 6),
190+
"EXTRAPORT4": str(base_port + 7),
191+
"EXTRAPORT5": str(base_port + 8),
192+
"EXTRAPORT6": str(base_port + 9),
193+
"EXTRAPORT7": str(base_port + 10),
194+
"EXTRAPORT8": str(base_port + 11),
195+
"CONTROLPORT": str(base_port + 12),
196+
}
197+
198+
@pytest.fixture(scope="module")
199+
def env(ports):
200+
"""Dictionary containing environment variables for the test."""
201+
env = CONF_ENV.copy()
202+
env.update(ports)
203+
env["builddir"] = f"{env['TOP_BUILDDIR']}/bin/tests/system"
204+
env["srcdir"] = f"{env['TOP_SRCDIR']}/bin/tests/system"
205+
return env

0 commit comments

Comments
 (0)