"""Tests for install_python_stack._build_uv_cmd torch-backend handling.""" from __future__ import annotations import glob import importlib import os import sys import tempfile from pathlib import Path from unittest import mock import pytest STUDIO_DIR = Path(__file__).resolve().parents[2] / "studio" sys.path.insert(0, str(STUDIO_DIR)) import install_python_stack as ips class TestBuildUvCmdTorchBackend: """Verify _build_uv_cmd only adds --torch-backend when UV_TORCH_BACKEND is set.""" def _call(self, args: tuple[str, ...] = ()) -> list[str]: return ips._build_uv_cmd(args) def test_default_no_torch_backend(self): """Without UV_TORCH_BACKEND env var, no --torch-backend flag.""" env = os.environ.copy() env.pop("UV_TORCH_BACKEND", None) with mock.patch.dict(os.environ, env, clear = True): cmd = self._call(("somepackage",)) assert not any( a.startswith("--torch-backend") for a in cmd ), f"--torch-backend should not appear by default, got: {cmd}" def test_uv_torch_backend_auto(self): """UV_TORCH_BACKEND=auto adds --torch-backend=auto.""" with mock.patch.dict(os.environ, {"UV_TORCH_BACKEND": "auto"}): cmd = self._call(("somepackage",)) assert "--torch-backend=auto" in cmd def test_uv_torch_backend_cpu(self): """UV_TORCH_BACKEND=cpu adds --torch-backend=cpu.""" with mock.patch.dict(os.environ, {"UV_TORCH_BACKEND": "cpu"}): cmd = self._call(("somepackage",)) assert "--torch-backend=cpu" in cmd def test_uv_torch_backend_empty(self): """UV_TORCH_BACKEND="" (empty string) should NOT add --torch-backend.""" with mock.patch.dict(os.environ, {"UV_TORCH_BACKEND": ""}): cmd = self._call(("somepackage",)) assert not any( a.startswith("--torch-backend") for a in cmd ), f"Empty UV_TORCH_BACKEND should not add flag, got: {cmd}" def test_uv_torch_backend_skipped_for_pinned_index(self): """A pinned-index command must NOT get --torch-backend: uv's torch backend redirects torch resolution to its own per-backend index even when --index-url is given (verified: cu128 pin + backend cpu installs torch+cpu), defeating the pin.""" for pin_flag in ("--index-url", "--default-index"): with mock.patch.dict(os.environ, {"UV_TORCH_BACKEND": "cpu"}): cmd = self._call(("torch", pin_flag, "https://download.pytorch.org/whl/cu128")) assert not any( a.startswith("--torch-backend") for a in cmd ), f"{pin_flag} command must not carry --torch-backend, got: {cmd}" def test_uv_torch_backend_kept_for_unpinned(self): """Non-pinned commands still honour UV_TORCH_BACKEND.""" with mock.patch.dict(os.environ, {"UV_TORCH_BACKEND": "cpu"}): cmd = self._call(("somepackage",)) assert "--torch-backend=cpu" in cmd class TestUvSafePath: """_uv_safe_path hands uv a space-free `-c`/`-r` path (issue #6503).""" def test_passthrough_when_no_space(self): """A path without a space is returned unchanged on every platform.""" p = "/tmp/plain/constraints.txt" assert ips._uv_safe_path(p) == p @pytest.mark.skipif(ips.IS_WINDOWS, reason = "POSIX temp-copy fallback") def test_posix_space_path_returns_spacefree_copy(self, tmp_path): src = tmp_path / "Open Source" / "constraints.txt" src.parent.mkdir(parents = True) src.write_text("torch>=2.6\n") out = ips._uv_safe_path(str(src)) assert " " not in out, f"uv-safe path still has a space: {out!r}" assert out != str(src) assert Path(out).read_text() == "torch>=2.6\n" @pytest.mark.skipif(ips.IS_WINDOWS, reason = "POSIX temp-copy fallback") def test_posix_missing_file_falls_back_to_original(self): """No file to copy -> return the original path rather than raise.""" p = "/nonexistent dir/constraints.txt" assert ips._uv_safe_path(p) == p class TestUvSafePathHardening: """Edge cases for uv_safe_path + the UV_OVERRIDE channel (issue #6503).""" @pytest.mark.skipif(ips.IS_WINDOWS, reason = "POSIX temp-copy fallback") def test_tmpdir_with_space_falls_back(self, tmp_path, monkeypatch): """A space in the temp root itself -> fall back to the original path.""" from backend.utils import uv_path_safety as uvps spaced = tmp_path / "tmp dir with space" spaced.mkdir() monkeypatch.setattr(uvps.tempfile, "mkdtemp", lambda *a, **k: str(spaced)) src = tmp_path / "Open Source" / "constraints.txt" src.parent.mkdir(parents = True) src.write_text("idna\n") assert uvps.uv_safe_path(str(src)) == str(src) @pytest.mark.skipif(ips.IS_WINDOWS, reason = "POSIX temp-copy fallback") def test_no_temp_dir_leak_on_copy_failure(self, tmp_path, monkeypatch): """A copyfile failure after mkdtemp must not orphan the temp dir.""" from backend.utils import uv_path_safety as uvps src = tmp_path / "Open Source" / "constraints.txt" src.parent.mkdir(parents = True) src.write_text("idna\n") pattern = os.path.join(tempfile.gettempdir(), "unsloth_uv_*") before = set(glob.glob(pattern)) def boom(*a, **k): raise OSError("boom") monkeypatch.setattr(uvps.shutil, "copyfile", boom) out = uvps.uv_safe_path(str(src)) assert out == str(src) assert set(glob.glob(pattern)) == before @pytest.mark.skipif(ips.IS_WINDOWS, reason = "POSIX temp-copy fallback") def test_cleanup_removes_and_clears_registry(self, tmp_path): """The atexit-registered cleanup removes the copies and empties the list.""" from backend.utils import uv_path_safety as uvps src = tmp_path / "Open Source" / "constraints.txt" src.parent.mkdir(parents = True) src.write_text("idna\n") out = uvps.uv_safe_path(str(src)) tmp_dir = Path(out).parent assert tmp_dir.is_dir() and str(tmp_dir) in uvps._UV_SAFE_PATH_TMPDIRS uvps._cleanup_uv_safe_path_tmpdirs() assert not tmp_dir.exists() assert uvps._UV_SAFE_PATH_TMPDIRS == [] @pytest.mark.skipif(ips.IS_WINDOWS, reason = "POSIX temp-copy fallback") def test_uv_override_value_is_space_safe(self, tmp_path): """The value stored for UV_OVERRIDE must be space-free.""" from backend.utils import uv_path_safety as uvps overrides = tmp_path / "Open Source" / "overrides-darwin-arm64.txt" overrides.parent.mkdir(parents = True) overrides.write_text("transformers>=4.57.6\n") value = uvps.uv_safe_path(overrides) assert " " not in value assert Path(value).read_text() == "transformers>=4.57.6\n" class TestPinnedIndexClearsUvEnv: """A pinned torch install (--index-url / --default-index) must neutralise an inherited UV_INDEX / UV_EXTRA_INDEX_URL so the pinned wheel index wins. uv treats the default index (--index-url / --default-index) as LOWEST priority, so an inherited UV_INDEX / UV_EXTRA_INDEX_URL (a corporate/CPU mirror) would be searched first and, under uv's default first-index strategy, resolve torch from the wrong mirror -- after which the marker records a wheel index that was never used. install.sh (#6898), install.ps1 and setup.ps1 already clear these for pinned installs; install_python_stack must match (parity across all installers). """ UV_VARS = ("UV_DEFAULT_INDEX", "UV_INDEX_URL", "UV_INDEX", "UV_EXTRA_INDEX_URL") def test_pinned_index_url_strips_uv_index_vars(self): cmd = [ "uv", "pip", "install", "--force-reinstall", "torch", "torchvision", "torchaudio", "--index-url", "https://download.pytorch.org/whl/cu128", ] with mock.patch.dict( os.environ, { "UV_INDEX": "https://mirror.corp/simple", "UV_EXTRA_INDEX_URL": "https://mirror.corp/extra", "UV_INDEX_URL": "https://mirror.corp/root", "UV_DEFAULT_INDEX": "https://mirror.corp/default", }, ): env = ips._install_env_for_cmd(cmd) assert env is not None, "a --index-url install must run with a scrubbed env" for var in self.UV_VARS: assert var not in env, f"{var} must be cleared for a pinned-index install" def test_pinned_default_index_strips_uv_index_vars(self): # --default-index must be gated too (matches install.sh / install.ps1). cmd = ["uv", "pip", "install", "torch", "--default-index", "https://x/cu126"] with mock.patch.dict(os.environ, {"UV_INDEX": "https://mirror.corp/simple"}): env = ips._install_env_for_cmd(cmd) assert env is not None assert "UV_INDEX" not in env def test_non_pinned_install_keeps_user_mirror(self): # A plain install (no --index-url) must NOT scrub the env, so a user's mirror # still applies to base packages. cmd = ["uv", "pip", "install", "unsloth", "unsloth-zoo"] with mock.patch.dict(os.environ, {"UV_INDEX": "https://mirror.corp/simple"}): env = ips._install_env_for_cmd(cmd) assert env is None, "non-pinned installs must inherit the caller env unchanged" def test_scrubbed_env_preserves_other_vars(self): cmd = ["uv", "pip", "install", "torch", "--index-url", "https://x/cu128"] with mock.patch.dict( os.environ, {"UV_INDEX": "https://mirror.corp/simple", "PATH_SENTINEL_XYZ": "keepme"}, ): env = ips._install_env_for_cmd(cmd) assert env is not None assert env.get("PATH_SENTINEL_XYZ") == "keepme", "only uv index vars are removed" def test_pinned_cmd_strips_pip_extra_index_url(self): """PIP_EXTRA_INDEX_URL is stripped for pinned commands so the pip fallback cannot satisfy torch from an inherited extra index.""" with mock.patch.dict(os.environ, {"PIP_EXTRA_INDEX_URL": "https://mirror/simple"}): env = ips._install_env_for_cmd( ["pip", "install", "torch", "--index-url", "https://x/cu128"] ) assert env is not None and "PIP_EXTRA_INDEX_URL" not in env def test_pinned_cmd_strips_uv_torch_backend(self): """UV_TORCH_BACKEND is stripped for pinned commands so uv cannot read it from the environment and reroute torch off the pinned index.""" with mock.patch.dict(os.environ, {"UV_TORCH_BACKEND": "cpu"}): env = ips._install_env_for_cmd( ["uv", "pip", "install", "torch", "--index-url", "https://x/cu128"] ) assert env is not None and "UV_TORCH_BACKEND" not in env def test_pinned_cmd_disables_uv_config_discovery(self): """A DISCOVERED uv.toml / pyproject [tool.uv] outranks the CLI pin too (verified with uv 0.10: [pip] torch-backend = "cpu" and a non-default [[index]] both resolve torch+cpu against an explicit --index-url / --default-index cu126 pin). Pinned commands must run with UV_NO_CONFIG=1 and without an inherited UV_CONFIG_FILE.""" with mock.patch.dict(os.environ, {"UV_CONFIG_FILE": "/etc/uv/uv.toml"}): env = ips._install_env_for_cmd( ["uv", "pip", "install", "torch", "--index-url", "https://x/cu128"] ) assert env is not None assert env.get("UV_NO_CONFIG") == "1" assert "UV_CONFIG_FILE" not in env def test_pinned_cmd_disables_pip_config_files(self): """The pip FALLBACK honours user/site pip config files (pip config set global.extra-index-url) even with the PIP_* env vars stripped; pip loads NO configuration files when PIP_CONFIG_FILE is os.devnull. Harmless for uv, decisive for the fallback.""" env = ips._install_env_for_cmd( ["uv", "pip", "install", "torch", "--index-url", "https://x/cu128"] ) assert env is not None assert env.get("PIP_CONFIG_FILE") == os.devnull def test_non_pinned_cmd_keeps_uv_config_discovery(self): """Non-pinned installs inherit the caller env unchanged, so a user's uv configuration still applies to base packages.""" env = ips._install_env_for_cmd(["uv", "pip", "install", "unsloth"]) assert env is None