from __future__ import annotations import json import os import sys import time from contextlib import contextmanager from pathlib import Path from subprocess import check_call from typing import TYPE_CHECKING from unittest import mock from zipfile import ZipFile import pytest from filelock import FileLock from packaging.requirements import Requirement if TYPE_CHECKING: from collections.abc import Callable, Iterator, Sequence from build import DistributionType from tox.pytest import MonkeyPatch, TempPathFactory, ToxProjectCreator from importlib.metadata import Distribution ROOT = Path(__file__).parents[1] @contextmanager def elapsed(msg: str) -> Iterator[None]: start = time.monotonic() try: yield finally: print(f"done in {time.monotonic() - start}s {msg}") # noqa: T201 @pytest.fixture(scope="session") def tox_wheel( tmp_path_factory: TempPathFactory, worker_id: str, pkg_builder: Callable[[Path, Path, list[str], bool], Path], ) -> Path: if worker_id == "master": # if not running under xdist we can just return return _make_tox_wheel(tmp_path_factory, pkg_builder) # pragma: no cover # otherwise we need to ensure only one worker creates the wheel, and the rest reuses root_tmp_dir = tmp_path_factory.getbasetemp().parent cache_file = root_tmp_dir / "tox_wheel.json" with FileLock(f"{cache_file}.lock"): if cache_file.is_file(): data = Path(json.loads(cache_file.read_text())) # pragma: no cover else: data = _make_tox_wheel(tmp_path_factory, pkg_builder) cache_file.write_text(json.dumps(str(data))) return data def _make_tox_wheel( tmp_path_factory: TempPathFactory, pkg_builder: Callable[[Path, Path, list[str], bool], Path], ) -> Path: with elapsed("acquire current tox wheel"): # takes around 3.2s on build into = tmp_path_factory.mktemp("dist") # pragma: no cover from tox.version import version_tuple # noqa: PLC0415 patch_version = version_tuple[2] if isinstance(patch_version, str) and patch_version[:3] == "dev": # Version is in the form of 1.23.dev456, we need to increment the 456 part version = f"{version_tuple[0]}.{version_tuple[1]}.dev{int(patch_version[3:]) + 1}" else: version = f"{version_tuple[0]}.{version_tuple[1]}.{int(patch_version) + 1}" with mock.patch.dict(os.environ, {"SETUPTOOLS_SCM_PRETEND_VERSION": version}): return pkg_builder(into, Path(__file__).parents[1], ["wheel"], False) # pragma: no cover @pytest.fixture(scope="session") def tox_wheels(tox_wheel: Path, tmp_path_factory: TempPathFactory) -> list[Path]: with elapsed("acquire dependencies for current tox"): # takes around 1.5s if already cached result: list[Path] = [tox_wheel] info = tmp_path_factory.mktemp("info") with ZipFile(str(tox_wheel), "r") as zip_file: zip_file.extractall(path=info) dist_info = next((i for i in info.iterdir() if i.suffix == ".dist-info"), None) if dist_info is None: # pragma: no cover msg = f"no tox.dist-info inside {tox_wheel}" raise RuntimeError(msg) distribution = Distribution.at(dist_info) wheel_cache = ROOT / ".wheel_cache" / f"{sys.version_info.major}.{sys.version_info.minor}" wheel_cache.mkdir(parents=True, exist_ok=True) cmd = [sys.executable, "-m", "pip", "download", "-d", str(wheel_cache)] assert distribution.requires is not None for req in distribution.requires: requirement = Requirement(req) if not requirement.extras: # pragma: no branch # we don't need to install any extras (tests/docs/etc) cmd.append(req) check_call(cmd) result.extend(wheel_cache.iterdir()) res = "\n".join(str(i) for i in result) with elapsed(f"acquired dependencies for current tox: {res}"): pass return result def test_provision_requires_nok(tox_project: ToxProjectCreator) -> None: ini = "[tox]\nrequires = pkg-does-not-exist\n setuptools==1\nskipsdist=true\n" outcome = tox_project({"tox.ini": ini}).run("c", "-e", "py") outcome.assert_failed() outcome.assert_out_err( r".*will run in automatically provisioned tox, host .* is missing \[requires \(has\)\]:" r" pkg-does-not-exist, setuptools==1 \(.*\).*", r".*", regex=True, ) @pytest.mark.integration def test_provision_plugin_runner_in_provision(tox_project: ToxProjectCreator, tmp_path: Path) -> None: """Ensure that provision environment can be explicitly configured.""" log = tmp_path / "out.log" proj = tox_project({"tox.ini": "[tox]\nrequires=somepkg123xyz\n[testenv:.tox]\nrunner=example"}) with pytest.raises(KeyError, match="example"): proj.run("r", "-e", "py", "--result-json", str(log)) @pytest.mark.parametrize("subcommand", ["r", "p", "de", "l", "d", "c", "q", "e", "le"]) def test_provision_default_arguments_exists(tox_project: ToxProjectCreator, subcommand: str) -> None: ini = r""" [tox] requires = tox<4.14 [testenv] package = skip """ project = tox_project({"tox.ini": ini}) project.patch_execute(lambda r: 0 if "install" in r.run_id else None) outcome = project.run(subcommand) for argument in ["result_json", "hash_seed", "discover", "list_dependencies"]: assert hasattr(outcome.state.conf.options, argument)