tox-dev / tox
1 15
import json
2 15
import os
3 15
import sys
4 15
import time
5 15
from contextlib import contextmanager
6 15
from pathlib import Path
7 15
from subprocess import check_call
8 15
from typing import Iterator, List, Optional
9 15
from zipfile import ZipFile
10

11 15
import pytest
12 15
from filelock import FileLock
13 15
from packaging.requirements import Requirement
14

15 15
from tox.pytest import Index, IndexServer, MonkeyPatch, TempPathFactory, ToxProjectCreator
16

17
if sys.version_info >= (3, 8):  # pragma: no cover (py38+)
18
    from importlib.metadata import Distribution  # type: ignore[attr-defined]
19
else:  # pragma: no cover (<py38)
20
    from importlib_metadata import Distribution  # noqa
21

22 15
ROOT = Path(__file__).parents[1]
23

24

25 15
@contextmanager
26 15
def elapsed(msg: str) -> Iterator[None]:
27 15
    start = time.monotonic()
28 15
    try:
29 15
        yield
30
    finally:
31 15
        print(f"done in {time.monotonic() - start}s {msg}")
32

33

34 15
@pytest.fixture(scope="session")
35 15
def tox_wheel(tmp_path_factory: TempPathFactory, worker_id: str) -> Path:
36 15
    if worker_id == "master":  # if not running under xdist we can just return
37
        return _make_tox_wheel(tmp_path_factory)  # pragma: no cover
38
    # otherwise we need to ensure only one worker creates the wheel, and the rest reuses
39 15
    root_tmp_dir = tmp_path_factory.getbasetemp().parent
40 15
    cache_file = root_tmp_dir / "tox_wheel.json"
41 15
    with FileLock(f"{cache_file}.lock"):
42 15
        if cache_file.is_file():
43
            data = Path(json.loads(cache_file.read_text()))  # pragma: no cover
44
        else:
45 15
            data = _make_tox_wheel(tmp_path_factory)
46 15
            cache_file.write_text(json.dumps(str(data)))
47 15
    return data
48

49

50 15
def _make_tox_wheel(tmp_path_factory: TempPathFactory) -> Path:
51 15
    with elapsed("acquire current tox wheel"):  # takes around 3.2s on build
52 15
        package: Optional[Path] = None
53 15
        if "TOX_PACKAGE" in os.environ:
54
            env_tox_pkg = Path(os.environ["TOX_PACKAGE"])  # pragma: no cover
55
            if env_tox_pkg.exists() and env_tox_pkg.suffix == ".whl":  # pragma: no cover
56
                package = env_tox_pkg  # pragma: no cover
57 15
        if package is None:
58
            # when we don't get a wheel path injected, build it (for example when running from an IDE)
59
            into = tmp_path_factory.mktemp("dist")  # pragma: no cover
60
            package = build_wheel(into, Path(__file__).parents[1], isolation=False)  # pragma: no cover
61 15
        return package
62

63

64 15
@pytest.fixture(scope="session")
65 15
def tox_wheels(tox_wheel: Path, tmp_path_factory: TempPathFactory) -> List[Path]:
66 15
    with elapsed("acquire dependencies for current tox"):  # takes around 1.5s if already cached
67 15
        result: List[Path] = [tox_wheel]
68 15
        info = tmp_path_factory.mktemp("info")
69 15
        with ZipFile(str(tox_wheel), "r") as zip_file:
70 15
            zip_file.extractall(path=info)
71 15
        dist_info = next((i for i in info.iterdir() if i.suffix == ".dist-info"), None)
72
        if dist_info is None:  # pragma: no cover
73
            raise RuntimeError(f"no tox.dist-info inside {tox_wheel}")
74 15
        distribution = Distribution.at(dist_info)
75 15
        wheel_cache = ROOT / ".wheel_cache" / f"{sys.version_info.major}.{sys.version_info.minor}"
76 15
        wheel_cache.mkdir(parents=True, exist_ok=True)
77 15
        cmd = [sys.executable, "-I", "-m", "pip", "download", "-d", str(wheel_cache)]
78 15
        for req in distribution.requires:
79 15
            requirement = Requirement(req)
80 15
            if not requirement.extras:  # pragma: no branch  # we don't need to install any extras (tests/docs/etc)
81 15
                cmd.append(req)
82 15
        check_call(cmd)
83 15
        result.extend(wheel_cache.iterdir())
84 15
        return result
85

86

87 15
@pytest.fixture(scope="session")
88 15
def demo_pkg_inline_wheel(tmp_path_factory: TempPathFactory, demo_pkg_inline: Path) -> Path:
89 15
    return build_wheel(tmp_path_factory.mktemp("dist"), demo_pkg_inline)
90

91

92 15
def build_wheel(dist_dir: Path, of: Path, isolation: bool = True) -> Path:
93 15
    from build.__main__ import build_package  # noqa
94

95 15
    build_package(str(of), str(dist_dir), distributions=["wheel"], isolation=isolation)
96 15
    package = next(dist_dir.iterdir())
97 15
    return package
98

99

100 15
@pytest.fixture(scope="session")
101 15
def pypi_index_self(pypi_server: IndexServer, tox_wheels: List[Path], demo_pkg_inline_wheel: Path) -> Index:
102 15
    with elapsed("start devpi and create index"):  # takes around 1s
103 15
        self_index = pypi_server.create_index("self", "volatile=False")
104 15
    with elapsed("upload tox and its wheels to devpi"):  # takes around 3.2s on build
105 15
        self_index.upload(tox_wheels + [demo_pkg_inline_wheel])
106 15
    return self_index
107

108

109 15
def test_provision_requires_nok(tox_project: ToxProjectCreator) -> None:
110 15
    ini = "[tox]\nrequires = pkg-does-not-exist\n setuptools==1\nskipsdist=true\n"
111 15
    outcome = tox_project({"tox.ini": ini}).run("c", "-e", "py")
112 15
    outcome.assert_failed()
113 15
    outcome.assert_out_err(
114
        r".*will run in automatically provisioned tox, host .* is missing \[requires \(has\)\]:"
115
        r" pkg-does-not-exist \(N/A\), setuptools==1 \(.*\).*",
116
        r".*",
117
        regex=True,
118
    )
119

120

121 15
@pytest.mark.integration()
122 15
def test_provision_requires_ok(
123
    tox_project: ToxProjectCreator, pypi_index_self: Index, monkeypatch: MonkeyPatch, tmp_path: Path
124
) -> None:
125 15
    pypi_index_self.use(monkeypatch)
126 15
    proj = tox_project({"tox.ini": "[tox]\nrequires=demo-pkg-inline\n[testenv]\npackage=skip"})
127 15
    log = tmp_path / "out.log"
128

129
    # initial run
130 15
    result_first = proj.run("r", "--result-json", str(log))
131 15
    result_first.assert_success()
132 15
    prov_msg = (
133
        f"ROOT: will run in automatically provisioned tox, host {sys.executable} is missing"
134
        f" [requires (has)]: demo-pkg-inline (N/A)"
135
    )
136 15
    assert prov_msg in result_first.out
137

138 15
    with log.open("rt") as file_handler:
139 15
        log_report = json.load(file_handler)
140 15
    assert "py" in log_report["testenvs"]
141

142
    # recreate without recreating the provisioned env
143 15
    provision_env = result_first.env_conf(".tox")["env_dir"]
144 15
    result_recreate_no_pr = proj.run("r", "--recreate", "--no-recreate-provision")
145 15
    result_recreate_no_pr.assert_success()
146 15
    assert prov_msg in result_recreate_no_pr.out
147 15
    assert f"ROOT: remove tox env folder {provision_env}" not in result_recreate_no_pr.out, result_recreate_no_pr.out
148

149
    # recreate with recreating the provisioned env
150 15
    result_recreate = proj.run("r", "--recreate")
151 15
    result_recreate.assert_success()
152 15
    assert prov_msg in result_recreate.out
153 15
    assert f"ROOT: remove tox env folder {provision_env}" in result_recreate.out, result_recreate.out
154

155

156 15
@pytest.mark.integration()
157 15
def test_provision_platform_check(
158
    tox_project: ToxProjectCreator, pypi_index_self: Index, monkeypatch: MonkeyPatch
159
) -> None:
160 15
    pypi_index_self.use(monkeypatch)
161 15
    ini = "[tox]\nrequires=demo-pkg-inline\n[testenv]\npackage=skip\n[testenv:.tox]\nplatform=wrong_platform"
162 15
    proj = tox_project({"tox.ini": ini})
163

164 15
    result = proj.run("r")
165 15
    result.assert_failed(-2)
166 15
    msg = f"cannot provision tox environment .tox because platform {sys.platform} does not match wrong_platform"
167 15
    assert msg in result.out

Read our documentation on viewing source code .

Loading