Files
scylladb/test/conftest.py
Andrei Chekun 3cb5838619 test.py: move setup cgroups to the generic method
This changes needed for later integration for pytest executing the C++
tests to be able to gather resource metric.
2025-04-24 14:05:49 +02:00

181 lines
8.1 KiB
Python

#
# Copyright (C) 2024-present ScyllaDB
#
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
#
from __future__ import annotations
import asyncio
import logging
import sys
from argparse import BooleanOptionalAction
from pathlib import Path
from random import randint
from typing import TYPE_CHECKING
import pytest
from test import ALL_MODES, TEST_RUNNER, TOP_SRC_DIR
from test.pylib.report_plugin import ReportPlugin
from test.pylib.util import get_configured_modes
from test.pylib.suite.base import (
TestSuite,
find_suite_config,
init_testsuite_globals,
prepare_dirs,
start_3rd_party_services,
)
if TYPE_CHECKING:
from asyncio import AbstractEventLoop
from collections.abc import Generator
from test.pylib.cpp.item import CppTestFunction
from test.pylib.suite.base import Test
logger = logging.getLogger(__name__)
def pytest_addoption(parser: pytest.Parser) -> None:
parser.addoption('--mode', choices=ALL_MODES, action="append", dest="modes",
help="Run only tests for given build mode(s)")
parser.addoption('--tmpdir', action='store', default=str(TOP_SRC_DIR / 'testlog'),
help='Path to temporary test data and log files. The data is further segregated per build mode.')
parser.addoption('--run_id', action='store', default=None, help='Run id for the test run')
parser.addoption('--byte-limit', action="store", default=randint(0, 2000), type=int,
help="Specific byte limit for failure injection (random by default)")
parser.addoption("--gather-metrics", action=BooleanOptionalAction, default=False,
help='Switch on gathering cgroup metrics')
# Following option is to use with bare pytest command.
#
# For compatibility with reasons need to run bare pytest with --test-py-init option
# to run a test.py-compatible pytest session.
#
# TODO: remove this when we'll completely switch to bare pytest runner.
parser.addoption('--test-py-init', action='store_true', default=False,
help='Run pytest session in test.py-compatible mode. I.e., start all required services, etc.')
# Options for compatibility with test.py
parser.addoption('--save-log-on-success', default=False,
dest="save_log_on_success", action="store_true",
help="Save test log output on success.")
parser.addoption('--coverage', action='store_true', default=False,
help="When running code instrumented with coverage support"
"Will route the profiles to `tmpdir`/mode/coverage/`suite` and post process them in order to generate "
"lcov file per suite, lcov file per mode, and an lcov file for the entire run, "
"The lcov files can eventually be used for generating coverage reports")
parser.addoption("--cluster-pool-size", type=int,
help="Set the pool_size for PythonTest and its descendants. Alternatively environment variable "
"CLUSTER_POOL_SIZE can be used to achieve the same")
parser.addoption("--extra-scylla-cmdline-options", default=[],
help="Passing extra scylla cmdline options for all tests. Options should be space separated:"
" '--logger-log-level raft=trace --default-log-level error'")
# Pass information about Scylla node from test.py to pytest.
parser.addoption("--scylla-log-filename",
help="Path to a log file of a ScyllaDB node (for suites with type: Python)")
@pytest.fixture(scope="session")
def build_mode(request: pytest.FixtureRequest) -> str:
"""
This fixture returns current build mode.
This is for running tests through the test.py script, where only one mode is passed to the test
"""
# to avoid issues when there's no provided mode parameter, do it in two steps: get the parameter and if it's not
# None, get the first value from the list
mode = request.config.getoption("modes")
if mode:
return mode[0]
return mode
@pytest.fixture(autouse=True)
def print_scylla_log_filename(request: pytest.FixtureRequest) -> Generator[None]:
"""Print out a path to a ScyllaDB log.
This is a fixture for Python test suites, because they are using a single node clusters created inside test.py,
but it is handy to have this information printed to a pytest log.
"""
yield
if scylla_log_filename := request.config.getoption("--scylla-log-filename"):
logger.info("ScyllaDB log file: %s", scylla_log_filename)
@pytest.fixture(scope="module")
async def testpy_testsuite(request: pytest.FixtureRequest, build_mode: str) -> TestSuite:
suite_config = find_suite_config(path=request.path)
return TestSuite.opt_create(path=str(suite_config.parent), options=request.config.option, mode=build_mode)
@pytest.fixture(scope="module")
async def testpy_test(request: pytest.FixtureRequest, testpy_testsuite: TestSuite) -> Test:
# this name modification is done to have the same output as everywhere is used:
# suite_name.directory_subdirectory_file
# The first directory represents suite, and it deleted since later it added in Test class
shortname = "_".join(request.node.nodeid.split('.')[0].split('/')[1:])
await testpy_testsuite.add_test(shortname=shortname, casename=None)
return testpy_testsuite.tests[-1] # most recent test added to the test suite; i.e., by the previous line.
def pytest_configure(config: pytest.Config) -> None:
config.pluginmanager.register(ReportPlugin())
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item | CppTestFunction]) -> None:
"""
This is a standard pytest method.
This is needed to modify the test names with dev mode and run id to differ them one from another
"""
run_id = config.getoption('run_id', None)
for item in items:
# check if this is custom cpp tests that have additional attributes for name modification
if hasattr(item, 'mode'):
# modify name with mode that is always present in cpp tests
item.nodeid = f'{item.nodeid}.{item.mode}'
item.name = f'{item.name}.{item.mode}'
if item.run_id:
item.nodeid = f'{item.nodeid}.{item.run_id}'
item.name = f'{item.name}.{item.run_id}'
else:
# here go python tests that are executed through test.py
# since test.py is responsible for creating several tests with the required mode,
# a list with modes contains only one value,
# that's why in name modification the first element is used
modes = config.getoption('modes')
if modes:
item._nodeid = f'{item._nodeid}.{modes[0]}'
item.name = f'{item.name}.{modes[0]}'
if run_id:
item._nodeid = f'{item._nodeid}.{run_id}'
item.name = f'{item.name}.{run_id}'
def pytest_sessionstart(session: pytest.Session) -> None:
# test.py starts S3 mock and create/cleanup testlog by itself. Also, if we run with --collect-only option,
# we don't need this stuff.
if TEST_RUNNER != "pytest" or session.config.getoption("--collect-only"):
return
if not session.config.getoption("--test-py-init"):
return
init_testsuite_globals()
TestSuite.artifacts.add_exit_artifact(None, TestSuite.hosts.cleanup)
# Run stuff just once for the pytest session even running under xdist.
if "xdist" not in sys.modules or not sys.modules["xdist"].is_xdist_worker(request_or_session=session):
temp_dir = Path(session.config.getoption("--tmpdir")).absolute()
prepare_dirs(tempdir_base=temp_dir, modes=session.config.getoption("--mode") or get_configured_modes(), gather_metrics=session.config.getoption("--gather-metrics"))
start_3rd_party_services(tempdir_base=temp_dir, toxiproxy_byte_limit=session.config.getoption('byte_limit'))
def pytest_sessionfinish() -> None:
if getattr(TestSuite, "artifacts", None) is not None:
asyncio.get_event_loop().run_until_complete(TestSuite.artifacts.cleanup_before_exit())