2021-06-21 14:51:43 +02:00
|
|
|
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
|
|
|
#
|
2021-06-03 08:37:05 +02:00
|
|
|
# SPDX-License-Identifier: MPL-2.0
|
|
|
|
#
|
2021-06-21 14:51:43 +02:00
|
|
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
2021-06-03 08:37:05 +02:00
|
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
2021-06-21 14:51:43 +02:00
|
|
|
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
#
|
|
|
|
# See the COPYRIGHT file distributed with this work for additional
|
|
|
|
# information regarding copyright ownership.
|
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
from functools import partial
|
2021-06-21 14:51:43 +02:00
|
|
|
import os
|
2023-09-07 15:21:54 +02:00
|
|
|
from pathlib import Path
|
|
|
|
import re
|
|
|
|
import shutil
|
|
|
|
import subprocess
|
|
|
|
import tempfile
|
|
|
|
import time
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
|
2021-06-21 14:51:43 +02:00
|
|
|
import pytest
|
|
|
|
|
2023-12-04 18:05:50 +01:00
|
|
|
pytest.register_assert_rewrite("isctest")
|
|
|
|
|
2023-07-25 14:37:05 +02:00
|
|
|
import isctest
|
|
|
|
|
2023-12-04 18:05:50 +01:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
# Silence warnings caused by passing a pytest fixture to another fixture.
|
|
|
|
# pylint: disable=redefined-outer-name
|
2023-01-12 15:48:55 +01:00
|
|
|
|
|
|
|
|
2024-03-13 18:18:42 +01:00
|
|
|
isctest.log.init_conftest_logger()
|
|
|
|
isctest.log.avoid_duplicated_logs()
|
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
# ----------------- Older pytest / xdist compatibility -------------------
|
|
|
|
# As of 2023-01-11, the minimal supported pytest / xdist versions are
|
|
|
|
# determined by what is available in EL8/EPEL8:
|
|
|
|
# - pytest 3.4.2
|
|
|
|
# - pytest-xdist 1.24.1
|
|
|
|
_pytest_ver = pytest.__version__.split(".")
|
|
|
|
_pytest_major_ver = int(_pytest_ver[0])
|
|
|
|
if _pytest_major_ver < 7:
|
|
|
|
# pytest.Stash/pytest.StashKey mechanism has been added in 7.0.0
|
|
|
|
# for older versions, use regular dictionary with string keys instead
|
|
|
|
FIXTURE_OK = "fixture_ok" # type: Any
|
2023-04-04 17:44:10 +02:00
|
|
|
else:
|
2023-09-07 15:21:54 +02:00
|
|
|
FIXTURE_OK = pytest.StashKey[bool]() # pylint: disable=no-member
|
|
|
|
|
|
|
|
# ----------------------- Globals definition -----------------------------
|
|
|
|
|
|
|
|
XDIST_WORKER = os.environ.get("PYTEST_XDIST_WORKER", "")
|
|
|
|
FILE_DIR = os.path.abspath(Path(__file__).parent)
|
|
|
|
ENV_RE = re.compile(b"([^=]+)=(.*)")
|
|
|
|
PORT_MIN = 5001
|
|
|
|
PORT_MAX = 32767
|
|
|
|
PORTS_PER_TEST = 20
|
|
|
|
PRIORITY_TESTS = [
|
|
|
|
# Tests that are scheduled first. Speeds up parallel execution.
|
|
|
|
"rpz/",
|
|
|
|
"rpzrecurse/",
|
|
|
|
"serve-stale/",
|
|
|
|
"timeouts/",
|
|
|
|
"upforwd/",
|
|
|
|
]
|
|
|
|
PRIORITY_TESTS_RE = re.compile("|".join(PRIORITY_TESTS))
|
|
|
|
SYSTEM_TEST_DIR_GIT_PATH = "bin/tests/system"
|
|
|
|
SYSTEM_TEST_NAME_RE = re.compile(f"{SYSTEM_TEST_DIR_GIT_PATH}" + r"/([^/]+)")
|
2023-09-19 17:20:32 +02:00
|
|
|
SYMLINK_REPLACEMENT_RE = re.compile(r"/tests(_.*)\.py")
|
2023-09-07 15:21:54 +02:00
|
|
|
|
|
|
|
# ---------------------- Module initialization ---------------------------
|
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
def parse_env(env_bytes):
|
|
|
|
"""Parse the POSIX env format into Python dictionary."""
|
|
|
|
out = {}
|
|
|
|
for line in env_bytes.splitlines():
|
|
|
|
match = ENV_RE.match(line)
|
|
|
|
if match:
|
|
|
|
# EL8+ workaround for https://access.redhat.com/solutions/6994985
|
|
|
|
# FUTURE: can be removed when we no longer need to parse env vars
|
|
|
|
if match.groups()[0] in [b"which_declare", b"BASH_FUNC_which%%"]:
|
|
|
|
continue
|
|
|
|
out[match.groups()[0]] = match.groups()[1]
|
|
|
|
return out
|
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
def get_env_bytes(cmd):
|
|
|
|
try:
|
|
|
|
proc = subprocess.run(
|
|
|
|
[cmd],
|
|
|
|
shell=True,
|
|
|
|
check=True,
|
|
|
|
cwd=FILE_DIR,
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
)
|
|
|
|
except subprocess.CalledProcessError as exc:
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.error("failed to get shell env: %s", exc)
|
2023-09-07 15:21:54 +02:00
|
|
|
raise exc
|
|
|
|
env_bytes = proc.stdout
|
|
|
|
return parse_env(env_bytes)
|
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
# Read common environment variables for running tests from conf.sh.
|
|
|
|
# FUTURE: Remove conf.sh entirely and define all variables in pytest only.
|
|
|
|
CONF_ENV = get_env_bytes(". ./conf.sh && env")
|
|
|
|
os.environb.update(CONF_ENV)
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.debug("variables in env: %s", ", ".join([str(key) for key in CONF_ENV]))
|
2023-09-07 15:21:54 +02:00
|
|
|
|
|
|
|
# --------------------------- pytest hooks -------------------------------
|
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
def pytest_addoption(parser):
|
|
|
|
parser.addoption(
|
|
|
|
"--noclean",
|
|
|
|
action="store_true",
|
|
|
|
default=False,
|
|
|
|
help="don't remove the temporary test directories with artifacts",
|
2023-04-12 14:22:27 +02:00
|
|
|
)
|
2023-01-12 16:47:55 +01:00
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
def pytest_configure(config):
|
|
|
|
# Ensure this hook only runs on the main pytest instance if xdist is
|
|
|
|
# used to spawn other workers.
|
|
|
|
if not XDIST_WORKER:
|
|
|
|
if config.pluginmanager.has_plugin("xdist") and config.option.numprocesses:
|
|
|
|
# system tests depend on module scope for setup & teardown
|
|
|
|
# enforce use "loadscope" scheduler or disable paralelism
|
|
|
|
try:
|
|
|
|
import xdist.scheduler.loadscope # pylint: disable=unused-import
|
|
|
|
except ImportError:
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.debug(
|
2023-09-07 15:21:54 +02:00
|
|
|
"xdist is too old and does not have "
|
|
|
|
"scheduler.loadscope, disabling parallelism"
|
|
|
|
)
|
|
|
|
config.option.dist = "no"
|
|
|
|
else:
|
|
|
|
config.option.dist = "loadscope"
|
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
def pytest_ignore_collect(path):
|
|
|
|
# System tests are executed in temporary directories inside
|
|
|
|
# bin/tests/system. These temporary directories contain all files
|
|
|
|
# needed for the system tests - including tests_*.py files. Make sure to
|
|
|
|
# ignore these during test collection phase. Otherwise, test artifacts
|
|
|
|
# from previous runs could mess with the runner. Also ignore the
|
|
|
|
# convenience symlinks to those test directories. In both of those
|
|
|
|
# cases, the system test name (directory) contains an underscore, which
|
|
|
|
# is otherwise and invalid character for a system test name.
|
|
|
|
match = SYSTEM_TEST_NAME_RE.search(str(path))
|
|
|
|
if match is None:
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.warning("unexpected test path: %s (ignored)", path)
|
2023-09-07 15:21:54 +02:00
|
|
|
return True
|
|
|
|
system_test_name = match.groups()[0]
|
|
|
|
return "_" in system_test_name
|
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
def pytest_collection_modifyitems(items):
|
|
|
|
"""Schedule long-running tests first to get more benefit from parallelism."""
|
|
|
|
priority = []
|
|
|
|
other = []
|
|
|
|
for item in items:
|
|
|
|
if PRIORITY_TESTS_RE.search(item.nodeid):
|
|
|
|
priority.append(item)
|
|
|
|
else:
|
|
|
|
other.append(item)
|
|
|
|
items[:] = priority + other
|
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
class NodeResult:
|
|
|
|
def __init__(self, report=None):
|
|
|
|
self.outcome = None
|
|
|
|
self.messages = []
|
|
|
|
if report is not None:
|
|
|
|
self.update(report)
|
|
|
|
|
|
|
|
def update(self, report):
|
|
|
|
if self.outcome is None or report.outcome != "passed":
|
|
|
|
self.outcome = report.outcome
|
|
|
|
if report.longreprtext:
|
|
|
|
self.messages.append(report.longreprtext)
|
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
|
|
|
def pytest_runtest_makereport(item):
|
|
|
|
"""Hook that is used to expose test results to session (for use in fixtures)."""
|
|
|
|
# execute all other hooks to obtain the report object
|
|
|
|
outcome = yield
|
|
|
|
report = outcome.get_result()
|
|
|
|
|
|
|
|
# Set the test outcome in session, so we can access it from module-level
|
|
|
|
# fixture using nodeid. Note that this hook is called three times: for
|
|
|
|
# setup, call and teardown. We only care about the overall result so we
|
|
|
|
# merge the results together and preserve the information whether a test
|
|
|
|
# passed.
|
|
|
|
test_results = {}
|
|
|
|
try:
|
|
|
|
test_results = getattr(item.session, "test_results")
|
|
|
|
except AttributeError:
|
|
|
|
setattr(item.session, "test_results", test_results)
|
|
|
|
node_result = test_results.setdefault(item.nodeid, NodeResult())
|
|
|
|
node_result.update(report)
|
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
# --------------------------- Fixtures -----------------------------------
|
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
def modules():
|
|
|
|
"""
|
|
|
|
Sorted list of ALL modules.
|
|
|
|
|
|
|
|
The list includes even test modules that are not tested in the current
|
|
|
|
session. It is used to determine port distribution. Using a complete
|
|
|
|
list of all possible test modules allows independent concurrent pytest
|
|
|
|
invocations.
|
|
|
|
"""
|
|
|
|
mods = []
|
|
|
|
for dirpath, _dirs, files in os.walk(FILE_DIR):
|
|
|
|
for file in files:
|
|
|
|
if file.startswith("tests_") and file.endswith(".py"):
|
|
|
|
mod = f"{dirpath}/{file}"
|
|
|
|
if not pytest_ignore_collect(mod):
|
|
|
|
mods.append(mod)
|
|
|
|
return sorted(mods)
|
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
def module_base_ports(modules):
|
|
|
|
"""
|
|
|
|
Dictionary containing assigned base port for every module.
|
|
|
|
|
|
|
|
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:
|
2023-09-07 15:22:23 +02:00
|
|
|
raise RuntimeError("not enough ports to assign unique port set to each module")
|
2023-01-12 17:38:06 +01:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
# 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) + port_min
|
|
|
|
|
|
|
|
return {mod: base_port + i * PORTS_PER_TEST for i, mod in enumerate(modules)}
|
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
@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
|
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
@pytest.fixture(scope="module")
|
|
|
|
def ports(base_port):
|
|
|
|
"""Dictionary containing port names and their assigned values."""
|
|
|
|
return {
|
2023-08-15 13:55:56 +02:00
|
|
|
"PORT": base_port,
|
|
|
|
"TLSPORT": base_port + 1,
|
|
|
|
"HTTPPORT": base_port + 2,
|
|
|
|
"HTTPSPORT": base_port + 3,
|
|
|
|
"EXTRAPORT1": base_port + 4,
|
|
|
|
"EXTRAPORT2": base_port + 5,
|
|
|
|
"EXTRAPORT3": base_port + 6,
|
|
|
|
"EXTRAPORT4": base_port + 7,
|
|
|
|
"EXTRAPORT5": base_port + 8,
|
|
|
|
"EXTRAPORT6": base_port + 9,
|
|
|
|
"EXTRAPORT7": base_port + 10,
|
|
|
|
"EXTRAPORT8": base_port + 11,
|
|
|
|
"CONTROLPORT": base_port + 12,
|
2023-09-07 15:21:54 +02:00
|
|
|
}
|
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-08-15 13:55:56 +02:00
|
|
|
@pytest.fixture(scope="module")
|
|
|
|
def named_port(ports):
|
|
|
|
return ports["PORT"]
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
|
|
def named_tlsport(ports):
|
|
|
|
return ports["TLSPORT"]
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
|
|
def named_httpsport(ports):
|
|
|
|
return ports["HTTPSPORT"]
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
|
|
def control_port(ports):
|
|
|
|
return ports["CONTROLPORT"]
|
|
|
|
|
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
@pytest.fixture(scope="module")
|
|
|
|
def env(ports):
|
|
|
|
"""Dictionary containing environment variables for the test."""
|
|
|
|
env = os.environ.copy()
|
2023-08-15 13:55:56 +02:00
|
|
|
for portname, portnum in ports.items():
|
|
|
|
env[portname] = str(portnum)
|
2023-09-07 15:21:54 +02:00
|
|
|
env["builddir"] = f"{env['TOP_BUILDDIR']}/{SYSTEM_TEST_DIR_GIT_PATH}"
|
|
|
|
env["srcdir"] = f"{env['TOP_SRCDIR']}/{SYSTEM_TEST_DIR_GIT_PATH}"
|
|
|
|
return env
|
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
@pytest.fixture(scope="module")
|
|
|
|
def system_test_name(request):
|
|
|
|
"""Name of the system test directory."""
|
|
|
|
path = Path(request.fspath)
|
|
|
|
return path.parent.name
|
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-12-19 15:58:36 +01:00
|
|
|
def _get_marker(node, marker):
|
|
|
|
try:
|
|
|
|
# pytest >= 4.x
|
|
|
|
return node.get_closest_marker(marker)
|
|
|
|
except AttributeError:
|
|
|
|
# pytest < 4.x
|
|
|
|
return node.get_marker(marker)
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
def wait_for_zones_loaded(request, servers):
|
|
|
|
"""Wait for all zones to be loaded by specified named instances."""
|
|
|
|
instances = _get_marker(request.node, "requires_zones_loaded")
|
|
|
|
if not instances:
|
|
|
|
return
|
|
|
|
|
|
|
|
for instance in instances.args:
|
|
|
|
with servers[instance].watch_log_from_start() as watcher:
|
|
|
|
watcher.wait_for_line("all zones loaded")
|
|
|
|
|
|
|
|
|
2024-02-15 14:47:13 +01:00
|
|
|
@pytest.fixture(autouse=True)
|
2023-09-07 15:21:54 +02:00
|
|
|
def logger(request, system_test_name):
|
2024-02-15 14:47:13 +01:00
|
|
|
"""Sets up logging facility specific to a particular test."""
|
|
|
|
isctest.log.init_test_logger(system_test_name, request.node.name)
|
|
|
|
yield
|
|
|
|
isctest.log.deinit_test_logger()
|
2023-09-07 15:21:54 +02:00
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
@pytest.fixture(scope="module")
|
|
|
|
def system_test_dir(
|
2024-02-15 14:47:13 +01:00
|
|
|
request, env, system_test_name
|
2023-09-07 15:21:54 +02:00
|
|
|
): # pylint: disable=too-many-statements,too-many-locals
|
|
|
|
"""
|
|
|
|
Temporary directory for executing the test.
|
|
|
|
|
|
|
|
This fixture is responsible for creating (and potentially removing) a
|
|
|
|
copy of the system test directory which is used as a temporary
|
|
|
|
directory for the test execution.
|
|
|
|
|
|
|
|
FUTURE: This removes the need to have clean.sh scripts.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def get_test_result():
|
|
|
|
"""Aggregate test results from all individual tests from this module
|
|
|
|
into a single result: failed > skipped > passed."""
|
2023-01-12 17:41:35 +01:00
|
|
|
try:
|
2023-09-07 15:21:54 +02:00
|
|
|
all_test_results = request.session.test_results
|
2023-01-12 17:41:35 +01:00
|
|
|
except AttributeError:
|
2023-09-07 15:21:54 +02:00
|
|
|
# This may happen if pytest execution is interrupted and
|
|
|
|
# pytest_runtest_makereport() is never called.
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.debug("can't obtain test results, test run was interrupted")
|
2023-09-07 15:21:54 +02:00
|
|
|
return "error"
|
|
|
|
test_results = {
|
|
|
|
node.nodeid: all_test_results[node.nodeid]
|
|
|
|
for node in request.node.collect()
|
|
|
|
if node.nodeid in all_test_results
|
2023-01-12 16:52:49 +01:00
|
|
|
}
|
2023-09-07 15:21:54 +02:00
|
|
|
assert len(test_results)
|
|
|
|
messages = []
|
|
|
|
for node, result in test_results.items():
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.debug("%s %s", result.outcome.upper(), node)
|
2023-09-07 15:21:54 +02:00
|
|
|
messages.extend(result.messages)
|
|
|
|
for message in messages:
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.debug("\n" + message)
|
2023-09-07 15:21:54 +02:00
|
|
|
failed = any(res.outcome == "failed" for res in test_results.values())
|
|
|
|
skipped = any(res.outcome == "skipped" for res in test_results.values())
|
|
|
|
if failed:
|
|
|
|
return "failed"
|
|
|
|
if skipped:
|
|
|
|
return "skipped"
|
|
|
|
assert all(res.outcome == "passed" for res in test_results.values())
|
|
|
|
return "passed"
|
|
|
|
|
|
|
|
def unlink(path):
|
2023-01-12 17:30:22 +01:00
|
|
|
try:
|
2023-09-07 15:21:54 +02:00
|
|
|
path.unlink() # missing_ok=True isn't available on Python 3.6
|
|
|
|
except FileNotFoundError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
# Create a temporary directory with a copy of the original system test dir contents
|
|
|
|
system_test_root = Path(f"{env['TOP_BUILDDIR']}/{SYSTEM_TEST_DIR_GIT_PATH}")
|
|
|
|
testdir = Path(
|
|
|
|
tempfile.mkdtemp(prefix=f"{system_test_name}_tmp_", dir=system_test_root)
|
|
|
|
)
|
|
|
|
shutil.rmtree(testdir)
|
|
|
|
shutil.copytree(system_test_root / system_test_name, testdir)
|
|
|
|
|
|
|
|
# Create a convenience symlink with a stable and predictable name
|
2023-09-19 17:20:32 +02:00
|
|
|
module_name = SYMLINK_REPLACEMENT_RE.sub(r"\1", request.node.name)
|
2023-09-07 15:21:54 +02:00
|
|
|
symlink_dst = system_test_root / module_name
|
|
|
|
unlink(symlink_dst)
|
|
|
|
symlink_dst.symlink_to(os.path.relpath(testdir, start=system_test_root))
|
|
|
|
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.init_module_logger(system_test_name, testdir)
|
2023-09-07 15:21:54 +02:00
|
|
|
|
|
|
|
# System tests are meant to be executed from their directory - switch to it.
|
|
|
|
old_cwd = os.getcwd()
|
|
|
|
os.chdir(testdir)
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.info("switching to tmpdir: %s", testdir)
|
2023-09-07 15:21:54 +02:00
|
|
|
try:
|
|
|
|
yield testdir # other fixtures / tests will execute here
|
|
|
|
finally:
|
|
|
|
os.chdir(old_cwd)
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.debug("changed workdir to: %s", old_cwd)
|
2023-09-07 15:21:54 +02:00
|
|
|
|
|
|
|
result = get_test_result()
|
|
|
|
|
|
|
|
# Clean temporary dir unless it should be kept
|
|
|
|
keep = False
|
|
|
|
if request.config.getoption("--noclean"):
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.debug(
|
2023-09-07 15:21:54 +02:00
|
|
|
"--noclean requested, keeping temporary directory %s", testdir
|
|
|
|
)
|
|
|
|
keep = True
|
|
|
|
elif result == "failed":
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.debug(
|
2023-09-07 15:21:54 +02:00
|
|
|
"test failure detected, keeping temporary directory %s", testdir
|
|
|
|
)
|
|
|
|
keep = True
|
|
|
|
elif not request.node.stash[FIXTURE_OK]:
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.debug(
|
2023-09-07 15:21:54 +02:00
|
|
|
"test setup/teardown issue detected, keeping temporary directory %s",
|
|
|
|
testdir,
|
|
|
|
)
|
|
|
|
keep = True
|
2023-08-10 16:24:38 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
if keep:
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.info(
|
2023-09-07 15:21:54 +02:00
|
|
|
"test artifacts in: %s", symlink_dst.relative_to(system_test_root)
|
|
|
|
)
|
|
|
|
else:
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.debug("deleting temporary directory")
|
|
|
|
|
|
|
|
isctest.log.deinit_module_logger()
|
|
|
|
if not keep:
|
2023-09-07 15:21:54 +02:00
|
|
|
shutil.rmtree(testdir)
|
|
|
|
unlink(symlink_dst)
|
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
def _run_script( # pylint: disable=too-many-arguments
|
|
|
|
env,
|
|
|
|
system_test_dir: Path,
|
|
|
|
interpreter: str,
|
|
|
|
script: str,
|
|
|
|
args: Optional[List[str]] = None,
|
|
|
|
):
|
|
|
|
"""Helper function for the shell / perl script invocations (through fixtures below)."""
|
|
|
|
if args is None:
|
|
|
|
args = []
|
|
|
|
path = Path(script)
|
|
|
|
if not path.is_absolute():
|
|
|
|
# make sure relative paths are always relative to system_dir
|
|
|
|
path = system_test_dir.parent / path
|
|
|
|
script = str(path)
|
|
|
|
cwd = os.getcwd()
|
|
|
|
if not path.exists():
|
|
|
|
raise FileNotFoundError(f"script {script} not found in {cwd}")
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.debug("running script: %s %s %s", interpreter, script, " ".join(args))
|
|
|
|
isctest.log.debug(" workdir: %s", cwd)
|
2023-09-07 15:21:54 +02:00
|
|
|
returncode = 1
|
|
|
|
|
|
|
|
cmd = [interpreter, script] + args
|
|
|
|
with subprocess.Popen(
|
|
|
|
cmd,
|
|
|
|
env=env,
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
stderr=subprocess.STDOUT,
|
|
|
|
bufsize=1,
|
|
|
|
universal_newlines=True,
|
|
|
|
errors="backslashreplace",
|
|
|
|
) as proc:
|
|
|
|
if proc.stdout:
|
|
|
|
for line in proc.stdout:
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.info(" %s", line.rstrip("\n"))
|
2023-09-07 15:21:54 +02:00
|
|
|
proc.communicate()
|
|
|
|
returncode = proc.returncode
|
|
|
|
if returncode:
|
|
|
|
raise subprocess.CalledProcessError(returncode, cmd)
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.debug(" exited with %d", returncode)
|
2023-01-12 17:27:07 +01:00
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
@pytest.fixture(scope="module")
|
2024-02-15 14:47:13 +01:00
|
|
|
def shell(env, system_test_dir):
|
2023-09-07 15:21:54 +02:00
|
|
|
"""Function to call a shell script with arguments."""
|
2024-02-15 14:47:13 +01:00
|
|
|
return partial(_run_script, env, system_test_dir, env["SHELL"])
|
2023-01-12 17:27:07 +01:00
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
@pytest.fixture(scope="module")
|
2024-02-15 14:47:13 +01:00
|
|
|
def perl(env, system_test_dir):
|
2023-09-07 15:21:54 +02:00
|
|
|
"""Function to call a perl script with arguments."""
|
2024-02-15 14:47:13 +01:00
|
|
|
return partial(_run_script, env, system_test_dir, env["PERL"])
|
2023-01-12 17:27:07 +01:00
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
@pytest.fixture(scope="module")
|
|
|
|
def run_tests_sh(system_test_dir, shell):
|
|
|
|
"""Utility function to execute tests.sh as a python test."""
|
|
|
|
|
|
|
|
def run_tests():
|
|
|
|
shell(f"{system_test_dir}/tests.sh")
|
|
|
|
|
|
|
|
return run_tests
|
|
|
|
|
2023-09-07 15:22:23 +02:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
@pytest.fixture(scope="module", autouse=True)
|
|
|
|
def system_test( # pylint: disable=too-many-arguments,too-many-statements
|
|
|
|
request,
|
|
|
|
env: Dict[str, str],
|
|
|
|
system_test_dir,
|
|
|
|
shell,
|
|
|
|
perl,
|
|
|
|
):
|
|
|
|
"""
|
|
|
|
Driver of the test setup/teardown process. Used automatically for every test module.
|
|
|
|
|
|
|
|
This is the most important one-fixture-to-rule-them-all. Note the
|
|
|
|
autouse=True which causes this fixture to be loaded by every test
|
|
|
|
module without the need to explicitly specify it.
|
|
|
|
|
|
|
|
When this fixture is used, it utilizes other fixtures, such as
|
|
|
|
system_test_dir, which handles the creation of the temporary test
|
|
|
|
directory.
|
|
|
|
|
|
|
|
Afterwards, it checks the test environment and takes care of starting
|
|
|
|
the servers. When everything is ready, that's when the actual tests are
|
|
|
|
executed. Once that is done, this fixture stops the servers and checks
|
|
|
|
for any artifacts indicating an issue (e.g. coredumps).
|
|
|
|
|
|
|
|
Finally, when this fixture reaches an end (or encounters an exception,
|
|
|
|
which may be caused by fail/skip invocations), any fixtures which is
|
|
|
|
used by this one are finalized - e.g. system_test_dir performs final
|
|
|
|
checks and cleans up the temporary test directory.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def check_net_interfaces():
|
|
|
|
try:
|
|
|
|
perl("testsock.pl", ["-p", env["PORT"]])
|
|
|
|
except subprocess.CalledProcessError as exc:
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.error("testsock.pl: exited with code %d", exc.returncode)
|
2023-09-07 15:21:54 +02:00
|
|
|
pytest.skip("Network interface aliases not set up.")
|
2023-01-12 17:27:07 +01:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
def check_prerequisites():
|
|
|
|
try:
|
|
|
|
shell(f"{system_test_dir}/prereq.sh")
|
|
|
|
except FileNotFoundError:
|
|
|
|
pass # prereq.sh is optional
|
|
|
|
except subprocess.CalledProcessError:
|
|
|
|
pytest.skip("Prerequisites missing.")
|
2023-01-12 17:59:51 +01:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
def setup_test():
|
|
|
|
try:
|
|
|
|
shell(f"{system_test_dir}/setup.sh")
|
|
|
|
except FileNotFoundError:
|
|
|
|
pass # setup.sh is optional
|
|
|
|
except subprocess.CalledProcessError as exc:
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.error("Failed to run test setup")
|
2023-09-07 15:21:54 +02:00
|
|
|
pytest.fail(f"setup.sh exited with {exc.returncode}")
|
2023-01-12 17:27:07 +01:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
def start_servers():
|
|
|
|
try:
|
|
|
|
perl("start.pl", ["--port", env["PORT"], system_test_dir.name])
|
|
|
|
except subprocess.CalledProcessError as exc:
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.error("Failed to start servers")
|
2023-09-07 15:21:54 +02:00
|
|
|
pytest.fail(f"start.pl exited with {exc.returncode}")
|
2023-01-12 17:59:51 +01:00
|
|
|
|
2023-09-07 15:21:54 +02:00
|
|
|
def stop_servers():
|
2023-01-12 17:27:07 +01:00
|
|
|
try:
|
2023-09-07 15:21:54 +02:00
|
|
|
perl("stop.pl", [system_test_dir.name])
|
|
|
|
except subprocess.CalledProcessError as exc:
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.error("Failed to stop servers")
|
2023-01-12 17:27:07 +01:00
|
|
|
get_core_dumps()
|
2023-09-07 15:21:54 +02:00
|
|
|
pytest.fail(f"stop.pl exited with {exc.returncode}")
|
|
|
|
|
|
|
|
def get_core_dumps():
|
|
|
|
try:
|
|
|
|
shell("get_core_dumps.sh", [system_test_dir.name])
|
|
|
|
except subprocess.CalledProcessError as exc:
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.error("Found core dumps or sanitizer reports")
|
2023-09-07 15:21:54 +02:00
|
|
|
pytest.fail(f"get_core_dumps.sh exited with {exc.returncode}")
|
|
|
|
|
|
|
|
os.environ.update(env) # Ensure pytests have the same env vars as shell tests.
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.info(f"test started: {request.node.name}")
|
2023-09-07 15:21:54 +02:00
|
|
|
port = int(env["PORT"])
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.info("using port range: <%d, %d>", port, port + PORTS_PER_TEST - 1)
|
2023-09-07 15:21:54 +02:00
|
|
|
|
|
|
|
if not hasattr(request.node, "stash"): # compatibility with pytest<7.0.0
|
|
|
|
request.node.stash = {} # use regular dict instead of pytest.Stash
|
|
|
|
request.node.stash[FIXTURE_OK] = True
|
|
|
|
|
|
|
|
# Perform checks which may skip this test.
|
|
|
|
check_net_interfaces()
|
|
|
|
check_prerequisites()
|
|
|
|
|
|
|
|
# Store the fact that this fixture hasn't successfully finished yet.
|
|
|
|
# This is checked before temporary directory teardown to decide whether
|
|
|
|
# it's okay to remove the directory.
|
|
|
|
request.node.stash[FIXTURE_OK] = False
|
|
|
|
|
|
|
|
setup_test()
|
|
|
|
try:
|
|
|
|
start_servers()
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.debug("executing test(s)")
|
2023-09-07 15:21:54 +02:00
|
|
|
yield
|
|
|
|
finally:
|
2024-02-15 14:47:13 +01:00
|
|
|
isctest.log.debug("test(s) finished")
|
2023-09-07 15:21:54 +02:00
|
|
|
stop_servers()
|
|
|
|
get_core_dumps()
|
|
|
|
request.node.stash[FIXTURE_OK] = True
|
2023-07-25 14:37:05 +02:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2024-02-15 14:47:13 +01:00
|
|
|
def servers(ports, system_test_dir):
|
2023-07-25 14:37:05 +02:00
|
|
|
instances = {}
|
|
|
|
for entry in system_test_dir.rglob("*"):
|
|
|
|
if entry.is_dir():
|
|
|
|
try:
|
|
|
|
dir_name = entry.name
|
|
|
|
# LATER: Make ports fixture return NamedPorts directly
|
|
|
|
named_ports = isctest.instance.NamedPorts(
|
|
|
|
dns=int(ports["PORT"]), rndc=int(ports["CONTROLPORT"])
|
|
|
|
)
|
2024-02-15 14:47:13 +01:00
|
|
|
instance = isctest.instance.NamedInstance(dir_name, named_ports)
|
2023-07-25 14:37:05 +02:00
|
|
|
instances[dir_name] = instance
|
|
|
|
except ValueError:
|
|
|
|
continue
|
|
|
|
return instances
|