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
540 lines
19 KiB
Python
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
|
|
)
|