unsloth/tests/python/test_amd_extras_contract.py
LeoBorcherding df00cb05c1 Merge pre-commit.ci formatting into fix/main-extras-guard
# Conflicts:
#	tests/python/test_amd_extras_contract.py
2026-07-28 13:49:47 -05:00

102 lines
3.9 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
"""Contract tests for the amd / huggingfacenotorch extras.
security-audit.yml builds its hf-stack scan set by indexing
[huggingfacenotorch] straight out of pyproject.toml. When the pip release
branch shipped without that extra, four security jobs died on a bare
KeyError for three weeks and nobody noticed, because the failure looked
like a generic Python crash rather than a missing extra.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
import pytest
from packaging.specifiers import SpecifierSet
from packaging.version import Version
REPO_ROOT = Path(__file__).resolve().parents[2]
PYPROJECT = REPO_ROOT / "pyproject.toml"
SECURITY_AUDIT = REPO_ROOT / ".github" / "workflows" / "security-audit.yml"
# 4-bit decode is unreliable on ROCm before 0.50.0, the first PyPI release
# carrying the full path (bnb #1887, #1979, #2012).
BNB_MIN = Version("0.50.0")
def _extras() -> dict[str, list[str]]:
if sys.version_info >= (3, 11):
import tomllib
else:
tomllib = pytest.importorskip("tomli")
data = tomllib.loads(PYPROJECT.read_text(encoding = "utf-8"))
return data["project"]["optional-dependencies"]
def _project_name(spec: str) -> str:
"""Leading distribution name of a PEP 508 requirement, lowercased."""
return re.split(r"[<>=!~;\[\s@]", spec.strip(), maxsplit = 1)[0].strip().lower()
class TestExtrasExist:
"""Both extras must be present, on every branch."""
@pytest.mark.parametrize("name", ["huggingfacenotorch", "amd"])
def test_extra_present(self, name: str):
assert name in _extras(), f"pyproject.toml is missing the [{name}] extra"
def test_amd_pulls_the_no_torch_stack(self):
assert any(
s.replace(" ", "") == "unsloth[huggingfacenotorch]" for s in _extras()["amd"]
), "the amd extra must pull unsloth[huggingfacenotorch]"
class TestHuggingfaceNoTorchIsTorchFree:
"""The whole point of the extra is that it names no torch distribution."""
@pytest.mark.parametrize("banned", ["torch", "torchvision"])
def test_no_torch_distribution(self, banned: str):
named = [s for s in _extras()["huggingfacenotorch"] if _project_name(s) == banned]
assert not named, f"[huggingfacenotorch] must not name {banned}: {named}"
class TestAmdBitsandbytesFloor:
"""Keeps the pre-0.50.0 ROCm range out of the AMD install path."""
def test_every_marker_line_excludes_the_broken_range(self):
specs = [s for s in _extras()["amd"] if _project_name(s) == "bitsandbytes"]
assert specs, "the amd extra must pin bitsandbytes"
for spec in specs:
requirement = spec.split(";", 1)[0].strip()
allowed = SpecifierSet(requirement[len("bitsandbytes") :].strip())
assert not allowed.contains(
Version("0.49.2")
), f"{requirement} still admits bnb 0.49.2, which predates the ROCm 4-bit fixes"
assert allowed.contains(BNB_MIN), f"{requirement} excludes the fixed release {BNB_MIN}"
class TestSecurityAuditWorkflowStaysInSync:
"""Every extra the audit workflow indexes has to actually exist.
This is the generic form of the failure: the workflow reaches into
pyproject.toml by name, so any rename or omission takes out the scan
jobs rather than the branch that caused it.
"""
def test_indexed_extras_exist(self):
referenced = set(
re.findall(
r'optional-dependencies"\]\["([^"]+)"\]',
SECURITY_AUDIT.read_text(encoding = "utf-8"),
)
)
assert referenced, "expected security-audit.yml to index at least one extra"
missing = sorted(referenced - set(_extras()))
assert (
not missing
), f"security-audit.yml indexes extras that pyproject.toml lacks: {missing}"