Files
scylladb/test/pylib/suite/python.py
Evgeniy Naydanov 39a10d6d67 test: remove dead suite subclasses and legacy execution pipeline
After all test suites migrated to test_config.yaml with type: Python,
the specialized suite classes (Topology, CQLApproval, Run, Tool) and
the legacy execution pipeline (find_tests, run_test, TestSuite.run,
Test.run) became unreachable. Remove all this dead code.

Deleted files:
- suite/topology.py, suite/cql_approval.py, suite/run.py, suite/tool.py

Simplified:
- base.py: remove run_test(), read_log(), TestSuite.run(),
  add_test_list(), build_test_list(), all_tests(), test_count(),
  SUITE_CONFIG_FILENAME, disabled/flaky test tracking, and dead
  Test attributes (args, core_args, valid_exit_codes, allure_dir,
  is_flaky, is_cancelled, etc.)
- python.py: remove PythonTestSuite.run(), PythonTest.run(),
  _prepare_pytest_params(), pattern, test_file_ext, xmlout,
  server_log, scylla_env setup, and shlex import.
  Simplify run_ctx() to take no parameters.
- runner.py: remove --scylla-log-filename option,
  print_scylla_log_filename fixture, SUITE_CONFIG_FILENAME import,
  and suite.yaml probe in TestSuiteConfig.from_pytest_node().
- __init__.py: remove re-exports of deleted classes.
- test_config.yaml: Topology -> Python, Approval -> Python.
- conftest files: run_ctx(options=...) -> run_ctx().
- docs/dev/testing.md: update to reflect current pytest-based
  architecture, log paths, and removed features.

Co-Authored-By: Claude Opus 4.6 (200K context) <noreply@anthropic.com>

Closes scylladb/scylladb#29613
2026-05-17 22:16:31 +03:00

233 lines
10 KiB
Python

#
# Copyright (C) 2025-present ScyllaDB
#
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.1
#
from __future__ import annotations
import logging
import os
import pathlib
from contextlib import asynccontextmanager
from functools import cache
from typing import TYPE_CHECKING
from test import path_to
from test.pylib.pool import Pool
from test.pylib.scylla_cluster import ScyllaCluster, ScyllaServer, merge_cmdline_options, get_current_version_description
from test.pylib.suite.base import Test, TestSuite
from test.pylib.util import LogPrefixAdapter
if TYPE_CHECKING:
import argparse
from collections.abc import Callable, Awaitable, AsyncGenerator
from typing import Optional, Union
from pytest import Parser
class PythonTestSuite(TestSuite):
"""A collection of Python pytests against a single Scylla instance"""
def __init__(self, path, cfg: dict, options: argparse.Namespace, mode: str) -> None:
super().__init__(path, cfg, options, mode)
self.scylla_exe = path_to(self.mode, "scylla")
cluster_cfg = self.cfg.get("cluster", {"initial_size": 1})
cluster_size = cluster_cfg["initial_size"]
env_pool_size = os.getenv("CLUSTER_POOL_SIZE")
if options.cluster_pool_size is not None:
pool_size = options.cluster_pool_size
elif env_pool_size is not None:
pool_size = int(env_pool_size)
else:
pool_size = cfg.get("pool_size", 2)
self.dirties_cluster = set(cfg.get("dirties_cluster", []))
self.create_cluster = self.get_cluster_factory(cluster_size, options)
async def recycle_cluster(cluster: ScyllaCluster) -> None:
"""When a dirty cluster is returned to the cluster pool,
stop it and release the used IPs. We don't necessarily uninstall() it yet,
which would delete the log file and directory - we might want to preserve
these if it came from a failed test.
"""
for srv in cluster.servers.values():
if srv.log_file is not None:
srv.log_file.close()
srv.maintenance_socket_dir.cleanup()
await cluster.stop()
# Close API client to release connector resources
if cluster.api is not None:
cluster.api.close()
cluster.api = None
await cluster.release_ips()
self.clusters = Pool(pool_size, self.create_cluster, recycle_cluster)
def get_cluster_factory(self, cluster_size: int, options: argparse.Namespace) -> Callable[..., Awaitable]:
def create_server(create_cfg: ScyllaCluster.CreateServerParams):
cmdline_options = self.cfg.get("extra_scylla_cmdline_options", [])
if type(cmdline_options) == str:
cmdline_options = [cmdline_options]
cmdline_options = merge_cmdline_options(cmdline_options, create_cfg.cmdline_from_test)
cmdline_options = merge_cmdline_options(cmdline_options, options.extra_scylla_cmdline_options.split())
# There are multiple sources of config options, with increasing priority
# (if two sources provide the same config option, the higher priority one wins):
# 1. the defaults
# 2. suite-specific config options (in "extra_scylla_config_options")
# 3. config options from tests (when servers are added during a test)
default_config_options = \
{"authenticator": "PasswordAuthenticator",
"authorizer": "CassandraAuthorizer"}
default_config_options["tablets_initial_scale_factor"] = 4 if self.mode == "release" else 2
config_options = default_config_options | \
self.cfg.get("extra_scylla_config_options", {}) | \
create_cfg.config_from_test
server = ScyllaServer(
mode=self.mode,
version=(create_cfg.version or get_current_version_description(self.scylla_exe)),
vardir=self.log_dir,
logger=create_cfg.logger,
cluster_name=create_cfg.cluster_name,
ip_addr=create_cfg.ip_addr,
seeds=create_cfg.seeds,
cmdline_options=cmdline_options,
config_options=config_options,
property_file=create_cfg.property_file,
append_env=self.base_env,
server_encryption=create_cfg.server_encryption)
return server
async def create_cluster(logger: Union[logging.Logger, logging.LoggerAdapter]) -> ScyllaCluster:
cluster = ScyllaCluster(logger, self.hosts, cluster_size, create_server)
async def stop() -> None:
await cluster.stop()
# Suite artifacts are removed when
# the entire suite ends successfully.
self.artifacts.add_suite_artifact(self, stop)
if not self.options.save_log_on_success:
# If a test fails, we might want to keep the data dirs.
async def uninstall() -> None:
await cluster.uninstall()
self.artifacts.add_suite_artifact(self, uninstall)
self.artifacts.add_exit_artifact(self, stop)
await cluster.install_and_start()
# If cluster failed to start, raise the exception immediately
# so the pool doesn't return a broken cluster to tests
if cluster.start_exception is not None:
# Clean up the broken cluster before raising
try:
await cluster.stop()
if cluster.api is not None:
await cluster.api.close()
cluster.api = None
await cluster.release_ips()
except:
pass # Ignore cleanup errors
raise cluster.start_exception
return cluster
return create_cluster
async def add_test(self, shortname, casename) -> None:
test = PythonTest(self.next_id((shortname, self.suite_key)), shortname, casename, self)
self.tests.append(test)
class PythonTest(Test):
"""Run a pytest collection of cases against a standalone Scylla"""
def __init__(self, test_no: int, shortname: str, casename: str, suite) -> None:
super().__init__(test_no, shortname, suite)
self.casename = casename
self.server_address: str | None = None
self.server_log_filename: Optional[pathlib.Path] = None
self.is_before_test_ok = False
self.is_after_test_ok = False
@asynccontextmanager
async def run_ctx(self) -> AsyncGenerator[None]:
"""A test's setup/teardown context manager.
Important part of this code is getting a ScyllaDB node from the pool and providing an address to the host
as a `--host` argument. This node returned to the pool after test is finished. If the test was failed then
the node will be marked as dirty.
"""
loggerPrefix = self.mode + '/' + self.uname
logger = LogPrefixAdapter(logging.getLogger(loggerPrefix), {'prefix': loggerPrefix})
cluster = None
try:
cluster = await self.suite.clusters.get(logger)
cluster.before_test(self.uname)
logger.info("Leasing Scylla cluster %s for test %s", cluster, self.uname)
self.server_address = cluster.endpoint()
self.server_log_filename = cluster.server_log_filename()
self.is_before_test_ok = True
cluster.take_log_savepoint()
yield cluster
if self.shortname in self.suite.dirties_cluster:
cluster.is_dirty = True
cluster.after_test(self.uname, self.success)
self.is_after_test_ok = True
except Exception as e:
if not self.is_before_test_ok:
print(f"Test {self.name} pre-check failed: {str(e)}\ncheck server logs: {self.server_log_filename}")
logger.info(f"Discarding cluster after failed start for test %s...", self.name)
elif not self.is_after_test_ok:
print(f"Test {self.name} post-check failed: {str(e)}\ncheck server logs: {self.server_log_filename}")
logger.info(f"Discarding cluster after failed test %s...", self.name)
self.success = False
if cluster is not None:
cluster.is_dirty = True
raise
finally:
if cluster is not None:
await self.suite.clusters.put(cluster, is_dirty=cluster.is_dirty)
logger.info("Test %s %s", self.uname, "succeeded" if self.success else "failed ")
# Use cache to execute this function once per pytest session.
@cache
def add_host_option(parser: Parser) -> None:
parser.addoption("--host", default="localhost",
help="a DB server host to connect to")
# Use cache to execute this function once per pytest session.
@cache
def add_cql_connection_options(parser: Parser) -> None:
"""Add pytest options for a CQL connection."""
cql_options = parser.getgroup("CQL connection options")
cql_options.addoption("--port", default="9042",
help="CQL port to connect to")
cql_options.addoption("--ssl", action="store_true",
help="Connect to CQL via an encrypted TLSv1.2 connection", default=False)
cql_options.addoption("--auth_username",
help="username for authentication", default=None)
cql_options.addoption("--auth_password",
help="password for authentication", default=None)
# Use cache to execute this function once per pytest session.
@cache
def add_s3_options(parser: Parser) -> None:
"""Options for tests which use S3 server (i.e., cluster/object_store and cqlpy/test_tools.py)"""
s3_options = parser.getgroup("S3 server settings")
s3_options.addoption('--s3-server-address', default=None)
s3_options.addoption('--s3-server-port', type=int, default=None)
s3_options.addoption('--aws-access-key', default=None)
s3_options.addoption('--aws-secret-key', default=None)
s3_options.addoption('--aws-region', default=None)
s3_options.addoption('--s3-server-bucket', default=None)