e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
570 lines
22 KiB
Python
570 lines
22 KiB
Python
"""Install fixes: tokenizers in no-torch-runtime.txt, and TORCH_CONSTRAINT in install.sh."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pathlib
|
|
import re
|
|
import subprocess
|
|
import textwrap
|
|
|
|
import pytest
|
|
|
|
# Locate source files relative to this test.
|
|
_TESTS_DIR = pathlib.Path(__file__).resolve().parent.parent # tests/
|
|
_REPO_ROOT = _TESTS_DIR.parent # unsloth/
|
|
_INSTALL_SH = _REPO_ROOT / "install.sh"
|
|
_INSTALL_PS1 = _REPO_ROOT / "install.ps1"
|
|
_SETUP_PS1 = _REPO_ROOT / "studio" / "setup.ps1"
|
|
_NO_TORCH_RT = _REPO_ROOT / "studio" / "backend" / "requirements" / "no-torch-runtime.txt"
|
|
|
|
|
|
def _read(path: pathlib.Path) -> str:
|
|
return path.read_text(encoding = "utf-8")
|
|
|
|
|
|
def _lines(path: pathlib.Path) -> list[str]:
|
|
"""Return non-comment, non-blank lines stripped."""
|
|
return [
|
|
ln.strip()
|
|
for ln in _read(path).splitlines()
|
|
if ln.strip() and not ln.strip().startswith("#")
|
|
]
|
|
|
|
|
|
# Group 1 -- Structural checks (no network, instant)
|
|
class TestStructuralTokenizers:
|
|
"""Verify tokenizers presence and ordering in no-torch-runtime.txt."""
|
|
|
|
def test_tokenizers_present(self):
|
|
"""tokenizers must be a standalone package line."""
|
|
pkgs = _lines(_NO_TORCH_RT)
|
|
bare_names = [p.split(">")[0].split("<")[0].split("!")[0].split("=")[0] for p in pkgs]
|
|
assert "tokenizers" in bare_names
|
|
|
|
def test_tokenizers_before_transformers(self):
|
|
"""tokenizers should appear before transformers (install order intent)."""
|
|
pkgs = _lines(_NO_TORCH_RT)
|
|
bare_names = [p.split(">")[0].split("<")[0].split("!")[0].split("=")[0] for p in pkgs]
|
|
idx_tok = bare_names.index("tokenizers")
|
|
idx_tf = bare_names.index("transformers")
|
|
assert idx_tok < idx_tf, (
|
|
f"tokenizers at index {idx_tok} should appear before " f"transformers at index {idx_tf}"
|
|
)
|
|
|
|
def test_torch_not_in_no_torch_file(self):
|
|
"""torch itself must NOT be listed in the no-torch requirements."""
|
|
pkgs = _lines(_NO_TORCH_RT)
|
|
bare_names = [p.split(">")[0].split("<")[0].split("!")[0].split("=")[0] for p in pkgs]
|
|
assert "torch" not in bare_names
|
|
|
|
|
|
class TestStructuralTorchConstraint:
|
|
"""Verify TORCH_CONSTRAINT wiring in install.sh."""
|
|
|
|
_sh = _read(_INSTALL_SH)
|
|
|
|
def test_default_assignment_exists(self):
|
|
assert 'TORCH_CONSTRAINT="torch>=2.4,<2.11.0"' in self._sh
|
|
|
|
def test_tightened_assignment_exists(self):
|
|
assert 'TORCH_CONSTRAINT="torch>=2.6,<2.11.0"' in self._sh
|
|
|
|
def test_variable_used_in_pip_install(self):
|
|
"""$TORCH_CONSTRAINT must appear in a uv pip install line."""
|
|
assert '"$TORCH_CONSTRAINT"' in self._sh
|
|
|
|
def test_hardcoded_torch_constraint_only_once(self):
|
|
"""The hard-coded torch>=2.4,<2.11.0 string should appear exactly once
|
|
in install.sh (the default assignment), not in pip install lines."""
|
|
count = self._sh.count('"torch>=2.4,<2.11.0"')
|
|
assert count == 1, f"Expected 1, found {count}"
|
|
|
|
def test_tightening_guarded_by_skip_torch(self):
|
|
"""The block must check SKIP_TORCH=false."""
|
|
# Find the tightening if-block
|
|
m = re.search(
|
|
r"if\s.*SKIP_TORCH.*=\s*false.*&&.*OS.*=.*macos.*&&.*_ARCH.*=.*arm64",
|
|
self._sh,
|
|
)
|
|
assert m is not None, "Guard not found: SKIP_TORCH + macos + arm64"
|
|
|
|
def test_tightening_guarded_by_arch(self):
|
|
m = re.search(r"_ARCH.*=.*arm64", self._sh)
|
|
assert m is not None
|
|
|
|
def test_tightening_guarded_by_os(self):
|
|
m = re.search(r"OS.*=.*macos", self._sh)
|
|
assert m is not None
|
|
|
|
|
|
class TestStructuralInstallPs1Unchanged:
|
|
"""install.ps1 should NOT have TORCH_CONSTRAINT variable."""
|
|
|
|
_ps1 = _read(_INSTALL_PS1)
|
|
|
|
def test_no_torch_constraint_variable(self):
|
|
assert "TORCH_CONSTRAINT" not in self._ps1
|
|
assert "$TorchConstraint" not in self._ps1
|
|
|
|
def test_hardcoded_torch_constraint_present(self):
|
|
assert '"torch>=2.4,<2.11.0"' in self._ps1
|
|
|
|
|
|
class TestInstallPs1UvDefaultIndex:
|
|
"""Installer-managed torch indexes must override inherited uv defaults."""
|
|
|
|
_ps1 = _read(_INSTALL_PS1)
|
|
|
|
def test_torch_installs_use_default_index(self):
|
|
assert "--default-index $TorchIndexUrl" in self._ps1
|
|
assert "--default-index $ROCmIndexUrl" in self._ps1
|
|
|
|
def test_torch_installs_do_not_use_deprecated_index_url(self):
|
|
assert "--index-url $TorchIndexUrl" not in self._ps1
|
|
assert "--index-url $ROCmIndexUrl" not in self._ps1
|
|
|
|
def test_torch_installs_neutralize_all_uv_index_env_vars(self):
|
|
# Extra-index vars outrank --default-index, so pinned installs must clear them.
|
|
for var in ("UV_DEFAULT_INDEX", "UV_INDEX_URL", "UV_INDEX", "UV_EXTRA_INDEX_URL"):
|
|
assert var in self._ps1
|
|
assert 'Remove-Item "Env:$n"' in self._ps1
|
|
|
|
|
|
class TestSetupPs1FastInstallIndex:
|
|
"""setup.ps1 Fast-Install must neutralize inherited uv indexes when pinning."""
|
|
|
|
_ps1 = _read(_SETUP_PS1)
|
|
|
|
def test_fast_install_clears_all_uv_index_env_vars(self):
|
|
for var in ("UV_DEFAULT_INDEX", "UV_INDEX_URL", "UV_INDEX", "UV_EXTRA_INDEX_URL"):
|
|
assert var in self._ps1
|
|
# Must truly remove the vars (child sees no value), not set them empty.
|
|
assert 'Remove-Item "Env:$n"' in self._ps1
|
|
|
|
|
|
class TestInstallShUvDefaultIndex:
|
|
"""Linux/Mac installer torch indexes must override inherited uv defaults."""
|
|
|
|
_sh = _read(_INSTALL_SH)
|
|
|
|
def test_torch_installs_use_default_index(self):
|
|
assert '--default-index "$TORCH_INDEX_URL"' in self._sh
|
|
|
|
def test_torch_installs_do_not_use_deprecated_index_url(self):
|
|
assert '--index-url "$TORCH_INDEX_URL"' not in self._sh
|
|
|
|
def test_torch_installs_neutralize_all_uv_index_env_vars(self):
|
|
# --default-index installs run with all uv index env vars unset via `env -u`.
|
|
assert (
|
|
"env -u UV_DEFAULT_INDEX -u UV_INDEX_URL -u UV_INDEX -u UV_EXTRA_INDEX_URL" in self._sh
|
|
)
|
|
|
|
|
|
# Group 2 -- Shell snippet tests (bash subprocess, mocked python)
|
|
class TestTorchConstraintShell:
|
|
"""Test the TORCH_CONSTRAINT block via bash with mocked python minor versions."""
|
|
|
|
# Snippet tested in isolation: override OS/_ARCH/SKIP_TORCH and a mock python.
|
|
_SNIPPET_TEMPLATE = textwrap.dedent(r"""
|
|
#!/bin/bash
|
|
set -e
|
|
SKIP_TORCH={skip_torch}
|
|
OS="{os}"
|
|
_ARCH="{arch}"
|
|
VENV_DIR="{venv_dir}"
|
|
|
|
TORCH_CONSTRAINT="torch>=2.4,<2.11.0"
|
|
if [ "$SKIP_TORCH" = false ] && [ "$OS" = "macos" ] && [ "$_ARCH" = "arm64" ]; then
|
|
_PY_MINOR=$("$VENV_DIR/bin/python" -c \
|
|
"import sys; print(sys.version_info.minor)" 2>/dev/null || echo "0")
|
|
if [ "$_PY_MINOR" -ge 13 ] 2>/dev/null; then
|
|
TORCH_CONSTRAINT="torch>=2.6,<2.11.0"
|
|
fi
|
|
fi
|
|
echo "$TORCH_CONSTRAINT"
|
|
""").strip()
|
|
|
|
@staticmethod
|
|
def _make_mock_python(tmp_path: pathlib.Path, minor: int) -> pathlib.Path:
|
|
"""Create a mock python that prints a controlled minor version."""
|
|
venv = tmp_path / "venv"
|
|
bin_dir = venv / "bin"
|
|
bin_dir.mkdir(parents = True, exist_ok = True)
|
|
mock_py = bin_dir / "python"
|
|
mock_py.write_text(
|
|
textwrap.dedent(f"""\
|
|
#!/bin/bash
|
|
# Mock python: always report minor={minor}
|
|
if echo "$@" | grep -q "sys.version_info.minor"; then
|
|
echo "{minor}"
|
|
else
|
|
echo "0"
|
|
fi
|
|
""")
|
|
)
|
|
mock_py.chmod(0o755)
|
|
return venv
|
|
|
|
def _run(
|
|
self,
|
|
tmp_path: pathlib.Path,
|
|
*,
|
|
py_minor: int = 12,
|
|
os_val: str = "macos",
|
|
arch: str = "arm64",
|
|
skip_torch: str = "false",
|
|
) -> str:
|
|
venv = self._make_mock_python(tmp_path, py_minor)
|
|
script = self._SNIPPET_TEMPLATE.format(
|
|
skip_torch = skip_torch,
|
|
os = os_val,
|
|
arch = arch,
|
|
venv_dir = str(venv),
|
|
)
|
|
script_file = tmp_path / "test_snippet.sh"
|
|
script_file.write_text(script)
|
|
script_file.chmod(0o755)
|
|
result = subprocess.run(
|
|
["bash", str(script_file)],
|
|
capture_output = True,
|
|
text = True,
|
|
timeout = 10,
|
|
)
|
|
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
|
return result.stdout.strip()
|
|
|
|
def test_arm64_macos_py313_tightened(self, tmp_path):
|
|
out = self._run(tmp_path, py_minor = 13, os_val = "macos", arch = "arm64")
|
|
assert out == "torch>=2.6,<2.11.0"
|
|
|
|
def test_arm64_macos_py314_tightened(self, tmp_path):
|
|
out = self._run(tmp_path, py_minor = 14, os_val = "macos", arch = "arm64")
|
|
assert out == "torch>=2.6,<2.11.0"
|
|
|
|
def test_arm64_macos_py312_default(self, tmp_path):
|
|
out = self._run(tmp_path, py_minor = 12, os_val = "macos", arch = "arm64")
|
|
assert out == "torch>=2.4,<2.11.0"
|
|
|
|
def test_arm64_macos_py311_default(self, tmp_path):
|
|
out = self._run(tmp_path, py_minor = 11, os_val = "macos", arch = "arm64")
|
|
assert out == "torch>=2.4,<2.11.0"
|
|
|
|
# Linux is unaffected by the tightening.
|
|
def test_linux_x86_py313_default(self, tmp_path):
|
|
out = self._run(tmp_path, py_minor = 13, os_val = "linux", arch = "x86_64")
|
|
assert out == "torch>=2.4,<2.11.0"
|
|
|
|
def test_linux_aarch64_py313_default(self, tmp_path):
|
|
out = self._run(tmp_path, py_minor = 13, os_val = "linux", arch = "aarch64")
|
|
assert out == "torch>=2.4,<2.11.0"
|
|
|
|
# Intel Mac: arch mismatch, no tightening.
|
|
def test_intel_mac_x86_py313_default(self, tmp_path):
|
|
out = self._run(tmp_path, py_minor = 13, os_val = "macos", arch = "x86_64")
|
|
assert out == "torch>=2.4,<2.11.0"
|
|
|
|
# SKIP_TORCH bypasses the tightening.
|
|
def test_skip_torch_arm64_macos_py313_default(self, tmp_path):
|
|
out = self._run(
|
|
tmp_path,
|
|
py_minor = 13,
|
|
os_val = "macos",
|
|
arch = "arm64",
|
|
skip_torch = "true",
|
|
)
|
|
assert out == "torch>=2.4,<2.11.0"
|
|
|
|
def test_wsl_py313_default(self, tmp_path):
|
|
out = self._run(tmp_path, py_minor = 13, os_val = "wsl", arch = "x86_64")
|
|
assert out == "torch>=2.4,<2.11.0"
|
|
|
|
def test_py_minor_0_fallback_default(self, tmp_path):
|
|
"""Failed python query (returns 0) keeps the default constraint."""
|
|
out = self._run(tmp_path, py_minor = 0, os_val = "macos", arch = "arm64")
|
|
assert out == "torch>=2.4,<2.11.0"
|
|
|
|
def test_boundary_py_minor_12_not_tightened(self, tmp_path):
|
|
out = self._run(tmp_path, py_minor = 12, os_val = "macos", arch = "arm64")
|
|
assert out == "torch>=2.4,<2.11.0"
|
|
|
|
def test_boundary_py_minor_13_tightened(self, tmp_path):
|
|
out = self._run(tmp_path, py_minor = 13, os_val = "macos", arch = "arm64")
|
|
assert out == "torch>=2.6,<2.11.0"
|
|
|
|
def test_mock_uv_receives_correct_constraint(self, tmp_path):
|
|
"""A mock uv receives the tightened constraint on py3.13 arm64 macOS."""
|
|
venv = self._make_mock_python(tmp_path, minor = 13)
|
|
|
|
# Mock uv logs its arguments.
|
|
mock_uv = tmp_path / "mock_uv"
|
|
log_file = tmp_path / "uv_log.txt"
|
|
mock_uv.write_text(
|
|
textwrap.dedent(f"""\
|
|
#!/bin/bash
|
|
echo "$@" >> {log_file}
|
|
""")
|
|
)
|
|
mock_uv.chmod(0o755)
|
|
|
|
script = textwrap.dedent(f"""\
|
|
#!/bin/bash
|
|
set -e
|
|
SKIP_TORCH=false
|
|
OS="macos"
|
|
_ARCH="arm64"
|
|
VENV_DIR="{venv}"
|
|
|
|
TORCH_CONSTRAINT="torch>=2.4,<2.11.0"
|
|
if [ "$SKIP_TORCH" = false ] && [ "$OS" = "macos" ] && [ "$_ARCH" = "arm64" ]; then
|
|
_PY_MINOR=$("$VENV_DIR/bin/python" -c \\
|
|
"import sys; print(sys.version_info.minor)" 2>/dev/null || echo "0")
|
|
if [ "$_PY_MINOR" -ge 13 ] 2>/dev/null; then
|
|
TORCH_CONSTRAINT="torch>=2.6,<2.11.0"
|
|
fi
|
|
fi
|
|
# Simulate the uv pip install line
|
|
{mock_uv} pip install --python "$VENV_DIR/bin/python" "$TORCH_CONSTRAINT" torchvision torchaudio
|
|
""")
|
|
script_file = tmp_path / "test_uv.sh"
|
|
script_file.write_text(script)
|
|
script_file.chmod(0o755)
|
|
|
|
result = subprocess.run(
|
|
["bash", str(script_file)],
|
|
capture_output = True,
|
|
text = True,
|
|
timeout = 10,
|
|
)
|
|
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
|
logged = log_file.read_text()
|
|
assert "torch>=2.6,<2.11.0" in logged, f"uv log: {logged}"
|
|
|
|
def test_mock_uv_receives_default_constraint(self, tmp_path):
|
|
"""On py3.12 arm64 macOS, uv should receive the default constraint."""
|
|
venv = self._make_mock_python(tmp_path, minor = 12)
|
|
mock_uv = tmp_path / "mock_uv"
|
|
log_file = tmp_path / "uv_log.txt"
|
|
mock_uv.write_text(
|
|
textwrap.dedent(f"""\
|
|
#!/bin/bash
|
|
echo "$@" >> {log_file}
|
|
""")
|
|
)
|
|
mock_uv.chmod(0o755)
|
|
|
|
script = textwrap.dedent(f"""\
|
|
#!/bin/bash
|
|
set -e
|
|
SKIP_TORCH=false
|
|
OS="macos"
|
|
_ARCH="arm64"
|
|
VENV_DIR="{venv}"
|
|
|
|
TORCH_CONSTRAINT="torch>=2.4,<2.11.0"
|
|
if [ "$SKIP_TORCH" = false ] && [ "$OS" = "macos" ] && [ "$_ARCH" = "arm64" ]; then
|
|
_PY_MINOR=$("$VENV_DIR/bin/python" -c \\
|
|
"import sys; print(sys.version_info.minor)" 2>/dev/null || echo "0")
|
|
if [ "$_PY_MINOR" -ge 13 ] 2>/dev/null; then
|
|
TORCH_CONSTRAINT="torch>=2.6,<2.11.0"
|
|
fi
|
|
fi
|
|
{mock_uv} pip install --python "$VENV_DIR/bin/python" "$TORCH_CONSTRAINT" torchvision torchaudio
|
|
""")
|
|
script_file = tmp_path / "test_uv.sh"
|
|
script_file.write_text(script)
|
|
script_file.chmod(0o755)
|
|
|
|
result = subprocess.run(
|
|
["bash", str(script_file)],
|
|
capture_output = True,
|
|
text = True,
|
|
timeout = 10,
|
|
)
|
|
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
|
logged = log_file.read_text()
|
|
assert "torch>=2.4,<2.11.0" in logged, f"uv log: {logged}"
|
|
|
|
|
|
# Group 3 -- E2E tokenizers fix (requires network, ~2-5 min)
|
|
@pytest.mark.e2e
|
|
class TestE2ETokenizersFix:
|
|
"""Real uv venvs verify tokenizers + transformers work without torch installed."""
|
|
|
|
@staticmethod
|
|
def _create_venv(tmp_path: pathlib.Path, name: str, py: str) -> pathlib.Path:
|
|
venv = tmp_path / name
|
|
result = subprocess.run(
|
|
["uv", "venv", str(venv), "--python", py],
|
|
capture_output = True,
|
|
text = True,
|
|
timeout = 120,
|
|
)
|
|
if result.returncode != 0:
|
|
pytest.skip(f"uv venv creation failed for {py}: {result.stderr}")
|
|
return venv
|
|
|
|
@staticmethod
|
|
def _pip_install(venv: pathlib.Path, *args: str) -> subprocess.CompletedProcess:
|
|
py = str(venv / "bin" / "python")
|
|
cmd = ["uv", "pip", "install", "--python", py, *args]
|
|
return subprocess.run(cmd, capture_output = True, text = True, timeout = 300)
|
|
|
|
@staticmethod
|
|
def _run_python(venv: pathlib.Path, code: str) -> subprocess.CompletedProcess:
|
|
py = str(venv / "bin" / "python")
|
|
return subprocess.run(
|
|
[py, "-c", code],
|
|
capture_output = True,
|
|
text = True,
|
|
timeout = 60,
|
|
)
|
|
|
|
@pytest.mark.parametrize("py_version", ["3.12", "3.13"])
|
|
def test_autoconfig_works_with_no_torch_runtime(self, tmp_path, py_version):
|
|
"""Install no-torch-runtime.txt with --no-deps, then AutoConfig must import."""
|
|
venv = self._create_venv(tmp_path, f"tok-{py_version}", py_version)
|
|
r = self._pip_install(venv, "--no-deps", "-r", str(_NO_TORCH_RT))
|
|
assert r.returncode == 0, f"Install failed: {r.stderr}"
|
|
|
|
result = self._run_python(venv, "from transformers import AutoConfig; print('OK')")
|
|
assert (
|
|
result.returncode == 0
|
|
), f"AutoConfig import failed:\nstdout: {result.stdout}\nstderr: {result.stderr}"
|
|
assert "OK" in result.stdout
|
|
|
|
@pytest.mark.parametrize("py_version", ["3.12", "3.13"])
|
|
def test_tokenizers_directly_importable(self, tmp_path, py_version):
|
|
venv = self._create_venv(tmp_path, f"tok-imp-{py_version}", py_version)
|
|
r = self._pip_install(venv, "--no-deps", "-r", str(_NO_TORCH_RT))
|
|
assert r.returncode == 0, f"Install failed: {r.stderr}"
|
|
result = self._run_python(venv, "import tokenizers; print('OK')")
|
|
assert result.returncode == 0, f"Failed: {result.stderr}"
|
|
|
|
@pytest.mark.parametrize("py_version", ["3.12", "3.13"])
|
|
def test_torch_not_importable(self, tmp_path, py_version):
|
|
"""In the no-torch scenario, torch should not be available."""
|
|
venv = self._create_venv(tmp_path, f"no-torch-{py_version}", py_version)
|
|
r = self._pip_install(venv, "--no-deps", "-r", str(_NO_TORCH_RT))
|
|
assert r.returncode == 0, f"Install failed: {r.stderr}"
|
|
result = self._run_python(venv, "import torch")
|
|
assert result.returncode != 0, "torch should NOT be importable"
|
|
|
|
def test_negative_control_no_tokenizers(self, tmp_path):
|
|
"""Without the tokenizers line, AutoConfig must fail (negative control)."""
|
|
venv = self._create_venv(tmp_path, "neg-ctrl", "3.12")
|
|
req_no_tokenizers = tmp_path / "no-tokenizers.txt"
|
|
req_no_tokenizers.write_text(
|
|
"\n".join(
|
|
line for line in _read(_NO_TORCH_RT).splitlines() if line.strip() != "tokenizers"
|
|
),
|
|
encoding = "utf-8",
|
|
)
|
|
r = self._pip_install(venv, "--no-deps", "-r", str(req_no_tokenizers))
|
|
assert r.returncode == 0, f"Install failed: {r.stderr}"
|
|
result = self._run_python(venv, "from transformers import AutoConfig")
|
|
assert result.returncode != 0, "AutoConfig should fail without tokenizers installed"
|
|
assert "tokenizers" in result.stderr.lower() or "ModuleNotFoundError" in result.stderr
|
|
|
|
|
|
# Group 4 -- Integration: install.sh reads no-torch-runtime.txt correctly
|
|
class TestInstallShNoTorchIntegration:
|
|
"""Verify install.sh has the correct no-torch-runtime.txt wiring."""
|
|
|
|
_sh = _read(_INSTALL_SH)
|
|
|
|
def test_find_no_torch_runtime_exists(self):
|
|
assert "_find_no_torch_runtime()" in self._sh
|
|
|
|
def test_no_deps_invocation_for_migrated(self):
|
|
"""Migrated path should use --no-deps -r."""
|
|
assert '--no-deps -r "$_NO_TORCH_RT"' in self._sh
|
|
|
|
def test_no_deps_invocation_for_fresh(self):
|
|
"""Fresh install path should also use --no-deps -r."""
|
|
count = self._sh.count('--no-deps -r "$_NO_TORCH_RT"')
|
|
assert count >= 2, f"Expected >=2 no-deps -r invocations, found {count}"
|
|
|
|
def test_mock_uv_skip_torch_reads_requirements(self, tmp_path):
|
|
"""SKIP_TORCH=true blocks must call _find_no_torch_runtime."""
|
|
skip_blocks = re.findall(
|
|
r'if \[ "\$SKIP_TORCH" = true \].*?(?=\n (?:else|elif|fi))',
|
|
self._sh,
|
|
re.DOTALL,
|
|
)
|
|
found = any("_find_no_torch_runtime" in block for block in skip_blocks)
|
|
assert found, "SKIP_TORCH=true block should call _find_no_torch_runtime"
|
|
|
|
|
|
# Group 5 -- Full no-torch sandbox (requires network, ~5 min)
|
|
@pytest.mark.e2e
|
|
class TestE2EFullNoTorchSandbox:
|
|
"""Creates venvs and installs the actual no-torch-runtime.txt."""
|
|
|
|
@staticmethod
|
|
def _create_venv(tmp_path: pathlib.Path, name: str) -> pathlib.Path:
|
|
venv = tmp_path / name
|
|
result = subprocess.run(
|
|
["uv", "venv", str(venv), "--python", "3.12"],
|
|
capture_output = True,
|
|
text = True,
|
|
timeout = 120,
|
|
)
|
|
if result.returncode != 0:
|
|
pytest.skip(f"uv venv creation failed: {result.stderr}")
|
|
return venv
|
|
|
|
@staticmethod
|
|
def _pip_install(venv: pathlib.Path, *args: str) -> subprocess.CompletedProcess:
|
|
py = str(venv / "bin" / "python")
|
|
cmd = ["uv", "pip", "install", "--python", py, *args]
|
|
return subprocess.run(cmd, capture_output = True, text = True, timeout = 600)
|
|
|
|
@staticmethod
|
|
def _run_python(venv: pathlib.Path, code: str) -> subprocess.CompletedProcess:
|
|
py = str(venv / "bin" / "python")
|
|
return subprocess.run(
|
|
[py, "-c", code],
|
|
capture_output = True,
|
|
text = True,
|
|
timeout = 60,
|
|
)
|
|
|
|
def test_autoconfig_succeeds(self, tmp_path):
|
|
"""Install with --no-deps and verify AutoConfig imports (the bug fix)."""
|
|
venv = self._create_venv(tmp_path, "full-no-torch")
|
|
r = self._pip_install(venv, "--no-deps", "-r", str(_NO_TORCH_RT))
|
|
assert r.returncode == 0, f"Install failed: {r.stderr}"
|
|
result = self._run_python(venv, "from transformers import AutoConfig; print('OK')")
|
|
assert (
|
|
result.returncode == 0
|
|
), f"AutoConfig failed:\nstdout: {result.stdout}\nstderr: {result.stderr}"
|
|
|
|
def test_torch_not_importable(self, tmp_path):
|
|
"""With --no-deps, torch must not be pulled in."""
|
|
venv = self._create_venv(tmp_path, "no-torch-check")
|
|
r = self._pip_install(venv, "--no-deps", "-r", str(_NO_TORCH_RT))
|
|
assert r.returncode == 0, f"Install failed: {r.stderr}"
|
|
result = self._run_python(venv, "import torch")
|
|
assert result.returncode != 0, "torch should NOT be importable"
|
|
|
|
def test_tokenizers_importable(self, tmp_path):
|
|
venv = self._create_venv(tmp_path, "tok-check")
|
|
r = self._pip_install(venv, "--no-deps", "-r", str(_NO_TORCH_RT))
|
|
assert r.returncode == 0, f"Install failed: {r.stderr}"
|
|
result = self._run_python(venv, "import tokenizers; print('OK')")
|
|
assert result.returncode == 0, f"tokenizers import failed: {result.stderr}"
|
|
|
|
def test_safetensors_importable(self, tmp_path):
|
|
venv = self._create_venv(tmp_path, "st-check")
|
|
r = self._pip_install(venv, "--no-deps", "-r", str(_NO_TORCH_RT))
|
|
assert r.returncode == 0, f"Install failed: {r.stderr}"
|
|
result = self._run_python(venv, "import safetensors; print('OK')")
|
|
assert result.returncode == 0, f"safetensors import failed: {result.stderr}"
|
|
|
|
def test_huggingface_hub_importable(self, tmp_path):
|
|
venv = self._create_venv(tmp_path, "hfhub-check")
|
|
r = self._pip_install(venv, "--no-deps", "-r", str(_NO_TORCH_RT))
|
|
assert r.returncode == 0, f"Install failed: {r.stderr}"
|
|
result = self._run_python(venv, "import huggingface_hub; print('OK')")
|
|
assert result.returncode == 0, f"huggingface_hub import failed: {result.stderr}"
|