Files
unslothai--unsloth/tests/test_offline_loading_helpers.py
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:59:56 +08:00

540 lines
19 KiB
Python

"""Unit tests for the offline-loading helpers in unsloth/models/loader_utils.py:
error classification, _force_hf_offline flip/restore, and the retry orchestrator.
Pure CPU, no network, no GPU."""
import os
import socket
import pytest
from unsloth.models import loader_utils as L
# ---------------------------------------------------------------------------
# _env_says_offline / _get_effective_local_files_only
# ---------------------------------------------------------------------------
_OFFLINE_TRUE = ("1", "true", "yes", "on", "ON", " 1 ", "\tyes\n")
_OFFLINE_FALSE = ("0", "no", "false", "off", "", " ", "maybe")
@pytest.mark.parametrize("value", _OFFLINE_TRUE)
def test_env_says_offline_truthy(monkeypatch, value):
monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising = False)
monkeypatch.setenv("HF_HUB_OFFLINE", value)
assert L._env_says_offline() is True
@pytest.mark.parametrize("value", _OFFLINE_FALSE)
def test_env_says_offline_falsy(monkeypatch, value):
monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising = False)
monkeypatch.setenv("HF_HUB_OFFLINE", value)
assert L._env_says_offline() is False
def test_env_says_offline_absent(monkeypatch):
monkeypatch.delenv("HF_HUB_OFFLINE", raising = False)
monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising = False)
assert L._env_says_offline() is False
def test_env_says_offline_transformers_var(monkeypatch):
monkeypatch.delenv("HF_HUB_OFFLINE", raising = False)
monkeypatch.setenv("TRANSFORMERS_OFFLINE", "1")
assert L._env_says_offline() is True
def test_effective_lfo_kwarg_wins(monkeypatch):
monkeypatch.delenv("HF_HUB_OFFLINE", raising = False)
monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising = False)
assert L._get_effective_local_files_only({"local_files_only": True}) is True
def test_effective_lfo_env_only(monkeypatch):
monkeypatch.setenv("HF_HUB_OFFLINE", "1")
assert L._get_effective_local_files_only({}) is True
def test_effective_lfo_neither(monkeypatch):
monkeypatch.delenv("HF_HUB_OFFLINE", raising = False)
monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising = False)
assert L._get_effective_local_files_only({"local_files_only": False}) is False
def test_effective_lfo_is_read_only():
# Must not pop local_files_only: the weight load reuses the same kwarg.
kwargs = {"local_files_only": True}
L._get_effective_local_files_only(kwargs)
assert kwargs == {"local_files_only": True}
# ---------------------------------------------------------------------------
# _is_offline_related_error
# ---------------------------------------------------------------------------
def _http_error(status):
import requests
resp = requests.Response()
resp.status_code = status
return requests.exceptions.HTTPError("http %s" % status, response = resp)
def test_none_is_not_offline():
assert L._is_offline_related_error(None) is False
def test_plain_connection_error_is_offline():
assert L._is_offline_related_error(ConnectionError("down")) is True
def test_timeout_error_is_offline():
assert L._is_offline_related_error(TimeoutError("slow")) is True
def test_plain_file_not_found_propagates():
assert L._is_offline_related_error(FileNotFoundError("config.json")) is False
def test_unrelated_error_is_not_offline():
assert L._is_offline_related_error(ValueError("bad arg")) is False
def test_requests_connection_error_is_offline():
import requests
assert L._is_offline_related_error(requests.exceptions.ConnectionError("x")) is True
@pytest.mark.parametrize("status", (500, 502, 503, 504))
def test_http_5xx_is_offline(status):
assert L._is_offline_related_error(_http_error(status)) is True
@pytest.mark.parametrize("status", (400, 401, 403, 404))
def test_http_4xx_propagates(status):
assert L._is_offline_related_error(_http_error(status)) is False
def test_status_less_http_with_network_wording_is_offline():
import requests
err = requests.exceptions.HTTPError("Couldn't connect to the server")
assert L._is_offline_related_error(err) is True
def test_status_less_http_without_network_wording_propagates():
import requests
err = requests.exceptions.HTTPError("I'm a teapot")
assert L._is_offline_related_error(err) is False
def test_gaierror_dns_failure_is_offline():
assert L._is_offline_related_error(socket.gaierror(-2, "Name or service not known")) is True
def test_gaierror_without_wording_is_offline_by_type():
# Matched by type, so a locale-specific / empty message still classifies offline.
assert L._is_offline_related_error(socket.gaierror(-2, "")) is True
def test_urllib_urlerror_is_offline():
import urllib.error
assert L._is_offline_related_error(urllib.error.URLError("connection failed")) is True
def test_urllib_httperror_404_propagates():
import urllib.error
err = urllib.error.HTTPError("http://x", 404, "Not Found", {}, None)
assert L._is_offline_related_error(err) is False
def test_urllib_httperror_503_is_offline():
import urllib.error
err = urllib.error.HTTPError("http://x", 503, "Service Unavailable", {}, None)
assert L._is_offline_related_error(err) is True
def test_ssl_error_is_not_offline():
# TLS/cert failure must surface, not silently fall back to cached files.
import ssl
assert L._is_offline_related_error(ssl.SSLError("certificate verify failed")) is False
def test_requests_ssl_error_is_not_offline():
# requests.SSLError subclasses ConnectionError, but is still a TLS failure -> not offline.
requests = pytest.importorskip("requests")
assert L._is_offline_related_error(requests.exceptions.SSLError("bad cert")) is False
def test_urlerror_wrapping_ssl_is_not_offline():
import ssl
import urllib.error
err = urllib.error.URLError(ssl.SSLCertVerificationError("self-signed certificate"))
assert L._is_offline_related_error(err) is False
def test_ssl_node_does_not_hide_deeper_connection_cause():
# Skipping a TLS node must not abort the walk: a genuine outage deeper still counts.
import ssl
outer = RuntimeError("load failed")
mid = ssl.SSLError("cert")
mid.__context__ = ConnectionError("down")
outer.__cause__ = mid
assert L._is_offline_related_error(outer) is True
def test_oserror_network_unreachable_is_offline():
assert L._is_offline_related_error(OSError("Network is unreachable")) is True
def test_offline_mode_is_enabled_is_offline():
errors = pytest.importorskip("huggingface_hub.errors")
assert L._is_offline_related_error(errors.OfflineModeIsEnabled("offline")) is True
def test_local_entry_not_found_is_offline():
# Both a FileNotFoundError and an HfHubHTTPError, but means "not cached + Hub down" -> offline.
errors = pytest.importorskip("huggingface_hub.errors")
assert L._is_offline_related_error(errors.LocalEntryNotFoundError("missing")) is True
def test_chained_cause_connection_error_is_offline():
err = RuntimeError("combined load failure")
err.__cause__ = ConnectionError("down")
assert L._is_offline_related_error(err) is True
def test_chained_context_connection_error_is_offline():
try:
try:
raise ConnectionError("down")
except ConnectionError:
raise RuntimeError("wrap")
except RuntimeError as e:
err = e
assert L._is_offline_related_error(err) is True
def test_chained_cause_404_still_propagates():
err = RuntimeError("combined load failure")
err.__cause__ = _http_error(404)
assert L._is_offline_related_error(err) is False
def test_cause_context_cycle_terminates():
a = RuntimeError("a")
b = RuntimeError("b")
a.__context__ = b
b.__context__ = a
# Must not hang; neither is network-related.
assert L._is_offline_related_error(a) is False
# ---------------------------------------------------------------------------
# _force_hf_offline
# ---------------------------------------------------------------------------
def _inprocess_offline_flags():
flags = []
try:
import huggingface_hub.constants as hfc
if hasattr(hfc, "HF_HUB_OFFLINE"):
flags.append(hfc.HF_HUB_OFFLINE)
except Exception:
pass
try:
import transformers.utils.hub as tuh
for attr in ("_is_offline_mode", "OFFLINE"):
if hasattr(tuh, attr):
flags.append(getattr(tuh, attr))
except Exception:
pass
return flags
def test_force_offline_sets_and_restores_absent_env(monkeypatch):
monkeypatch.delenv("HF_HUB_OFFLINE", raising = False)
monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising = False)
with L._force_hf_offline():
assert os.environ.get("HF_HUB_OFFLINE") == "1"
assert os.environ.get("TRANSFORMERS_OFFLINE") == "1"
# Absent before -> absent after (not left as "1").
assert os.environ.get("HF_HUB_OFFLINE") is None
assert os.environ.get("TRANSFORMERS_OFFLINE") is None
def test_force_offline_preserves_prior_env_value(monkeypatch):
monkeypatch.setenv("HF_HUB_OFFLINE", "0")
with L._force_hf_offline():
assert os.environ.get("HF_HUB_OFFLINE") == "1"
assert os.environ.get("HF_HUB_OFFLINE") == "0"
def test_force_offline_flips_inprocess_constants():
before = _inprocess_offline_flags()
with L._force_hf_offline():
during = _inprocess_offline_flags()
assert during, "expected at least one in-process offline flag to inspect"
assert all(flag is True for flag in during)
assert _inprocess_offline_flags() == before
def test_force_offline_nesting_shares_one_flip(monkeypatch):
monkeypatch.delenv("HF_HUB_OFFLINE", raising = False)
monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising = False)
with L._force_hf_offline():
with L._force_hf_offline():
assert os.environ.get("HF_HUB_OFFLINE") == "1"
# Inner exit must NOT restore while the outer window is still open.
assert os.environ.get("HF_HUB_OFFLINE") == "1"
assert os.environ.get("HF_HUB_OFFLINE") is None
def test_force_offline_restores_on_exception(monkeypatch):
monkeypatch.delenv("HF_HUB_OFFLINE", raising = False)
monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising = False)
with pytest.raises(RuntimeError):
with L._force_hf_offline():
assert os.environ.get("HF_HUB_OFFLINE") == "1"
raise RuntimeError("boom")
assert os.environ.get("HF_HUB_OFFLINE") is None
assert os.environ.get("TRANSFORMERS_OFFLINE") is None
def test_force_offline_depth_returns_to_zero():
assert L._force_offline_depth == 0
with L._force_hf_offline():
assert L._force_offline_depth == 1
assert L._force_offline_depth == 0
def test_reset_hf_sessions_is_safe():
# Best-effort no-op when the hub helper is missing; must never raise.
L._reset_hf_sessions()
# ---------------------------------------------------------------------------
# _has_local_tokenizer_files / _resolve_checkpoint_tokenizer_name
# ---------------------------------------------------------------------------
def _touch(path, name):
open(os.path.join(path, name), "w").close()
def test_has_local_tokenizer_json(tmp_path):
_touch(tmp_path, "tokenizer.json")
assert L._has_local_tokenizer_files(str(tmp_path)) is True
def test_has_local_tokenizer_model(tmp_path):
_touch(tmp_path, "tokenizer.model")
assert L._has_local_tokenizer_files(str(tmp_path)) is True
def test_has_local_tokenizer_bpe_needs_merges(tmp_path):
# vocab.json alone is not loadable BPE; it needs merges.txt.
_touch(tmp_path, "vocab.json")
assert L._has_local_tokenizer_files(str(tmp_path)) is False
_touch(tmp_path, "merges.txt")
assert L._has_local_tokenizer_files(str(tmp_path)) is True
def test_has_local_tokenizer_empty_dir(tmp_path):
assert L._has_local_tokenizer_files(str(tmp_path)) is False
def test_resolve_tokenizer_explicit_override_wins(tmp_path):
kwargs = {"tokenizer_name": "base/repo"}
assert L._resolve_checkpoint_tokenizer_name(str(tmp_path), kwargs) == "base/repo"
# tokenizer_name is always popped (it is passed explicitly downstream too).
assert "tokenizer_name" not in kwargs
def test_resolve_tokenizer_self_sufficient_dir(tmp_path):
_touch(tmp_path, "tokenizer_config.json")
_touch(tmp_path, "tokenizer.json")
kwargs = {}
assert L._resolve_checkpoint_tokenizer_name(str(tmp_path), kwargs) == str(tmp_path)
def test_resolve_tokenizer_config_without_files_falls_back(tmp_path):
# Has tokenizer_config.json but no loadable tokenizer file -> base repo.
_touch(tmp_path, "tokenizer_config.json")
assert L._resolve_checkpoint_tokenizer_name(str(tmp_path), {}) is None
def test_resolve_tokenizer_nonexistent_dir_falls_back():
assert L._resolve_checkpoint_tokenizer_name("/no/such/dir", {}) is None
# ---------------------------------------------------------------------------
# _offline_aware_load (the retry orchestrator)
# ---------------------------------------------------------------------------
def test_retry_once_on_offline_error_then_succeed(monkeypatch):
monkeypatch.delenv("HF_HUB_OFFLINE", raising = False)
monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising = False)
calls = []
@L._offline_aware_load
def fake(*args, **kwargs):
calls.append(dict(kwargs))
if len(calls) == 1:
raise ConnectionError("network down")
return "ok"
assert fake("model") == "ok"
assert len(calls) == 2
assert not calls[0].get("local_files_only")
assert calls[1].get("local_files_only") is True
assert L._force_offline_depth == 0
def test_no_retry_on_non_offline_error(monkeypatch):
monkeypatch.delenv("HF_HUB_OFFLINE", raising = False)
monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising = False)
calls = []
@L._offline_aware_load
def fake(*args, **kwargs):
calls.append(1)
raise ValueError("genuine bug, not a network issue")
with pytest.raises(ValueError):
fake("model")
assert len(calls) == 1
def test_no_retry_when_already_offline_via_kwarg(monkeypatch):
monkeypatch.delenv("HF_HUB_OFFLINE", raising = False)
monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising = False)
calls = []
@L._offline_aware_load
def fake(*args, **kwargs):
calls.append(dict(kwargs))
# Offline window is active for the single attempt.
assert os.environ.get("HF_HUB_OFFLINE") == "1"
return "ok"
assert fake("model", local_files_only = True) == "ok"
assert len(calls) == 1
assert L._force_offline_depth == 0
def test_offline_error_when_already_offline_propagates(monkeypatch):
# Already offline -> no online attempt to retry, so the error propagates once.
monkeypatch.setenv("HF_HUB_OFFLINE", "1")
calls = []
@L._offline_aware_load
def fake(*args, **kwargs):
calls.append(1)
raise ConnectionError("still down")
with pytest.raises(ConnectionError):
fake("model")
assert len(calls) == 1
assert L._force_offline_depth == 0
def test_kwargs_preserved_across_retry(monkeypatch):
# Callee popping config/tokenizer_name must not change what the retry sees:
# fn(*args, **kwargs) re-packs a fresh **kwargs per call.
monkeypatch.delenv("HF_HUB_OFFLINE", raising = False)
monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising = False)
seen = []
@L._offline_aware_load
def fake(model_name, **kwargs):
cfg = kwargs.pop("config", None)
tok = kwargs.pop("tokenizer_name", None)
seen.append((cfg, tok))
if len(seen) == 1:
raise ConnectionError("down")
return cfg, tok
assert fake("m", config = "CFG", tokenizer_name = "TOK") == ("CFG", "TOK")
assert seen == [("CFG", "TOK"), ("CFG", "TOK")]
def test_retry_runs_gc_collect_between_attempts(monkeypatch):
# The retry lives OUTSIDE the except so the failed attempt's traceback (a
# partial model) is freed by gc.collect() before the second load reallocates.
monkeypatch.delenv("HF_HUB_OFFLINE", raising = False)
monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising = False)
gc_calls = []
monkeypatch.setattr(L.gc, "collect", lambda *a, **k: gc_calls.append(1))
calls = []
@L._offline_aware_load
def fake(*args, **kwargs):
calls.append(1)
if len(calls) == 1:
raise ConnectionError("down")
# By the retry attempt, gc.collect() must already have fired.
assert gc_calls, "gc.collect must run before the offline retry"
return "ok"
gc_calls.clear()
assert fake("model") == "ok"
assert len(calls) == 2
assert len(gc_calls) == 1
# ---------------------------------------------------------------------------
# _force_hf_offline — constant restore (no stale offline pin)
# ---------------------------------------------------------------------------
def test_force_offline_restores_freshly_imported_constant(monkeypatch):
# If huggingface_hub.constants is first imported inside the window, the saved value must
# be the pre-window state, not the just-forced "1"; otherwise the process pins offline.
import sys
monkeypatch.delenv("HF_HUB_OFFLINE", raising = False)
monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising = False)
saved_mod = sys.modules.get("huggingface_hub.constants")
saved_val = getattr(saved_mod, "HF_HUB_OFFLINE", None) if saved_mod else None
try:
sys.modules.pop("huggingface_hub.constants", None) # simulate "not imported yet"
with L._force_hf_offline():
import huggingface_hub.constants as hfc_in
assert hfc_in.HF_HUB_OFFLINE is True # forced offline inside the window
import huggingface_hub.constants as hfc_after
assert hfc_after.HF_HUB_OFFLINE is False # restored, not pinned True
assert os.environ.get("HF_HUB_OFFLINE") is None
finally:
if saved_mod is not None:
sys.modules["huggingface_hub.constants"] = saved_mod
if saved_val is not None:
saved_mod.HF_HUB_OFFLINE = saved_val
# ---------------------------------------------------------------------------
# _resolve_checkpoint_tokenizer_name — VLM needs local processor files
# ---------------------------------------------------------------------------
def test_resolve_tokenizer_vlm_without_processor_falls_back(tmp_path):
# VLM checkpoint with tokenizer files but no processor config -> base repo (None), so its
# cached processor still loads instead of AutoProcessor failing on the local dir.
_touch(tmp_path, "tokenizer_config.json")
_touch(tmp_path, "tokenizer.json")
assert L._resolve_checkpoint_tokenizer_name(str(tmp_path), {}, require_processor = True) is None
def test_resolve_tokenizer_vlm_with_processor_uses_local_dir(tmp_path):
_touch(tmp_path, "tokenizer_config.json")
_touch(tmp_path, "tokenizer.json")
_touch(tmp_path, "preprocessor_config.json")
assert L._resolve_checkpoint_tokenizer_name(str(tmp_path), {}, require_processor = True) == str(
tmp_path
)