Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/consolidated-tests-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,13 @@ jobs:
set -euxo pipefail
python -m pip install --upgrade pip
pip install --index-url https://download.pytorch.org/whl/cpu \
"torch>=2.4.0,<2.11.0"
"torch>=2.4.0,<2.11.0" "torchvision<0.26"
# torchvision: transitive import of transformers.models.qwen2_vl
# / qwen2_5_vl image processors. The Qwen2_VL image-processor
# zoo references chains through `from torchvision...` at module
# top, so a missing torchvision turns the existence-probe drift
# tests RED on "ModuleNotFoundError: No module named 'torchvision'".
# CPU build is plenty; we don't need the CUDA variant.
pip install -e .[core]
pip install --no-deps "unsloth @ git+https://github.com/unslothai/unsloth@main" || true
# Override with matrix-resolved specs.
Expand Down
20 changes: 16 additions & 4 deletions tests/security/conftest.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
"""Shared fixtures for the security regression suite.

The scanner scripts under audit are designed to be offline-safe. Pin
that invariant by autouse-installing a session-scoped network blocker
that invariant by autouse-installing a function-scoped network blocker
that refuses any non-loopback `socket.connect()` from inside the test
process. If a future test (or a scanner regression) accidentally tries
to reach the public internet, pytest fails loudly instead of leaking
the request.

Scope is intentionally ``function`` rather than ``session``: the swap
mutates a module-global (``socket.socket``), and a session-scoped swap
keeps the patch live for every test pytest runs after the first
security test in the same session -- which silently broke every
network-using test elsewhere in the tree (e.g.
``tests/test_upstream_pinned_symbols_transformers.py`` which fetches
HF modeling source over HTTPS). Per-function setup/teardown costs
~10us and contains the blast radius to security tests only.
"""

from __future__ import annotations
Expand Down Expand Up @@ -68,12 +77,15 @@ def connect_ex(self, address): # type: ignore[override]
return super().connect_ex(address)


@pytest.fixture(scope = "session", autouse = True)
@pytest.fixture(scope = "function", autouse = True)
def network_blocker():
"""Session-scoped fixture; replaces `socket.socket` with a blocker.
"""Function-scoped fixture; replaces `socket.socket` with a blocker.

Yields nothing; the swap is the side effect. Restored at teardown
so other test sessions (run interleaved) see a clean module.
so the *next* test (security or otherwise) sees the real socket.
Session scope was a footgun: it leaked the patch into every
network-using test in the parent ``tests/`` tree once a single
security test ran. See the module docstring for the regression.
"""
original = socket.socket
socket.socket = _BlockedSocket # type: ignore[assignment]
Expand Down
20 changes: 18 additions & 2 deletions tests/test_compiler_rewriter_exhaustive.py
Original file line number Diff line number Diff line change
Expand Up @@ -2033,11 +2033,23 @@ def test_unsloth_rl_peft_pattern_27_marker():

def test_unsloth_trainer_exec_marker():
"""``unsloth/trainer.py:614`` exec()'s synthesized trainer source;
pin that unsloth.trainer is importable."""
pytest.importorskip("unsloth")
pin that unsloth.trainer is importable.

Skips on a host without a real accelerator: ``import unsloth`` raises
``NotImplementedError("Unsloth cannot find any torch accelerator")``
at top-level on a CPU-only CI runner, which is neither an ImportError
nor a drift signal -- it's just the harness gate. ``importorskip``
only converts ``ImportError`` to ``skip``, so we have to wrap the
whole import path. Treat the no-accelerator case as skip so the
no-GPU CI cell goes green; the GPU cell still exercises the import
end-to-end.
"""
try:
import unsloth # noqa: F401
import unsloth.trainer as trainer_mod
except ImportError as e:
if e.name == "unsloth":
pytest.skip(f"unsloth is not installed: {e}")
_drift(
"unsloth/trainer.py:614",
"import unsloth.trainer",
Expand All @@ -2046,6 +2058,10 @@ def test_unsloth_trainer_exec_marker():
"unreachable.",
)
return
except NotImplementedError as e:
if "accelerator" in str(e) or "GPU" in str(e):
pytest.skip(f"No accelerator visible to unsloth import: {e}")
Comment on lines +2061 to +2063

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move no-accelerator catch around importorskip

In the CPU-only CI case described here, import unsloth is executed by pytest.importorskip("unsloth") before this try block, and pytest only converts import failures into skips. When unsloth is installed but raises NotImplementedError because no accelerator is visible, the exception escapes before this new handler runs, so the test still fails instead of skipping; wrap the importorskip call or perform the initial import inside this handler.

Useful? React with 👍 / 👎.

raise
# Module must expose some Trainer-family symbol downstream rewriter consumes.
if not any(
hasattr(trainer_mod, sym)
Expand Down
33 changes: 27 additions & 6 deletions tests/test_upstream_pinned_symbols_accelerator.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,17 +215,27 @@ def test_moe_expert_merges_call_active_merge_device():
# ---------------------------------------------------------------------------

def test_mlx_trainer_uses_modern_memory_apis_only():
"""unsloth_zoo.mlx_trainer must call the non-namespaced memory APIs
"""unsloth_zoo.mlx.trainer must call the non-namespaced memory APIs
(mx.set_memory_limit, mx.set_cache_limit, mx.set_wired_limit). The
namespaced mx.metal.set_* forms are deprecated upstream and reverting
to them resurrects the per-run deprecation warning that 70b93ad fixed.
"""
import importlib.util
import pathlib

mlx_trainer_path = pathlib.Path(
pkg_root = pathlib.Path(
importlib.util.find_spec("unsloth_zoo").submodule_search_locations[0]
) / "mlx_trainer.py"
)
# The MLX path was promoted from a flat module (mlx_trainer.py) to a
# subpackage (mlx/trainer.py) in e6d8f7f. Accept either layout so the
# test survives the rename.
candidates = [pkg_root / "mlx" / "trainer.py", pkg_root / "mlx_trainer.py"]
mlx_trainer_path = next((c for c in candidates if c.is_file()), None)
assert mlx_trainer_path is not None, (
f"Neither {candidates[0]} nor {candidates[1]} exists; the MLX "
f"trainer module was relocated again. Update this test's path "
f"candidates."
)
src = mlx_trainer_path.read_text()

# The deprecated forms must NOT appear.
Expand All @@ -240,7 +250,7 @@ def test_mlx_trainer_uses_modern_memory_apis_only():

# The modern forms must appear.
for modern in ("mx.set_memory_limit", "mx.set_cache_limit", "mx.set_wired_limit"):
assert modern in src, f"Expected modern API {modern} missing from mlx_trainer.py"
assert modern in src, f"Expected modern API {modern} missing from {mlx_trainer_path.name}"


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -327,10 +337,21 @@ def test_get_existing_mlx_quantization_detects_both_keys():
"""
# Import the helper without triggering the heavy mlx_loader import
# chain on the GPU-free harness. We pull the function directly.
# Layout was promoted from mlx_loader.py (flat) to mlx/loader.py
# (subpackage) in e6d8f7f. Try both so the test survives the rename.
import importlib.util
import pathlib
pkg_loc = importlib.util.find_spec("unsloth_zoo").submodule_search_locations[0]
src = (pathlib.Path(pkg_loc) / "mlx_loader.py").read_text()
pkg_loc = pathlib.Path(
importlib.util.find_spec("unsloth_zoo").submodule_search_locations[0]
)
candidates = [pkg_loc / "mlx" / "loader.py", pkg_loc / "mlx_loader.py"]
loader_path = next((c for c in candidates if c.is_file()), None)
assert loader_path is not None, (
f"Neither {candidates[0]} nor {candidates[1]} exists; the MLX "
f"loader module was relocated again. Update this test's path "
f"candidates."
)
src = loader_path.read_text()

# The function must check BOTH key names; otherwise repos saved by
# mlx-lm (key "quantization") OR by HF transformers ("quantization_config")
Expand Down
Loading