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
114 lines
3.8 KiB
Python
114 lines
3.8 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""Regression tests for unsloth_cli.commands.export: pin the CLI to the export_* 3-tuple contract (was unpacking 2, crashing every `unsloth export`) via a fake ExportBackend in sys.modules."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import types
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import typer
|
|
from typer.testing import CliRunner
|
|
|
|
|
|
class _FakeExportBackend:
|
|
"""Stand-in for ExportBackend: export_* return the 3-tuple, load_checkpoint stays a 2-tuple."""
|
|
|
|
def __init__(self) -> None:
|
|
self.loaded: str | None = None
|
|
|
|
def load_checkpoint(self, **kwargs):
|
|
self.loaded = kwargs.get("checkpoint_path")
|
|
return True, f"Loaded {self.loaded}"
|
|
|
|
def scan_checkpoints(self, **kwargs):
|
|
return []
|
|
|
|
def export_merged_model(self, **kwargs):
|
|
return True, "merged ok", str(Path(kwargs["save_directory"]).resolve())
|
|
|
|
def export_base_model(self, **kwargs):
|
|
return True, "base ok", str(Path(kwargs["save_directory"]).resolve())
|
|
|
|
def export_gguf(self, **kwargs):
|
|
return True, "gguf ok", str(Path(kwargs["save_directory"]).resolve())
|
|
|
|
def export_lora_adapter(self, **kwargs):
|
|
return True, "lora ok", str(Path(kwargs["save_directory"]).resolve())
|
|
|
|
|
|
def _install_fake_studio_backend(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Inject a fake studio.backend.core.export into sys.modules so the CLI's lazy import binds to it; parent packages stubbed to skip the structlog-dependent tree."""
|
|
for name in ("studio", "studio.backend", "studio.backend.core"):
|
|
monkeypatch.setitem(sys.modules, name, types.ModuleType(name))
|
|
|
|
fake_mod = types.ModuleType("studio.backend.core.export")
|
|
fake_mod.ExportBackend = _FakeExportBackend
|
|
monkeypatch.setitem(sys.modules, "studio.backend.core.export", fake_mod)
|
|
|
|
# Drop the cached CLI module so its deferred import re-resolves the fake.
|
|
monkeypatch.delitem(sys.modules, "unsloth_cli.commands.export", raising = False)
|
|
|
|
|
|
@pytest.fixture
|
|
def cli_app(monkeypatch: pytest.MonkeyPatch) -> typer.Typer:
|
|
"""Typer app wrapping unsloth_cli.commands.export.export."""
|
|
_install_fake_studio_backend(monkeypatch)
|
|
from unsloth_cli.commands import export as export_cmd
|
|
|
|
app = typer.Typer()
|
|
app.command("export")(export_cmd.export)
|
|
|
|
# Typer flattens a single-command app, making "export" look like a stray positional;
|
|
# a harmless second command keeps "export" a real subcommand.
|
|
@app.command("noop")
|
|
def _noop() -> None: # pragma: no cover - only exists to pin routing
|
|
pass
|
|
|
|
return app
|
|
|
|
|
|
@pytest.fixture
|
|
def runner() -> CliRunner:
|
|
return CliRunner()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"format_flag,quant_flag",
|
|
[
|
|
("merged-16bit", None),
|
|
("merged-4bit", None),
|
|
("gguf", "q4_k_m"),
|
|
("lora", None),
|
|
],
|
|
)
|
|
def test_cli_export_unpacks_three_tuple(
|
|
cli_app: typer.Typer,
|
|
runner: CliRunner,
|
|
tmp_path: Path,
|
|
format_flag: str,
|
|
quant_flag: str | None,
|
|
) -> None:
|
|
"""Each --format path unpacks the 3-tuple without ValueError (pre-fix: 'too many values to unpack (expected 2)')."""
|
|
ckpt = tmp_path / "ckpt"
|
|
ckpt.mkdir()
|
|
out = tmp_path / "out"
|
|
|
|
cli_args = ["export", str(ckpt), str(out), "--format", format_flag]
|
|
if quant_flag is not None:
|
|
cli_args += ["--quantization", quant_flag]
|
|
|
|
result = runner.invoke(cli_app, cli_args)
|
|
|
|
assert result.exit_code == 0, (
|
|
f"CLI exited with code {result.exit_code} for --format {format_flag}.\n"
|
|
f"Output:\n{result.output}\n"
|
|
f"Exception: {result.exception!r}"
|
|
)
|
|
# Fake backend's success message should reach stdout.
|
|
expected_prefix = format_flag.split("-")[0]
|
|
assert f"{expected_prefix} ok" in result.output
|