import json import os import subprocess from collections.abc import Generator import pytest from filelock import FileLock from cibuildwheel.architecture import Architecture from cibuildwheel.ci import detect_ci_provider from cibuildwheel.options import CommandLineArguments, Options from cibuildwheel.selector import EnableGroup from cibuildwheel.typing import PLATFORMS from cibuildwheel.venv import find_uv from . import utils from .utils import DEFAULT_CIBW_ENABLE, EMULATED_ARCHS, get_platform def pytest_addoption(parser: pytest.Parser) -> None: parser.addoption( "--run-emulation", action="store", default=None, help="run emulation tests", choices=("all", *EMULATED_ARCHS), ) parser.addoption("--run-podman", action="store_true", default=False, help="run podman tests") parser.addoption( "--run-cp38-universal2", action="store_true", default=False, help="macOS cp38 uses the universal2 installer", ) parser.addoption( "--enable", action="store", default=None, help="Set the CIBW_ENABLE environment variable for all tests.", ) parser.addoption( "--platform", action="store", default=None, help="Set the CIBW_PLATFORM environment variable for all tests.", ) def pytest_configure(config: pytest.Config) -> None: flag_enable = config.getoption("--enable") flag_platform = config.getoption("--platform") if flag_enable is not None and "CIBW_ENABLE" in os.environ: msg = ( "Both --enable pytest option and CIBW_ENABLE environment variable are set. " "Please specify only one." ) raise pytest.UsageError(msg) if flag_platform is not None and "CIBW_PLATFORM" in os.environ: msg = ( "Both --platform pytest option and CIBW_PLATFORM environment variable are set. " "Please specify only one." ) raise pytest.UsageError(msg) if flag_enable is not None: EnableGroup.parse_option_value(flag_enable) os.environ["CIBW_ENABLE"] = flag_enable if flag_enable is None and "CIBW_ENABLE" not in os.environ: # Set default value for CIBW_ENABLE os.environ["CIBW_ENABLE"] = DEFAULT_CIBW_ENABLE if flag_platform is not None: assert flag_platform in PLATFORMS, f"Invalid platform: {flag_platform}" os.environ["CIBW_PLATFORM"] = flag_platform def docker_warmup(request: pytest.FixtureRequest) -> None: machine = request.config.getoption("--run-emulation", default=None) if machine is None: archs = {arch.value for arch in Architecture.auto_archs("linux")} elif machine == "all": archs = set(EMULATED_ARCHS) else: archs = {machine} # Only include architectures where there are missing pre-installed interpreters archs &= {"x86_64", "i686", "aarch64"} if not archs: return options = Options( platform="linux", command_line_arguments=CommandLineArguments.defaults(), env={}, defaults=True, ) build_options = options.build_options(None) assert build_options.manylinux_images is not None assert build_options.musllinux_images is not None images = [build_options.manylinux_images[arch] for arch in archs] + [ build_options.musllinux_images[arch] for arch in archs ] command = ( "manylinux-interpreters ensure-all &&" "cpython3.13 -m pip download -d /tmp setuptools wheel pytest" ) for image in images: container_id = subprocess.run( ["docker", "create", image, "bash", "-c", command], text=True, check=True, stdout=subprocess.PIPE, ).stdout.strip() try: subprocess.run(["docker", "start", container_id], check=True, stdout=subprocess.DEVNULL) exit_code = subprocess.run( ["docker", "wait", container_id], text=True, check=True, stdout=subprocess.PIPE ).stdout.strip() assert exit_code == "0" subprocess.run( ["docker", "commit", container_id, image], check=True, stdout=subprocess.DEVNULL ) finally: subprocess.run(["docker", "rm", container_id], check=True, stdout=subprocess.DEVNULL) @pytest.fixture(scope="session", autouse=True) def docker_warmup_fixture( request: pytest.FixtureRequest, tmp_path_factory: pytest.TempPathFactory, worker_id: str ) -> None: # if we're in CI testing linux, let's warm-up docker images if detect_ci_provider() is None or get_platform() != "linux": return None if request.config.getoption("--run-emulation", default=None) is not None: # emulation tests only run one test in CI, caching the image only slows down the test return None if worker_id == "master": # not executing with multiple workers # it might be unsafe to write to tmp_path_factory.getbasetemp().parent return docker_warmup(request) # get the temp directory shared by all workers root_tmp_dir = tmp_path_factory.getbasetemp().parent fn = root_tmp_dir / "warmup.done" with FileLock(str(fn) + ".lock"): if not fn.is_file(): docker_warmup(request) fn.write_text("done") return None @pytest.fixture(params=["pip", "build"]) def build_frontend_env_nouv(request: pytest.FixtureRequest) -> dict[str, str]: frontend = request.param marks = {m.name for m in request.node.iter_markers()} platform = "pyodide" if "pyodide" in marks else get_platform() if platform == "pyodide" and frontend == "pip": pytest.skip("Can't use pip as build frontend for pyodide platform") return {"CIBW_BUILD_FRONTEND": frontend} @pytest.fixture(params=["pip", "build", "build[uv]", "uv"]) def build_frontend_env(request: pytest.FixtureRequest) -> Generator[dict[str, str], None, None]: frontend = request.param marks = {m.name for m in request.node.iter_markers()} if "android" in marks: platform = "android" elif "ios" in marks: platform = "ios" elif "pyodide" in marks: platform = "pyodide" else: platform = get_platform() if platform in {"pyodide", "ios", "android"} and frontend == "pip": pytest.skip(f"Can't use pip as build frontend for {platform}") if platform == "pyodide" and frontend in {"build[uv]", "uv"}: pytest.skip("Can't use uv with pyodide yet") uv_path = find_uv() if uv_path is None and frontend in {"build[uv]", "uv"}: pytest.skip("Can't find uv, so skipping uv tests") if uv_path is not None and frontend == "build" and platform not in {"android", "ios"}: pytest.skip("No need to check build when uv is present") # temporary workaround: uv doesn't work with graalpy yet uses_uv = "uv" in frontend env: dict[str, str] = {"CIBW_BUILD_FRONTEND": frontend} if uses_uv: utils.include_graalpy_in_expected_wheels = False env["CIBW_SKIP"] = "gp*" # skip graalpy when using uv, until uv supports it try: yield env finally: if uses_uv: utils.include_graalpy_in_expected_wheels = True @pytest.fixture def docker_cleanup() -> Generator[None, None, None]: def get_images() -> set[str]: if detect_ci_provider() is None or get_platform() != "linux": return set() images = subprocess.run( ["docker", "image", "ls", "--format", "{{json .ID}}"], text=True, check=True, stdout=subprocess.PIPE, ).stdout return {json.loads(image.strip()) for image in images.splitlines() if image.strip()} images_before = get_images() try: yield finally: images_after = get_images() for image in images_after - images_before: subprocess.run(["docker", "rmi", image], check=False)