diff --git a/bin/tests/system/conftest.py b/bin/tests/system/conftest.py index 69d40837fd..4d603c0cfe 100644 --- a/bin/tests/system/conftest.py +++ b/bin/tests/system/conftest.py @@ -16,6 +16,8 @@ import pytest # ======================= LEGACY=COMPATIBLE FIXTURES ========================= # The following fixtures are designed to work with both pytest system test # runner and the legacy system test framework. +# +# FUTURE: Rewrite the individual port fixtures to re-use the `ports` fixture. @pytest.fixture(scope="module") @@ -53,12 +55,19 @@ if os.getenv("LEGACY_TEST_RUNNER", "0") == "0": from pathlib import Path import re import subprocess + import time + + # Silence warnings caused by passing a pytest fixture to another fixture. + # pylint: disable=redefined-outer-name # ----------------------- Globals definition ----------------------------- XDIST_WORKER = os.environ.get("PYTEST_XDIST_WORKER", "") FILE_DIR = os.path.abspath(Path(__file__).parent) ENV_RE = re.compile("([^=]+)=(.*)") + PORT_MIN = 5001 + PORT_MAX = 32767 + PORTS_PER_TEST = 20 # ---------------------- Module initialization --------------------------- @@ -119,3 +128,78 @@ if os.getenv("LEGACY_TEST_RUNNER", "0") == "0": logging.error("failed to compile test files: %s", exc) raise exc logging.debug(proc.stdout) + + # --------------------------- Fixtures ----------------------------------- + + @pytest.fixture(scope="session") + def modules(): + """Sorted list of all modules. Used to determine port distribution.""" + mods = [] + for dirpath, _dirs, files in os.walk(os.getcwd()): + for file in files: + if file.startswith("tests_") and file.endswith(".py"): + mod = f"{dirpath}/{file}" + mods.append(mod) + return sorted(mods) + + @pytest.fixture(scope="session") + def module_base_ports(modules): + """ + Dictionary containing assigned base port for every module. + + Note that this is a session-wide fixture. The port numbers are + deterministically assigned before any testing starts. This fixture MUST + return the same value when called again during the same test session. + When running tests in parallel, this is exactly what happens - every + worker thread will call this fixture to determine test ports. + """ + port_min = PORT_MIN + port_max = PORT_MAX - len(modules) * PORTS_PER_TEST + if port_max < port_min: + raise RuntimeError( + "not enough ports to assign unique port set to each module" + ) + + # Rotate the base port value over time to detect possible test issues + # with using random ports. This introduces a very slight race condition + # risk. If this value changes between pytest invocation and spawning + # worker threads, multiple tests may have same port values assigned. If + # these tests are then executed simultaneously, the test results will + # be misleading. + base_port = int(time.time() // 3600) % (port_max - port_min) + + return {mod: base_port + i * PORTS_PER_TEST for i, mod in enumerate(modules)} + + @pytest.fixture(scope="module") + def base_port(request, module_base_ports): + """Start of the port range assigned to a particular test module.""" + port = module_base_ports[request.fspath] + return port + + @pytest.fixture(scope="module") + def ports(base_port): + """Dictionary containing port names and their assigned values.""" + return { + "PORT": str(base_port), + "TLSPORT": str(base_port + 1), + "HTTPPORT": str(base_port + 2), + "HTTPSPORT": str(base_port + 3), + "EXTRAPORT1": str(base_port + 4), + "EXTRAPORT2": str(base_port + 5), + "EXTRAPORT3": str(base_port + 6), + "EXTRAPORT4": str(base_port + 7), + "EXTRAPORT5": str(base_port + 8), + "EXTRAPORT6": str(base_port + 9), + "EXTRAPORT7": str(base_port + 10), + "EXTRAPORT8": str(base_port + 11), + "CONTROLPORT": str(base_port + 12), + } + + @pytest.fixture(scope="module") + def env(ports): + """Dictionary containing environment variables for the test.""" + env = CONF_ENV.copy() + env.update(ports) + env["builddir"] = f"{env['TOP_BUILDDIR']}/bin/tests/system" + env["srcdir"] = f"{env['TOP_SRCDIR']}/bin/tests/system" + return env