From 1379ff1da2154052c09a862d8c59693eb720edab Mon Sep 17 00:00:00 2001 From: Dmitry Kropachev Date: Fri, 16 Oct 2026 00:23:06 -0400 Subject: [PATCH 1/2] Fix: reuse configured CCM checkout during matrix setup CI already mounts a scylla-ccm checkout, but environment bootstrap downloads master.zip again. A GitHub 504 aborts setup before pytest can run. Pass CCM_DIR into the container and install that checkout when configured. Keep the archive fallback for direct runs without CCM_DIR. Re-raise install errors so failure metadata preserves the original cause instead of reporting a missing JUnit file. Fixes #143 --- run.py | 14 ++++-- scripts/run_test.sh | 1 + tests/test_run_setup.py | 83 +++++++++++++++++++++++++++++++++++ tests/test_run_test_script.py | 1 + 4 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 tests/test_run_setup.py diff --git a/run.py b/run.py index c2a4443..882bb87 100644 --- a/run.py +++ b/run.py @@ -2,6 +2,7 @@ import logging import os import re +import shlex import shutil import subprocess from functools import cached_property, lru_cache @@ -146,8 +147,14 @@ def _apply_patch_files(self) -> bool: @lru_cache(maxsize=None) def _create_venv(self): + ccm_source = "https://github.com/scylladb/scylla-ccm/archive/master.zip" + if self.environment.get("CCM_DIR"): + ccm_dir = Path(self.environment["CCM_DIR"]).resolve() + if not ccm_dir.is_dir(): + raise FileNotFoundError(f"CCM_DIR does not point to a directory: {ccm_dir}") + ccm_source = str(ccm_dir) basic_packages = ("pytest==7.4.4", - "https://github.com/scylladb/scylla-ccm/archive/master.zip", + ccm_source, "pytest-subtests") if self._venv_path.exists() and self._venv_path.is_dir(): logging.info("Removing old python venv in directory '%s'", self._venv_path) @@ -163,7 +170,8 @@ def _create_venv(self): pip_prefix = "" self._run_command_in_shell(cmd=venv_cmd) logging.info("Installing the following packages:\n%s", "\n".join(basic_packages)) - self._run_command_in_shell(cmd=f"{self._activate_venv_cmd()} && {pip_prefix}pip install {' '.join(basic_packages)}") + packages = shlex.join(basic_packages) + self._run_command_in_shell(cmd=f"{self._activate_venv_cmd()} && {pip_prefix}pip install {packages}") @lru_cache(maxsize=None) def _activate_venv_cmd(self): @@ -214,7 +222,7 @@ def _install_python_requirements(self): except Exception as exc: logging.error("Failed to install python requirements for version %s, with: %s", self.driver_version, str(exc)) - return False + raise def _checkout_branch(self): try: diff --git a/scripts/run_test.sh b/scripts/run_test.sh index 5ee4cd5..ab1455c 100755 --- a/scripts/run_test.sh +++ b/scripts/run_test.sh @@ -145,6 +145,7 @@ docker_cmd="docker run --init --detach=true \ -e DEV_MODE \ -e PIP_CACHE_DIR \ -e UV_CACHE_DIR \ + -e CCM_DIR \ -e PYTEST_ADDOPTS \ -e WORKSPACE \ ${BUILD_OPTIONS} \ diff --git a/tests/test_run_setup.py b/tests/test_run_setup.py new file mode 100644 index 0000000..09b8fa8 --- /dev/null +++ b/tests/test_run_setup.py @@ -0,0 +1,83 @@ +import argparse +import json +import shlex +from unittest.mock import Mock + +import pytest + +from run import Run + + +@pytest.fixture +def runner(tmp_path, monkeypatch): + monkeypatch.delenv("DEV_MODE", raising=False) + monkeypatch.delenv("CCM_DIR", raising=False) + result = Run(tmp_path, "apache", "", "3.29.3", 4, + "tests.integration.standard", "2026.1.15", False) + result.__dict__["xunit_dir"] = tmp_path + return result + + +@pytest.mark.parametrize("driver_type", ["apache", "scylla"]) +def test_bootstrap_uses_configured_ccm_checkout(runner, tmp_path, monkeypatch, driver_type): + ccm_dir = tmp_path / "CCM checkout; echo unwanted" + ccm_dir.mkdir() + monkeypatch.setenv("CCM_DIR", str(ccm_dir)) + runner._python_driver_type = driver_type + runner._run_command_in_shell = Mock() + + runner._create_venv() + + install = shlex.split(runner._run_command_in_shell.call_args.kwargs["cmd"]) + assert install[install.index("install") + 1:] == [ + "pytest==7.4.4", str(ccm_dir), "pytest-subtests", + ] + assert ("uv" in install) == (driver_type == "scylla") + + +def test_bootstrap_keeps_archive_fallback_without_ccm_dir(runner): + runner._run_command_in_shell = Mock() + + runner._create_venv() + + install = shlex.split(runner._run_command_in_shell.call_args.kwargs["cmd"]) + assert "https://github.com/scylladb/scylla-ccm/archive/master.zip" in install + + +def test_missing_configured_ccm_checkout_fails_before_install(runner, tmp_path, monkeypatch): + monkeypatch.setenv("CCM_DIR", str(tmp_path / "missing-ccm")) + runner._run_command_in_shell = Mock() + + with pytest.raises(FileNotFoundError, match="CCM_DIR"): + runner._create_venv() + + runner._run_command_in_shell.assert_not_called() + + +def test_install_failure_is_reported_with_original_cause(runner, monkeypatch): + import main + + monkeypatch.setattr(main, "Run", Mock(return_value=runner)) + monkeypatch.setattr("run.os.chdir", Mock()) + monkeypatch.setattr(runner, "_checkout_branch", Mock(return_value=True)) + monkeypatch.setattr(runner, "_apply_patch_files", Mock(return_value=True)) + monkeypatch.setattr(runner, "_run_command_in_shell", Mock(side_effect=[ + None, AssertionError("HTTP error 504 while getting scylla-ccm/archive/master.zip"), + ])) + execute_tests = Mock() + monkeypatch.setattr("run.subprocess.call", execute_tests) + arguments = argparse.Namespace( + versions=["3.29.3"], protocols=[4], python_driver_git=str(runner._python_driver_git), + driver_type="apache", scylla_install_dir="", tests="tests.integration.standard", + scylla_version="2026.1.15", collect_only=False, recipients=None, + ) + + with pytest.raises(SystemExit) as exit_info: + main.main(arguments) + + assert exit_info.value.code == 1 + execute_tests.assert_not_called() + metadata = json.loads((runner.xunit_dir / runner.metadata_file_name).read_text()) + assert "HTTP error 504" in metadata["failure_reason"] + assert "FileNotFoundError" not in metadata["failure_reason"] + assert not runner.xunit_file.exists() diff --git a/tests/test_run_test_script.py b/tests/test_run_test_script.py index 56aed7d..c5d4860 100644 --- a/tests/test_run_test_script.py +++ b/tests/test_run_test_script.py @@ -83,6 +83,7 @@ def test_run_test_constructs_one_docker_command_with_workspace_env(tmp_path): run_call = f" {calls[0]} " assert " -e PIP_CACHE_DIR " in run_call assert " -e UV_CACHE_DIR " in run_call + assert " -e CCM_DIR " in run_call assert " -e PYTEST_ADDOPTS " in run_call assert " -e WORKSPACE " in run_call assert f" -v {home}/.pip-cache:{home}/.pip-cache " in run_call From 1454d707b01a42e2bc91efce7967efe9540f1b6f Mon Sep 17 00:00:00 2001 From: Dmitry Kropachev Date: Fri, 16 Oct 2026 01:11:22 -0400 Subject: [PATCH 2/2] Fix: anchor relative CCM paths to the matrix invocation directory Run.run() changes the working directory to the driver checkout before bootstrap, so resolving a relative CCM_DIR there loses its original base. Save the invocation directory once and pass it to every matrix Run so later version and protocol combinations use the same checkout. Exercise both driver types through main with real directory changes and retain the archive fallback for unset or empty CCM_DIR. --- main.py | 5 ++++- run.py | 5 +++-- tests/test_run_setup.py | 48 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index 6bb305b..5b79b3c 100644 --- a/main.py +++ b/main.py @@ -15,6 +15,8 @@ def main(arguments: argparse.Namespace): status = 0 results = dict() + # Run.run() changes cwd, but relative CCM_DIR paths belong to this invocation. + invocation_dir = os.getcwd() for driver_version in arguments.versions: for protocol in arguments.protocols: @@ -26,7 +28,8 @@ def main(arguments: argparse.Namespace): protocol=protocol, tests=arguments.tests, scylla_version=arguments.scylla_version, - collect_only=arguments.collect_only) + collect_only=arguments.collect_only, + invocation_dir=invocation_dir) try: result = runner.run() logging.info("=== (%s:%s) PYTHON DRIVER MATRIX RESULTS FOR PROTOCOL v%s ===", diff --git a/run.py b/run.py index 882bb87..8a45bd9 100644 --- a/run.py +++ b/run.py @@ -17,7 +17,8 @@ class Run: def __init__(self, python_driver_git, python_driver_type, scylla_install_dir, tag, protocol, tests, scylla_version, - collect_only): + collect_only, invocation_dir=None): + self._invocation_dir = Path(invocation_dir) if invocation_dir is not None else Path.cwd() self.driver_version = tag.split("-", maxsplit=1)[0] self._full_driver_version = tag self._python_driver_git = Path(python_driver_git) @@ -149,7 +150,7 @@ def _apply_patch_files(self) -> bool: def _create_venv(self): ccm_source = "https://github.com/scylladb/scylla-ccm/archive/master.zip" if self.environment.get("CCM_DIR"): - ccm_dir = Path(self.environment["CCM_DIR"]).resolve() + ccm_dir = (self._invocation_dir / self.environment["CCM_DIR"]).resolve() if not ccm_dir.is_dir(): raise FileNotFoundError(f"CCM_DIR does not point to a directory: {ccm_dir}") ccm_source = str(ccm_dir) diff --git a/tests/test_run_setup.py b/tests/test_run_setup.py index 09b8fa8..579c505 100644 --- a/tests/test_run_setup.py +++ b/tests/test_run_setup.py @@ -1,6 +1,7 @@ import argparse import json import shlex +from pathlib import Path from unittest.mock import Mock import pytest @@ -35,7 +36,10 @@ def test_bootstrap_uses_configured_ccm_checkout(runner, tmp_path, monkeypatch, d assert ("uv" in install) == (driver_type == "scylla") -def test_bootstrap_keeps_archive_fallback_without_ccm_dir(runner): +@pytest.mark.parametrize("ccm_dir", [None, ""]) +def test_bootstrap_keeps_archive_fallback_without_ccm_dir(runner, monkeypatch, ccm_dir): + if ccm_dir is not None: + monkeypatch.setenv("CCM_DIR", ccm_dir) runner._run_command_in_shell = Mock() runner._create_venv() @@ -81,3 +85,45 @@ def test_install_failure_is_reported_with_original_cause(runner, monkeypatch): assert "HTTP error 504" in metadata["failure_reason"] assert "FileNotFoundError" not in metadata["failure_reason"] assert not runner.xunit_file.exists() + + +@pytest.mark.parametrize("driver_type", ["apache", "scylla"]) +def test_relative_ccm_checkout_survives_matrix_directory_changes(runner, tmp_path, monkeypatch, driver_type): + import main + + invocation_dir = tmp_path / "invocation" + ccm_dir = invocation_dir / "CCM checkout; echo unwanted" + ccm_dir.mkdir(parents=True) + monkeypatch.chdir(invocation_dir) + monkeypatch.setenv("CCM_DIR", ccm_dir.name) + monkeypatch.setattr(Run, "xunit_dir", tmp_path / "reports") + monkeypatch.setattr(Run, "_checkout_branch", Mock(return_value=True)) + monkeypatch.setattr(Run, "_apply_patch_files", Mock(return_value=True)) + monkeypatch.setattr("run.shutil.which", Mock(return_value="uv")) + monkeypatch.setattr("run.ProcessJUnit", Mock(return_value=Mock(summary={"tests": 1}, is_failed=False))) + install_commands = Mock() + monkeypatch.setattr(Run, "_run_command_in_shell", install_commands) + execute_tests = Mock() + monkeypatch.setattr("run.subprocess.call", execute_tests) + arguments = argparse.Namespace( + versions=["3.29.3", "3.29.2"], protocols=[3, 4], python_driver_git=str(runner._python_driver_git), + driver_type=driver_type, scylla_install_dir="", tests="tests.integration.standard", + scylla_version="2026.1.15", collect_only=False, recipients=None, + ) + + with pytest.raises(SystemExit) as exit_info: + main.main(arguments) + + assert exit_info.value.code == 0 + assert Path.cwd() == runner._python_driver_git + assert execute_tests.call_count == 4 + bootstrap_installs = [ + shlex.split(call.kwargs["cmd"]) + for call in install_commands.call_args_list + if "pytest==7.4.4" in call.kwargs.get("cmd", "") + ] + assert len(bootstrap_installs) == 4 + for install in bootstrap_installs: + assert install[install.index("install") + 1:] == [ + "pytest==7.4.4", str(ccm_dir), "pytest-subtests", + ]