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 c2a4443..8a45bd9 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 @@ -16,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) @@ -146,8 +148,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 = (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) 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 +171,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 +223,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..579c505 --- /dev/null +++ b/tests/test_run_setup.py @@ -0,0 +1,129 @@ +import argparse +import json +import shlex +from pathlib import Path +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") + + +@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() + + 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() + + +@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", + ] 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