Reduce and tighten comments and docstrings across the test suite (#6429)
* Reduce and tighten comments and docstrings in tests Shorten verbose comments and docstrings across the test suite without changing any test logic. Remove narration that restates the next line, collapse long module and test docstrings to a single line, and drop banner separators. Keep regression context (issue and PR references, run ids), skip reasons, mocking and timing rationale, license headers, lint and type directives, and commented-out code. Comments and docstrings only: an AST signature check confirms no code, assertions, or string literals changed, and the suite byte-compiles cleanly. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
1b697ed6fc
commit
a6dc10dad2
122 changed files with 1847 additions and 4511 deletions
|
|
@ -1,15 +1,10 @@
|
|||
"""Negative-path validation tests for unsloth.chat_templates.construct_chat_template.
|
||||
|
||||
Regression coverage for the str.find() / regex no-match guards added in
|
||||
PR #5763 follow-up: missing placeholders or unrecoverable two-example
|
||||
structures must raise RuntimeError with a clear message, not IndexError
|
||||
or AttributeError, and must never silently drop the last character via
|
||||
s[:-1].
|
||||
|
||||
Uses a minimal fake tokenizer so the cases run on CPU-only CI without
|
||||
HF_TOKEN and without downloading a gated model. The validation paths
|
||||
exercised here fail before construct_chat_template reaches any heavy
|
||||
tokenizer interaction, so the stub stays small.
|
||||
Regression coverage for the no-match guards added in the PR #5763 follow-up:
|
||||
missing placeholders or unrecoverable two-example structures must raise
|
||||
RuntimeError with a clear message (not IndexError/AttributeError) and must
|
||||
not silently drop the last char via s[:-1]. A minimal fake tokenizer keeps
|
||||
the cases CPU-only (no HF_TOKEN, no gated download).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
|
@ -18,8 +13,7 @@ from unsloth.chat_templates import construct_chat_template
|
|||
|
||||
|
||||
class _FakeTokenizer:
|
||||
"""Minimum surface construct_chat_template touches before the
|
||||
validation guards fire."""
|
||||
"""Minimal surface construct_chat_template touches before the guards fire."""
|
||||
|
||||
name_or_path = "fake/tokenizer"
|
||||
eos_token = "</s>"
|
||||
|
|
@ -48,9 +42,8 @@ def test_missing_placeholder_in_chat_template_raises(template, expected_in_messa
|
|||
|
||||
|
||||
def test_single_pair_template_raises_clear_error_not_attribute_error():
|
||||
"""One {INPUT}/{OUTPUT} pair (rather than the required two) used to
|
||||
crash with AttributeError on `found.group(1)` after the for-loop
|
||||
broke without setting `found`. Must raise RuntimeError now."""
|
||||
"""A single {INPUT}/{OUTPUT} pair must raise RuntimeError, not the old
|
||||
AttributeError on `found.group(1)` when the loop broke without setting `found`."""
|
||||
template = "user: {INPUT}\nassistant: {OUTPUT}\n"
|
||||
with pytest.raises(RuntimeError):
|
||||
construct_chat_template(
|
||||
|
|
@ -71,7 +64,6 @@ def test_error_message_excerpt_is_bounded():
|
|||
extra_eos_tokens = ["</s>"],
|
||||
)
|
||||
msg = str(exc_info.value)
|
||||
# Excerpt is repr-quoted and capped; total message should stay well
|
||||
# under the template length.
|
||||
# Excerpt is capped well under the template length.
|
||||
assert len(msg) < 1000
|
||||
assert "{OUTPUT}" in msg
|
||||
|
|
|
|||
|
|
@ -13,11 +13,7 @@ INSTALL_PS1 = REPO_ROOT / "install.ps1"
|
|||
|
||||
|
||||
class TestNoTorchBackendAutoInInstallSh:
|
||||
"""install.sh primary install paths must not use --torch-backend=auto.
|
||||
|
||||
The fallback else-branch (when TORCH_INDEX_URL is empty) is allowed to
|
||||
use --torch-backend=auto since that is the last-resort recovery path.
|
||||
"""
|
||||
"""install.sh primary paths must not use --torch-backend=auto (only the fallback else-branch may)."""
|
||||
|
||||
def test_no_torch_backend_auto_outside_fallback(self):
|
||||
lines = INSTALL_SH.read_text(encoding = "utf-8").splitlines()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,4 @@
|
|||
"""E2E sandbox tests for PR #4624 (fix/install-mac-intel-no-torch): BEFORE
|
||||
(top-level torch) crashes, AFTER (lazy imports) works, broken/partial torch,
|
||||
CPU hardware fallback, install.sh parsing, NO_TORCH filtering, and live server.
|
||||
|
||||
Run:
|
||||
# Lightweight (Groups 1-6):
|
||||
python -m pytest tests/python/test_e2e_no_torch_sandbox.py -v -k "not server"
|
||||
# Server (Group 7, requires studio venv):
|
||||
python -m pytest tests/python/test_e2e_no_torch_sandbox.py -v -m server
|
||||
"""
|
||||
"""E2E sandbox tests for PR #4624: lazy torch imports, CPU fallback, install.sh parsing, NO_TORCH filtering, live server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -23,10 +14,6 @@ from unittest import mock
|
|||
|
||||
import pytest
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Paths
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
STUDIO_DIR = REPO_ROOT / "studio"
|
||||
BACKEND_DIR = STUDIO_DIR / "backend"
|
||||
|
|
@ -45,17 +32,11 @@ HARDWARE_PY = HARDWARE_DIR / "hardware.py"
|
|||
# Studio venv for server tests
|
||||
STUDIO_VENV = Path.home() / ".unsloth" / "studio" / "unsloth_studio"
|
||||
|
||||
# Add studio to path for install_python_stack imports
|
||||
sys.path.insert(0, str(STUDIO_DIR))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cross-platform helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _venv_python(venv_dir: Path) -> Path:
|
||||
"""Return the Python executable path for a venv, cross-platform."""
|
||||
"""Return a venv's Python executable path, cross-platform."""
|
||||
if sys.platform == "win32":
|
||||
return venv_dir / "Scripts" / "python.exe"
|
||||
return venv_dir / "bin" / "python"
|
||||
|
|
@ -76,7 +57,6 @@ def _create_no_torch_venv(venv_dir: Path, python_version: str = "3.12") -> Path
|
|||
py = _venv_python(venv_dir)
|
||||
if not py.exists():
|
||||
return None
|
||||
# Verify torch is NOT importable
|
||||
check = subprocess.run([str(py), "-c", "import torch"], capture_output = True)
|
||||
if check.returncode == 0:
|
||||
return None
|
||||
|
|
@ -107,13 +87,8 @@ def _run_sh(script: str, timeout: int = 30) -> subprocess.CompletedProcess:
|
|||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stub generators
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _write_loggers_stub(sandbox: Path) -> None:
|
||||
"""Create a minimal loggers package stub (replaces structlog-backed real one)."""
|
||||
"""Create a minimal loggers package stub (replaces the structlog-backed real one)."""
|
||||
loggers_dir = sandbox / "loggers"
|
||||
loggers_dir.mkdir(exist_ok = True)
|
||||
(loggers_dir / "__init__.py").write_text(
|
||||
|
|
@ -165,11 +140,6 @@ def _write_hardware_stub(sandbox: Path) -> None:
|
|||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture(scope = "session")
|
||||
def repo_root():
|
||||
return REPO_ROOT
|
||||
|
|
@ -183,10 +153,7 @@ def sandbox_dir(tmp_path):
|
|||
|
||||
@pytest.fixture(params = ["3.12", "3.13"], scope = "module")
|
||||
def no_torch_venv(request, tmp_path_factory):
|
||||
"""Create a temporary uv venv with no torch.
|
||||
|
||||
Parametrized for 3.12 (Intel Mac default) and 3.13 (Apple Silicon/Linux).
|
||||
"""
|
||||
"""Temporary uv venv with no torch; 3.12 = Intel Mac default, 3.13 = Apple Silicon/Linux."""
|
||||
if not _has_uv():
|
||||
pytest.skip("uv not available")
|
||||
|
||||
|
|
@ -198,20 +165,16 @@ def no_torch_venv(request, tmp_path_factory):
|
|||
return str(py)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Group 1: BEFORE vs AFTER -- Import Chain (6 tests)
|
||||
# ===========================================================================
|
||||
# Group 1: BEFORE vs AFTER -- Import Chain
|
||||
|
||||
|
||||
class TestBeforeAfterImportChain:
|
||||
"""BEFORE (PR files with a synthetic top-level torch import, simulating
|
||||
main) crashes; AFTER (PR files as-is, lazy imports) works."""
|
||||
"""BEFORE (synthetic top-level torch import) crashes; AFTER (lazy imports) works."""
|
||||
|
||||
# -- BEFORE: crashes --
|
||||
|
||||
def test_before_chat_templates_crashes(self, no_torch_venv, sandbox_dir):
|
||||
"""BEFORE: chat_templates.py with top-level 'from torch.utils.data import
|
||||
IterableDataset' crashes without torch."""
|
||||
"""BEFORE: chat_templates.py with top-level IterableDataset import crashes without torch."""
|
||||
source = CHAT_TEMPLATES.read_text(encoding = "utf-8")
|
||||
before_source = "from torch.utils.data import IterableDataset\n" + source
|
||||
|
||||
|
|
@ -284,7 +247,6 @@ class TestBeforeAfterImportChain:
|
|||
encoding = "utf-8",
|
||||
)
|
||||
|
||||
# Minimal __init__.py that triggers the chain
|
||||
(pkg_dir / "__init__.py").write_text(
|
||||
textwrap.dedent("""\
|
||||
from .format_detection import detect_dataset_format
|
||||
|
|
@ -365,7 +327,6 @@ class TestBeforeAfterImportChain:
|
|||
if src.exists():
|
||||
shutil.copy2(src, pkg_dir / src.name)
|
||||
|
||||
# Minimal __init__.py
|
||||
(pkg_dir / "__init__.py").write_text(
|
||||
textwrap.dedent("""\
|
||||
from .format_detection import detect_dataset_format, detect_custom_format_heuristic
|
||||
|
|
@ -402,14 +363,11 @@ class TestBeforeAfterImportChain:
|
|||
assert b"OK: full import chain succeeded" in result.stdout
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Group 2: Dataclass Instantiation (4 tests)
|
||||
# ===========================================================================
|
||||
# Group 2: Dataclass Instantiation
|
||||
|
||||
|
||||
class TestDataclassInstantiation:
|
||||
"""Verify dataclass collators can be instantiated and constants accessed
|
||||
without torch in an isolated venv."""
|
||||
"""Dataclass collators instantiate and constants are accessible without torch."""
|
||||
|
||||
def test_speech_collator_instantiate(self, no_torch_venv):
|
||||
"""DataCollatorSpeechSeq2SeqWithPadding(processor=None) succeeds."""
|
||||
|
|
@ -485,19 +443,14 @@ class TestDataclassInstantiation:
|
|||
assert result.returncode == 0, f"Failed:\n{result.stderr.decode()}"
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Group 3: Edge Cases -- Partial/Broken Torch (4 tests)
|
||||
# ===========================================================================
|
||||
# Group 3: Edge Cases -- Partial/Broken Torch
|
||||
|
||||
|
||||
class TestEdgeCasesBrokenTorch:
|
||||
"""Test behavior with fake or broken torch modules on sys.path."""
|
||||
"""Behavior with fake or broken torch modules on sys.path."""
|
||||
|
||||
def test_fake_broken_torch_module(self, no_torch_venv, sandbox_dir):
|
||||
"""A fake torch that raises RuntimeError('CUDA not found') on import.
|
||||
|
||||
data_collators.py (no top-level torch import) should still load fine.
|
||||
"""
|
||||
"""Fake torch raising RuntimeError on import: data_collators.py (no top-level torch) still loads."""
|
||||
torch_dir = sandbox_dir / "torch"
|
||||
torch_dir.mkdir()
|
||||
(torch_dir / "__init__.py").write_text(
|
||||
|
|
@ -519,7 +472,7 @@ class TestEdgeCasesBrokenTorch:
|
|||
assert b"OK:" in result.stdout
|
||||
|
||||
def test_torch_import_error_hardware_fallback(self, no_torch_venv, sandbox_dir):
|
||||
"""A fake torch that raises ImportError. detect_hardware() falls back to CPU."""
|
||||
"""Fake torch raising ImportError: detect_hardware() falls back to CPU."""
|
||||
torch_dir = sandbox_dir / "torch"
|
||||
torch_dir.mkdir()
|
||||
(torch_dir / "__init__.py").write_text(
|
||||
|
|
@ -546,10 +499,7 @@ class TestEdgeCasesBrokenTorch:
|
|||
assert b"OK: detect_hardware returned CPU" in result.stdout
|
||||
|
||||
def test_fake_torch_no_cuda(self, no_torch_venv, sandbox_dir):
|
||||
"""Fake torch that imports OK but torch.cuda.is_available() returns False.
|
||||
|
||||
detect_hardware() should still fall back to CPU.
|
||||
"""
|
||||
"""Fake torch imports OK but cuda.is_available() is False: detect_hardware() falls back to CPU."""
|
||||
torch_dir = sandbox_dir / "torch"
|
||||
torch_dir.mkdir()
|
||||
(torch_dir / "__init__.py").write_text(
|
||||
|
|
@ -582,12 +532,7 @@ class TestEdgeCasesBrokenTorch:
|
|||
assert b"OK:" in result.stdout
|
||||
|
||||
def test_lazy_torch_fails_at_call_time_not_import_time(self, no_torch_venv, sandbox_dir):
|
||||
"""apply_chat_template_to_dataset is importable without torch.
|
||||
|
||||
Calling the alpaca branch triggers the lazy 'from torch.utils.data' inside
|
||||
the try block. This should fail at call time, not import time -- proving the
|
||||
lazy import pattern works correctly.
|
||||
"""
|
||||
"""apply_chat_template_to_dataset imports without torch; the lazy import fails at call time, not import time."""
|
||||
_write_loggers_stub(sandbox_dir)
|
||||
|
||||
code = textwrap.dedent(f"""\
|
||||
|
|
@ -631,16 +576,14 @@ class TestEdgeCasesBrokenTorch:
|
|||
assert b"OK: import succeeded" in result.stdout
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Group 4: Hardware Detection Without Torch (3 tests)
|
||||
# ===========================================================================
|
||||
# Group 4: Hardware Detection Without Torch
|
||||
|
||||
|
||||
class TestHardwareDetectionNoTorch:
|
||||
"""Hardware module works without torch, falling back to CPU."""
|
||||
|
||||
def test_detect_hardware_no_torch(self, no_torch_venv, sandbox_dir):
|
||||
"""detect_hardware() returns CPU device when torch is not installed."""
|
||||
"""detect_hardware() returns CPU when torch is not installed."""
|
||||
_write_loggers_stub(sandbox_dir)
|
||||
_write_structlog_stub(sandbox_dir)
|
||||
|
||||
|
|
@ -680,7 +623,7 @@ class TestHardwareDetectionNoTorch:
|
|||
assert b"OK:" in result.stdout
|
||||
|
||||
def test_hardware_module_import_no_torch(self, no_torch_venv, sandbox_dir):
|
||||
"""The hardware module imports and detect_hardware is callable without torch."""
|
||||
"""Hardware module imports and detect_hardware is callable without torch."""
|
||||
_write_loggers_stub(sandbox_dir)
|
||||
_write_structlog_stub(sandbox_dir)
|
||||
_write_hardware_stub(sandbox_dir)
|
||||
|
|
@ -707,13 +650,11 @@ class TestHardwareDetectionNoTorch:
|
|||
assert b"OK:" in result.stdout
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Group 5: install.sh Logic (5 tests via bash subprocess)
|
||||
# ===========================================================================
|
||||
# Group 5: install.sh Logic (via bash subprocess)
|
||||
|
||||
|
||||
class TestInstallShLogic:
|
||||
"""Test install.sh flag parsing, platform detection, and guard logic."""
|
||||
"""install.sh flag parsing, platform detection, and guard logic."""
|
||||
|
||||
@pytest.fixture(autouse = True)
|
||||
def _check_install_sh(self):
|
||||
|
|
@ -722,7 +663,6 @@ class TestInstallShLogic:
|
|||
|
||||
def test_python_flag_parsing(self):
|
||||
"""--python flag correctly sets _USER_PYTHON."""
|
||||
# Extract flag parser snippet from install.sh and test it
|
||||
script = textwrap.dedent("""\
|
||||
_USER_PYTHON=""
|
||||
_next_is_python=false
|
||||
|
|
@ -738,9 +678,8 @@ class TestInstallShLogic:
|
|||
done
|
||||
echo "$_USER_PYTHON"
|
||||
""")
|
||||
# Test: --python 3.12
|
||||
# --python 3.12
|
||||
r = _run_sh(f"{script}" + "\n", timeout = 10)
|
||||
# Need to pass args to the script
|
||||
r = subprocess.run(
|
||||
["bash", "-c", script + "\n", "_", "--python", "3.12"],
|
||||
capture_output = True,
|
||||
|
|
@ -748,7 +687,7 @@ class TestInstallShLogic:
|
|||
)
|
||||
assert r.stdout.strip() == b"3.12"
|
||||
|
||||
# Test: --local --python 3.11
|
||||
# --local --python 3.11
|
||||
r = subprocess.run(
|
||||
["bash", "-c", script + "\n", "_", "--local", "--python", "3.11"],
|
||||
capture_output = True,
|
||||
|
|
@ -756,7 +695,7 @@ class TestInstallShLogic:
|
|||
)
|
||||
assert r.stdout.strip() == b"3.11"
|
||||
|
||||
# Test: no --python flag
|
||||
# no --python flag
|
||||
r = subprocess.run(
|
||||
["bash", "-c", script + "\n", "_", "--local"],
|
||||
capture_output = True,
|
||||
|
|
@ -766,7 +705,6 @@ class TestInstallShLogic:
|
|||
|
||||
def test_python_flag_missing_arg_errors(self):
|
||||
"""--python without a version argument triggers an error."""
|
||||
# Extract the flag parser + error guard from install.sh
|
||||
script = textwrap.dedent("""\
|
||||
set -e
|
||||
_USER_PYTHON=""
|
||||
|
|
@ -819,7 +757,7 @@ class TestInstallShLogic:
|
|||
)
|
||||
assert r.stdout.strip() == b"3.12"
|
||||
|
||||
# Non-Intel, no override
|
||||
# non-Intel, no override
|
||||
r = subprocess.run(
|
||||
["bash", "-c", script + "\n", "_", "false", ""],
|
||||
capture_output = True,
|
||||
|
|
@ -865,7 +803,6 @@ class TestInstallShLogic:
|
|||
|
||||
def test_stale_venv_guard_respects_override(self):
|
||||
"""When _USER_PYTHON is set, the stale venv recreation guard is skipped."""
|
||||
# The guard: if MAC_INTEL=true && -z _USER_PYTHON && venv exists ...
|
||||
script = textwrap.dedent("""\
|
||||
MAC_INTEL=true
|
||||
_USER_PYTHON="$1"
|
||||
|
|
@ -877,7 +814,7 @@ class TestInstallShLogic:
|
|||
fi
|
||||
echo "$SHOULD_RECREATE"
|
||||
""")
|
||||
# With override: should NOT recreate
|
||||
# with override: should NOT recreate
|
||||
r = subprocess.run(
|
||||
["bash", "-c", script + "\n", "_", "3.11"],
|
||||
capture_output = True,
|
||||
|
|
@ -885,7 +822,7 @@ class TestInstallShLogic:
|
|||
)
|
||||
assert r.stdout.strip() == b"false"
|
||||
|
||||
# Without override: SHOULD recreate
|
||||
# without override: SHOULD recreate
|
||||
r = subprocess.run(
|
||||
["bash", "-c", script + "\n", "_", ""],
|
||||
capture_output = True,
|
||||
|
|
@ -894,13 +831,11 @@ class TestInstallShLogic:
|
|||
assert r.stdout.strip() == b"true"
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Group 6: install_python_stack.py NO_TORCH Filtering (4 tests)
|
||||
# ===========================================================================
|
||||
# Group 6: install_python_stack.py NO_TORCH Filtering
|
||||
|
||||
|
||||
class TestInstallPythonStackFiltering:
|
||||
"""Test the NO_TORCH filtering logic in install_python_stack.py."""
|
||||
"""NO_TORCH filtering logic in install_python_stack.py."""
|
||||
|
||||
@pytest.fixture(autouse = True)
|
||||
def _check_install_py(self):
|
||||
|
|
@ -956,7 +891,7 @@ class TestInstallPythonStackFiltering:
|
|||
):
|
||||
assert ips._infer_no_torch() is True
|
||||
|
||||
# Explicit false on Intel Mac
|
||||
# explicit false on Intel Mac
|
||||
with (
|
||||
mock.patch.dict(os.environ, {"UNSLOTH_NO_TORCH": "false"}),
|
||||
mock.patch.object(ips, "IS_MAC_INTEL", True),
|
||||
|
|
@ -978,7 +913,6 @@ class TestInstallPythonStackFiltering:
|
|||
|
||||
source = Path(ips.__file__).read_text(encoding = "utf-8")
|
||||
|
||||
# NO_TORCH guard before overrides
|
||||
assert "if NO_TORCH:" in source, "NO_TORCH guard not found in install_python_stack.py"
|
||||
|
||||
# macOS guard for triton
|
||||
|
|
@ -987,9 +921,7 @@ class TestInstallPythonStackFiltering:
|
|||
), "'not IS_WINDOWS and not IS_MACOS' guard for triton not found"
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Group 7: Live Server Startup (4 tests) -- Heavyweight
|
||||
# ===========================================================================
|
||||
# Group 7: Live Server Startup -- Heavyweight
|
||||
|
||||
|
||||
def _studio_venv_python() -> Path | None:
|
||||
|
|
@ -1013,14 +945,7 @@ server = pytest.mark.server
|
|||
|
||||
@server
|
||||
class TestLiveServerStartup:
|
||||
"""Live server startup tests.
|
||||
|
||||
These use the existing Studio venv at ~/.unsloth/studio/unsloth_studio.
|
||||
They temporarily ensure torch is not importable, test server startup,
|
||||
then leave the venv unchanged.
|
||||
|
||||
Run separately: pytest -m server
|
||||
"""
|
||||
"""Live server startup against the existing Studio venv with torch made unimportable (pytest -m server)."""
|
||||
|
||||
@pytest.fixture(autouse = True)
|
||||
def _check_studio_venv(self):
|
||||
|
|
@ -1038,7 +963,6 @@ class TestLiveServerStartup:
|
|||
port = _server_port()
|
||||
backend_dir = BACKEND_DIR
|
||||
|
||||
# Check if torch is installed in the studio venv
|
||||
check = subprocess.run(
|
||||
[str(py), "-c", "import torch; print(torch.__version__)"],
|
||||
capture_output = True,
|
||||
|
|
@ -1046,7 +970,6 @@ class TestLiveServerStartup:
|
|||
torch_was_installed = check.returncode == 0
|
||||
torch_version = check.stdout.decode().strip() if torch_was_installed else None
|
||||
|
||||
# Uninstall torch if present
|
||||
if torch_was_installed:
|
||||
subprocess.run(
|
||||
[
|
||||
|
|
@ -1063,7 +986,6 @@ class TestLiveServerStartup:
|
|||
timeout = 120,
|
||||
)
|
||||
|
||||
# Start server
|
||||
env = os.environ.copy()
|
||||
env["PYTHONPATH"] = str(backend_dir)
|
||||
proc = subprocess.Popen(
|
||||
|
|
@ -1091,7 +1013,6 @@ class TestLiveServerStartup:
|
|||
|
||||
if not ready:
|
||||
stdout, stderr = proc.communicate(timeout = 5)
|
||||
# Reinstall torch + torchvision + torchaudio
|
||||
if torch_was_installed and torch_version:
|
||||
subprocess.run(
|
||||
[
|
||||
|
|
@ -1189,7 +1110,7 @@ class TestLiveServerStartup:
|
|||
try:
|
||||
urllib.request.urlopen(f"http://127.0.0.1:{port}{ep}", timeout = 5)
|
||||
except urllib.error.HTTPError:
|
||||
pass # 4xx/5xx is fine -- server didn't crash
|
||||
pass # 4xx/5xx fine -- server didn't crash
|
||||
except urllib.error.URLError:
|
||||
pytest.fail(f"Server stopped responding at {ep}")
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ def _param_default(method, name):
|
|||
|
||||
|
||||
def _load_text_only_namespace():
|
||||
# Exec the text-only helpers from _utils into one namespace (no unsloth import),
|
||||
# Exec the _utils text-only helpers into one namespace (no unsloth import),
|
||||
# in dependency order so cross-references resolve.
|
||||
source = _source(UTILS_PATH)
|
||||
import transformers
|
||||
|
|
@ -123,8 +123,7 @@ def test_fast_language_model_forwards_text_only_to_fast_model():
|
|||
source = _source(LOADER_PATH)
|
||||
method = _class_method(ast.parse(source), "FastLanguageModel", "from_pretrained")
|
||||
|
||||
# text_only defaults False (opt-in, not forced True), and both FastModel
|
||||
# delegations forward it.
|
||||
# text_only defaults False (opt-in); both FastModel delegations forward it.
|
||||
text_only_default = _param_default(method, "text_only")
|
||||
assert isinstance(text_only_default, ast.Constant) and text_only_default.value is False
|
||||
|
||||
|
|
@ -206,22 +205,14 @@ def test_fast_base_model_text_only_bypasses_vision_auto_model():
|
|||
|
||||
|
||||
def test_gemma3_text_only_model_class_resolves_and_has_no_vision_tower():
|
||||
"""Tiny end-to-end: build a Gemma3 text-only config, instantiate the
|
||||
matching model class with shrunken hidden sizes, assert it has the
|
||||
text language model attributes and no vision tower attribute.
|
||||
|
||||
This is the integration check the AST-only tests were missing -- it
|
||||
proves the text-only routing actually produces a model that can be
|
||||
instantiated and that the resulting model is purely text. We use
|
||||
shrunken hidden sizes so the test is fast and CPU-only.
|
||||
"""
|
||||
"""End-to-end: a tiny Gemma3 text-only model instantiates with text LM attrs and no vision tower."""
|
||||
transformers = pytest.importorskip("transformers")
|
||||
helper = _load_text_only_helper()
|
||||
|
||||
full_config = transformers.Gemma3Config()
|
||||
text_config = helper(full_config, "google/gemma-3-27b-it")
|
||||
|
||||
# Shrink for a cheap CPU instantiation; keep the shape attrs read at construction.
|
||||
# Shrink for cheap CPU instantiation.
|
||||
text_config.num_hidden_layers = 1
|
||||
text_config.hidden_size = 32
|
||||
text_config.intermediate_size = 32
|
||||
|
|
@ -233,10 +224,9 @@ def test_gemma3_text_only_model_class_resolves_and_has_no_vision_tower():
|
|||
model_class = transformers.AutoModelForCausalLM._model_mapping[type(text_config)]
|
||||
model = model_class(text_config)
|
||||
|
||||
# Positive checks: text language model surface is present.
|
||||
assert hasattr(model, "lm_head"), "text-only Gemma3 model should expose lm_head"
|
||||
|
||||
# Negative checks: no vision tower / multimodal projector remains.
|
||||
# No vision tower / multimodal projector remains.
|
||||
assert not hasattr(
|
||||
model, "vision_tower"
|
||||
), "text-only Gemma3 model should not have a vision_tower"
|
||||
|
|
@ -246,7 +236,7 @@ def test_gemma3_text_only_model_class_resolves_and_has_no_vision_tower():
|
|||
|
||||
|
||||
def test_helper_defined_once_in_utils_and_imported():
|
||||
# _get_text_only_config is defined only in _utils and imported by loader + vision.
|
||||
# _get_text_only_config defined only in _utils, imported by loader + vision.
|
||||
def _defines(path):
|
||||
return any(
|
||||
isinstance(n, ast.FunctionDef) and n.name == "_get_text_only_config"
|
||||
|
|
@ -274,7 +264,7 @@ def _load_util_func(name):
|
|||
|
||||
|
||||
def test_text_only_guard_predicate_across_vlm_families():
|
||||
# Text-only is taken only when the resolved class remaps VLM weights.
|
||||
# Text-only taken only when the resolved class remaps VLM weights.
|
||||
transformers = pytest.importorskip("transformers")
|
||||
from transformers import AutoModelForCausalLM
|
||||
|
||||
|
|
@ -309,7 +299,7 @@ def test_text_only_guard_predicate_across_vlm_families():
|
|||
|
||||
|
||||
def test_text_only_helper_preserves_quantization_config():
|
||||
# quantization_config must survive the strip so pre-quantized repos still load. A
|
||||
# quantization_config must survive the strip so pre-quantized repos load. A
|
||||
# sentinel object avoids a bitsandbytes dependency on transformers 4.51.3.
|
||||
transformers = pytest.importorskip("transformers")
|
||||
helper = _load_text_only_helper()
|
||||
|
|
@ -318,13 +308,13 @@ def test_text_only_helper_preserves_quantization_config():
|
|||
config.quantization_config = sentinel
|
||||
text_config = helper(config, "google/gemma-3-27b-it")
|
||||
assert getattr(text_config, "quantization_config", None) is sentinel
|
||||
# The parent's shared text sub-config must not be mutated by the carry-over.
|
||||
# The parent's shared text sub-config must not be mutated.
|
||||
assert getattr(config.get_text_config(), "quantization_config", None) is None
|
||||
|
||||
|
||||
def test_text_only_key_mapping_targets_published_prefixes():
|
||||
# The mapping must remap the published VLM decoder prefixes and only apply on
|
||||
# transformers >=5 (on 4.x base_model_prefix handles it and a mapping hurts).
|
||||
# Remap the published VLM decoder prefixes, applying only on transformers >=5
|
||||
# (on 4.x base_model_prefix handles it and a mapping hurts).
|
||||
transformers = pytest.importorskip("transformers")
|
||||
get_key_mapping = _load_util_func("_get_text_only_key_mapping")
|
||||
mapping = get_key_mapping(transformers.Gemma3Config(), transformers.Gemma3TextConfig())
|
||||
|
|
@ -338,8 +328,8 @@ def test_text_only_key_mapping_targets_published_prefixes():
|
|||
|
||||
|
||||
def test_gemma3_text_only_loads_real_language_weights_from_vlm_checkpoint(tmp_path):
|
||||
# Regression for PR #5816: text-only loading of a Gemma 3 VLM checkpoint must load the
|
||||
# real language weights, not random ones. Fails on tf >=5 without the key_mapping fix.
|
||||
# PR #5816: text-only loading of a Gemma 3 VLM checkpoint must load real
|
||||
# language weights, not random ones. Fails on tf >=5 without the key_mapping fix.
|
||||
transformers = pytest.importorskip("transformers")
|
||||
torch = pytest.importorskip("torch")
|
||||
import shutil
|
||||
|
|
@ -391,7 +381,7 @@ def test_gemma3_text_only_loads_real_language_weights_from_vlm_checkpoint(tmp_pa
|
|||
save_dir = tmp_path / "vlm"
|
||||
full_model.save_pretrained(save_dir, safe_serialization = True)
|
||||
|
||||
# tf >=5 saves under an outer "model." prefix; strip it to reproduce the real
|
||||
# tf >=5 saves under an outer "model." prefix; strip it to reproduce the
|
||||
# language_model.model.* layout the published Gemma 3 checkpoints use.
|
||||
real_dir = tmp_path / "real"
|
||||
real_dir.mkdir()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
"""FastSentenceTransformer constructor-redirect lifecycle:
|
||||
- AutoModel/AutoProcessor/AutoTokenizer.from_pretrained are restored even
|
||||
when the Transformer constructor raises (try/finally invariant).
|
||||
- The closure that decides whether to substitute the pre-loaded objects
|
||||
(`is_requested_model_name`) handles HF repo IDs, local paths, trailing
|
||||
slashes, pathlib.Path objects, and missing identifiers correctly.
|
||||
"""
|
||||
"""FastSentenceTransformer constructor-redirect lifecycle: Auto*.from_pretrained
|
||||
are restored even when the Transformer constructor raises (try/finally), and
|
||||
`is_requested_model_name` matches HF IDs, local paths, trailing slashes, Path
|
||||
objects, and missing identifiers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -18,8 +15,7 @@ import pytest
|
|||
|
||||
|
||||
def _stub_module(name: str) -> types.ModuleType:
|
||||
# __spec__ set so find_spec(name) doesn't raise if a later test imports
|
||||
# the real package.
|
||||
# __spec__ set so find_spec(name) doesn't raise if a later test imports the real package.
|
||||
mod = types.ModuleType(name)
|
||||
mod.__spec__ = importlib.util.spec_from_loader(name, loader = None)
|
||||
return mod
|
||||
|
|
@ -34,9 +30,8 @@ _STUB_KEYS = (
|
|||
|
||||
@pytest.fixture(autouse = True)
|
||||
def _restore_sys_modules():
|
||||
"""Snapshot the entries we shadow with stubs and restore them after each
|
||||
test so a downstream test that does `import transformers` for real does
|
||||
not pick up our non-package stub."""
|
||||
"""Snapshot the stubbed module entries and restore them after each test so a
|
||||
downstream real `import transformers` doesn't pick up our stub."""
|
||||
saved = {k: sys.modules.get(k) for k in _STUB_KEYS}
|
||||
try:
|
||||
yield
|
||||
|
|
|
|||
|
|
@ -10,11 +10,9 @@ from unittest import mock
|
|||
|
||||
import pytest
|
||||
|
||||
# Add the studio directory so we can import install_python_stack.
|
||||
STUDIO_DIR = Path(__file__).resolve().parents[2] / "studio"
|
||||
sys.path.insert(0, str(STUDIO_DIR))
|
||||
|
||||
# Import after path setup.
|
||||
import install_python_stack as ips
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,4 @@
|
|||
"""Tests for install_python_stack NO_TORCH / IS_MACOS filtering logic.
|
||||
|
||||
Covers:
|
||||
- _filter_requirements unit tests (synthetic + REAL requirements files)
|
||||
- NO_TORCH / IS_MACOS / IS_WINDOWS env var parsing
|
||||
- Subprocess-mock of install_python_stack() to verify overrides/triton/filtering
|
||||
actually happen (or get skipped) under each platform/config combination
|
||||
- VCS URL and environment marker edge cases in filtering
|
||||
"""
|
||||
"""Tests for install_python_stack NO_TORCH / IS_MACOS requirement filtering."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -21,7 +13,7 @@ from unittest import mock
|
|||
|
||||
import pytest
|
||||
|
||||
# Add the studio directory so we can import install_python_stack
|
||||
# Add the studio directory so install_python_stack is importable.
|
||||
STUDIO_DIR = Path(__file__).resolve().parents[2] / "studio"
|
||||
sys.path.insert(0, str(STUDIO_DIR))
|
||||
|
||||
|
|
@ -59,7 +51,6 @@ class TestFilterRequirements:
|
|||
)
|
||||
result = ips._filter_requirements(req, ips.NO_TORCH_SKIP_PACKAGES)
|
||||
lines = Path(result).read_text(encoding = "utf-8").splitlines()
|
||||
# Only numpy should remain (non-blank lines)
|
||||
non_blank = [l.strip() for l in lines if l.strip()]
|
||||
assert non_blank == ["numpy"], f"Expected only numpy, got: {non_blank}"
|
||||
|
||||
|
|
@ -80,7 +71,7 @@ class TestFilterRequirements:
|
|||
result = ips._filter_requirements(req, ips.NO_TORCH_SKIP_PACKAGES)
|
||||
lines = Path(result).read_text(encoding = "utf-8").splitlines()
|
||||
non_blank = [l.strip() for l in lines if l.strip()]
|
||||
# Comment starts with "#", not "torch-stoi", so it's preserved
|
||||
# Comment lines start with "#", so they are preserved.
|
||||
assert len(non_blank) == 2
|
||||
assert non_blank[0].startswith("#")
|
||||
assert non_blank[1] == "numpy"
|
||||
|
|
@ -139,7 +130,7 @@ class TestFilterRequirements:
|
|||
)
|
||||
result = ips._filter_requirements(req, ips.NO_TORCH_SKIP_PACKAGES)
|
||||
content = Path(result).read_text(encoding = "utf-8")
|
||||
# Blank lines should be preserved (not stripped)
|
||||
# Blank lines must be preserved.
|
||||
assert "\n\n" in content or content.count("\n") >= 3
|
||||
|
||||
def test_stacked_windows_and_no_torch_filters(self, tmp_path):
|
||||
|
|
@ -154,7 +145,7 @@ class TestFilterRequirements:
|
|||
numpy
|
||||
""",
|
||||
)
|
||||
# First filter Windows packages, then NO_TORCH packages
|
||||
# Filter Windows packages, then NO_TORCH packages.
|
||||
intermediate = ips._filter_requirements(req, ips.WINDOWS_SKIP_PACKAGES)
|
||||
result = ips._filter_requirements(Path(intermediate), ips.NO_TORCH_SKIP_PACKAGES)
|
||||
lines = Path(result).read_text(encoding = "utf-8").splitlines()
|
||||
|
|
@ -203,7 +194,7 @@ class TestFilterRequirements:
|
|||
result = ips._filter_requirements(req, ips.NO_TORCH_SKIP_PACKAGES)
|
||||
lines = Path(result).read_text(encoding = "utf-8").splitlines()
|
||||
non_blank = [l.strip() for l in lines if l.strip()]
|
||||
# The git+ URL doesn't start with any skip package, so it is preserved
|
||||
# git+ URL starts with no skip package, so it is preserved.
|
||||
assert len(non_blank) == 2, f"git+ URL should be preserved, got: {non_blank}"
|
||||
|
||||
|
||||
|
|
@ -231,13 +222,13 @@ class TestRealRequirementsFiltering:
|
|||
filtered = self._non_blank_non_comment(Path(result))
|
||||
original = self._non_blank_non_comment(EXTRAS_TXT)
|
||||
|
||||
# These must be gone
|
||||
# These must be gone.
|
||||
for pkg in ["torch-stoi", "timm", "openai-whisper", "transformers-cfg"]:
|
||||
assert not any(
|
||||
l.lower().startswith(pkg) for l in filtered
|
||||
), f"{pkg} should be removed from extras.txt"
|
||||
|
||||
# Everything else must remain
|
||||
# Everything else must remain.
|
||||
expected = [
|
||||
l
|
||||
for l in original
|
||||
|
|
@ -364,9 +355,7 @@ class TestIsMacosConstant:
|
|||
|
||||
|
||||
class TestInstallPythonStackSubprocessMock:
|
||||
"""Monkeypatch subprocess.run to capture all pip/uv commands,
|
||||
then verify which requirements files are used/skipped under
|
||||
different NO_TORCH / IS_MACOS / IS_WINDOWS configurations."""
|
||||
"""Mock subprocess.run to verify which req files are used/skipped per config."""
|
||||
|
||||
@pytest.fixture(autouse = True)
|
||||
def _check_req_files(self):
|
||||
|
|
@ -383,10 +372,7 @@ class TestInstallPythonStackSubprocessMock:
|
|||
*,
|
||||
skip_base: bool = True,
|
||||
):
|
||||
"""Run install_python_stack() with mocked subprocess, capturing all commands.
|
||||
|
||||
Returns a list of string-joined commands (each element is ' '.join(cmd)).
|
||||
"""
|
||||
"""Run install_python_stack() with mocked subprocess; return joined commands."""
|
||||
captured_cmds: list[list[str]] = []
|
||||
|
||||
def mock_run(cmd, **kw):
|
||||
|
|
@ -471,11 +457,7 @@ class TestInstallPythonStackSubprocessMock:
|
|||
# -- Normal Linux path (NO_TORCH=False, IS_MACOS=False, IS_WINDOWS=False) --
|
||||
|
||||
def test_normal_linux_includes_overrides(self):
|
||||
"""Normal Linux: the torchao override step IS called.
|
||||
|
||||
The override step installs a torch-matched torchao spec via
|
||||
--force-reinstall (uv: --reinstall), not overrides.txt directly.
|
||||
"""
|
||||
"""Normal Linux: torchao override step runs (via --reinstall, not overrides.txt)."""
|
||||
cmds = self._capture_install(no_torch = False, is_macos = False, is_windows = False)
|
||||
assert any(
|
||||
"--reinstall" in cmd for cmd in cmds
|
||||
|
|
@ -512,12 +494,7 @@ class TestInstallPythonStackSubprocessMock:
|
|||
), "triton-kernels.txt should be skipped on Windows even without NO_TORCH"
|
||||
|
||||
def test_windows_only_includes_overrides(self):
|
||||
"""Windows (without NO_TORCH): overrides IS called (via filtered temp file).
|
||||
|
||||
On Windows, all req files go through _filter_requirements(WINDOWS_SKIP_PACKAGES),
|
||||
so the command uses a temp file, not overrides.txt directly. We check for
|
||||
--reinstall (uv translation of --force-reinstall) which is unique to overrides.
|
||||
"""
|
||||
"""Windows (no NO_TORCH): overrides runs via filtered temp file (check --reinstall)."""
|
||||
cmds = self._capture_install(no_torch = False, is_macos = False, is_windows = True)
|
||||
assert any(
|
||||
"--reinstall" in cmd for cmd in cmds
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ def _load_orpo_rewriter(name = "orpo_trainer_text_tokenizer"):
|
|||
src = open(RL_PATH).read()
|
||||
tree = ast.parse(src)
|
||||
ns = {"re": re}
|
||||
# Materialise sibling module-level assignments (e.g. _PAD_FALLBACK) so
|
||||
# any rewriter that references them at exec-time can resolve them.
|
||||
# Materialise sibling module-level _-prefixed assignments the rewriter may reference.
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
|
||||
|
||||
"""Regression tests: _patch_trl_rl_trainers must never raise.
|
||||
|
||||
The wrapper in unsloth/models/rl.py ring-fences the impl so direct
|
||||
callers (CI shims, downstream tools) don't have to. Lock that
|
||||
contract here.
|
||||
"""
|
||||
"""Regression: _patch_trl_rl_trainers (the ring-fencing wrapper in
|
||||
unsloth/models/rl.py) must never raise."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -39,7 +35,7 @@ def test_patch_trl_rl_trainers_swallows_garbage_input():
|
|||
|
||||
|
||||
def test_impl_is_separately_exposed():
|
||||
# Power users can still call the impl directly for the raising path.
|
||||
# The impl stays directly callable for the raising path.
|
||||
_wrapper, impl = _import_helpers()
|
||||
assert callable(impl)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,4 @@
|
|||
"""End-to-end sandbox tests: Studio modules in isolated no-torch venvs.
|
||||
|
||||
Covers:
|
||||
- Python 3.12 and 3.13 venv creation (Intel Mac uses 3.12, Apple Silicon/Linux 3.13)
|
||||
- data_collators.py loads and dataclasses instantiate without torch
|
||||
- chat_templates.py top-level exec works with stubs for relative imports
|
||||
- Negative control: prepending 'import torch' fails in no-torch venv
|
||||
- Negative control: installing torchao (from overrides.txt) fails in no-torch venv
|
||||
- AST structural checks for top-level torch imports
|
||||
"""
|
||||
"""Sandbox tests: Studio dataset modules load/run in isolated no-torch venvs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -48,10 +39,7 @@ def _create_venv(venv_dir: Path, python_version: str) -> Path | None:
|
|||
|
||||
@pytest.fixture(params = ["3.12", "3.13"], scope = "module")
|
||||
def no_torch_venv(request, tmp_path_factory):
|
||||
"""Create a temporary venv at the requested Python version with no torch.
|
||||
|
||||
Parametrized for 3.12 (Intel Mac) and 3.13 (Apple Silicon / Linux).
|
||||
"""
|
||||
"""Temp no-torch venv, parametrized for 3.12 (Intel Mac) and 3.13 (Apple Silicon / Linux)."""
|
||||
if not _has_uv():
|
||||
pytest.skip("uv not available")
|
||||
|
||||
|
|
@ -353,7 +341,7 @@ class TestFormatConversionAST:
|
|||
and child.module
|
||||
and child.module.startswith("torch")
|
||||
):
|
||||
# This torch import must be inside a Try node
|
||||
# This torch import must be inside a Try node.
|
||||
found_in_try = False
|
||||
for try_node in ast.walk(node):
|
||||
if isinstance(try_node, ast.Try):
|
||||
|
|
@ -524,10 +512,7 @@ class TestNegativeControls:
|
|||
os.unlink(temp_file)
|
||||
|
||||
def test_torchao_install_fails_no_torch_venv(self, no_torch_venv):
|
||||
"""Installing torchao (from overrides.txt) fails in a no-torch venv.
|
||||
|
||||
This proves the overrides.txt skip is necessary for Intel Mac.
|
||||
"""
|
||||
"""torchao install fails in a no-torch venv: proves the overrides.txt skip is needed."""
|
||||
result = subprocess.run(
|
||||
[
|
||||
no_torch_venv,
|
||||
|
|
@ -541,10 +526,10 @@ class TestNegativeControls:
|
|||
timeout = 60,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
# torchao install/resolution failed as expected
|
||||
# torchao install/resolution failed as expected.
|
||||
pass
|
||||
else:
|
||||
# pip dry-run may not catch dependency issues; verify torch is missing
|
||||
# dry-run may miss dep issues; verify torch is absent instead.
|
||||
check = subprocess.run(
|
||||
[no_torch_venv, "-c", "import torch"],
|
||||
capture_output = True,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
"""Tests for two install fixes:
|
||||
1. tokenizers in no-torch-runtime.txt (prevents AutoConfig crash)
|
||||
2. TORCH_CONSTRAINT in install.sh (arm64 macOS + py313+ -> torch>=2.6)
|
||||
"""
|
||||
"""Install fixes: tokenizers in no-torch-runtime.txt, and TORCH_CONSTRAINT in install.sh."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -12,7 +9,7 @@ import textwrap
|
|||
|
||||
import pytest
|
||||
|
||||
# ── Locate source files relative to this test ──────────────────────────
|
||||
# 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"
|
||||
|
|
@ -33,9 +30,7 @@ def _lines(path: pathlib.Path) -> list[str]:
|
|||
]
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# Group 1 -- Structural checks (no network, instant)
|
||||
# ======================================================================
|
||||
class TestStructuralTokenizers:
|
||||
"""Verify tokenizers presence and ordering in no-torch-runtime.txt."""
|
||||
|
||||
|
|
@ -114,15 +109,11 @@ class TestStructuralInstallPs1Unchanged:
|
|||
assert '"torch>=2.4,<2.11.0"' in self._ps1
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# Group 2 -- Shell snippet tests (bash subprocess, mocked python)
|
||||
# ======================================================================
|
||||
class TestTorchConstraintShell:
|
||||
"""Test the TORCH_CONSTRAINT block using bash subprocesses with
|
||||
mocked python binaries that return controlled minor versions."""
|
||||
"""Test the TORCH_CONSTRAINT block via bash with mocked python minor versions."""
|
||||
|
||||
# The extracted snippet we test in isolation. We override OS, _ARCH,
|
||||
# SKIP_TORCH, and provide a mock python at $VENV_DIR/bin/python.
|
||||
# Snippet tested in isolation: override OS/_ARCH/SKIP_TORCH and a mock python.
|
||||
_SNIPPET_TEMPLATE = textwrap.dedent(r"""
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
|
@ -191,8 +182,6 @@ class TestTorchConstraintShell:
|
|||
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
||||
return result.stdout.strip()
|
||||
|
||||
# -- arm64 macOS tightening cases --
|
||||
|
||||
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"
|
||||
|
|
@ -201,8 +190,6 @@ class TestTorchConstraintShell:
|
|||
out = self._run(tmp_path, py_minor = 14, os_val = "macos", arch = "arm64")
|
||||
assert out == "torch>=2.6,<2.11.0"
|
||||
|
||||
# -- arm64 macOS default (older python) --
|
||||
|
||||
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"
|
||||
|
|
@ -211,8 +198,7 @@ class TestTorchConstraintShell:
|
|||
out = self._run(tmp_path, py_minor = 11, os_val = "macos", arch = "arm64")
|
||||
assert out == "torch>=2.4,<2.11.0"
|
||||
|
||||
# -- Linux (unaffected) --
|
||||
|
||||
# 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"
|
||||
|
|
@ -221,14 +207,12 @@ class TestTorchConstraintShell:
|
|||
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) --
|
||||
|
||||
# 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 bypass --
|
||||
|
||||
# SKIP_TORCH bypasses the tightening.
|
||||
def test_skip_torch_arm64_macos_py313_default(self, tmp_path):
|
||||
out = self._run(
|
||||
tmp_path,
|
||||
|
|
@ -239,16 +223,12 @@ class TestTorchConstraintShell:
|
|||
)
|
||||
assert out == "torch>=2.4,<2.11.0"
|
||||
|
||||
# -- WSL --
|
||||
|
||||
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"
|
||||
|
||||
# -- Edge cases --
|
||||
|
||||
def test_py_minor_0_fallback_default(self, tmp_path):
|
||||
"""If python query fails (returns 0), should stay at default."""
|
||||
"""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"
|
||||
|
||||
|
|
@ -261,10 +241,10 @@ class TestTorchConstraintShell:
|
|||
assert out == "torch>=2.6,<2.11.0"
|
||||
|
||||
def test_mock_uv_receives_correct_constraint(self, tmp_path):
|
||||
"""Verify a mock uv would receive the correct constraint string."""
|
||||
"""A mock uv receives the tightened constraint on py3.13 arm64 macOS."""
|
||||
venv = self._make_mock_python(tmp_path, minor = 13)
|
||||
|
||||
# Create a mock uv that logs its arguments
|
||||
# Mock uv logs its arguments.
|
||||
mock_uv = tmp_path / "mock_uv"
|
||||
log_file = tmp_path / "uv_log.txt"
|
||||
mock_uv.write_text(
|
||||
|
|
@ -354,13 +334,10 @@ class TestTorchConstraintShell:
|
|||
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:
|
||||
"""Creates real uv venvs to verify tokenizers + transformers work
|
||||
without torch installed."""
|
||||
"""Real uv venvs verify tokenizers + transformers work without torch installed."""
|
||||
|
||||
@staticmethod
|
||||
def _create_venv(tmp_path: pathlib.Path, name: str, py: str) -> pathlib.Path:
|
||||
|
|
@ -393,8 +370,7 @@ class TestE2ETokenizersFix:
|
|||
|
||||
@pytest.mark.parametrize("py_version", ["3.12", "3.13"])
|
||||
def test_autoconfig_works_with_no_torch_runtime(self, tmp_path, py_version):
|
||||
"""Install from no-torch-runtime.txt with --no-deps (matching the
|
||||
real install.sh path), then verify AutoConfig imports successfully."""
|
||||
"""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}"
|
||||
|
|
@ -423,8 +399,7 @@ class TestE2ETokenizersFix:
|
|||
assert result.returncode != 0, "torch should NOT be importable"
|
||||
|
||||
def test_negative_control_no_tokenizers(self, tmp_path):
|
||||
"""Without tokenizers, AutoConfig should fail. We create a copy of
|
||||
no-torch-runtime.txt with the tokenizers line removed."""
|
||||
"""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(
|
||||
|
|
@ -440,9 +415,7 @@ class TestE2ETokenizersFix:
|
|||
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."""
|
||||
|
||||
|
|
@ -457,14 +430,11 @@ class TestInstallShNoTorchIntegration:
|
|||
|
||||
def test_no_deps_invocation_for_fresh(self):
|
||||
"""Fresh install path should also use --no-deps -r."""
|
||||
# Count occurrences of the no-deps -r pattern
|
||||
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):
|
||||
"""When SKIP_TORCH=true, the _find_no_torch_runtime path should be used."""
|
||||
# We test this structurally: verify the SKIP_TORCH=true blocks contain
|
||||
# _find_no_torch_runtime calls
|
||||
"""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,
|
||||
|
|
@ -474,9 +444,7 @@ class TestInstallShNoTorchIntegration:
|
|||
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."""
|
||||
|
|
@ -511,8 +479,7 @@ class TestE2EFullNoTorchSandbox:
|
|||
)
|
||||
|
||||
def test_autoconfig_succeeds(self, tmp_path):
|
||||
"""The real bug fix: install with --no-deps (matching install.sh)
|
||||
and verify from transformers import AutoConfig works."""
|
||||
"""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}"
|
||||
|
|
@ -522,7 +489,7 @@ class TestE2EFullNoTorchSandbox:
|
|||
), f"AutoConfig failed:\nstdout: {result.stdout}\nstderr: {result.stderr}"
|
||||
|
||||
def test_torch_not_importable(self, tmp_path):
|
||||
"""With --no-deps (as install.sh uses), torch must not be pulled in."""
|
||||
"""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}"
|
||||
|
|
|
|||
|
|
@ -1,15 +1,6 @@
|
|||
# Copyright 2025-present the Unsloth AI Inc. team. All rights reserved.
|
||||
|
||||
"""
|
||||
Truth-table tests for `resolve_tool_policy` -- the pure resolver behind
|
||||
`unsloth run --enable-tools/--disable-tools`.
|
||||
|
||||
Covers:
|
||||
- 127.0.0.1 default-on, explicit on, explicit off
|
||||
- 0.0.0.0 default-off, explicit off
|
||||
- 0.0.0.0 + explicit on: confirm prompt unless --silent or --yes,
|
||||
abort on negative answer.
|
||||
"""
|
||||
"""Truth-table tests for `resolve_tool_policy` behind `unsloth run --enable-tools/--disable-tools`."""
|
||||
|
||||
import pytest
|
||||
import typer
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue