Make test_studio_run.py e2e tests pytest-compatible
Add a hybrid session-scoped studio_server fixture in conftest.py that feeds base_url / api_key into the existing e2e test functions. Three invocation modes are now supported: 1. Script mode (unchanged) — python tests/test_studio_run.py 2. Pytest + external server — point at a running instance via UNSLOTH_E2E_BASE_URL / UNSLOTH_E2E_API_KEY env vars, no per-run GGUF load cost 3. Pytest + fixture-managed server — pytest drives _start_server / _kill_server itself via --unsloth-model / --unsloth-gguf-variant, CI-friendly The existing _start_server / _kill_server helpers and main() stay untouched so the script entry point keeps working exactly as before. Test function signatures are unchanged — the (base_url, api_key) parameters now resolve via the new fixtures when running under pytest.
This commit is contained in:
parent
70adc92f17
commit
3607d48969
2 changed files with 146 additions and 6 deletions
|
|
@ -3,14 +3,136 @@
|
|||
|
||||
"""
|
||||
Shared pytest configuration for the backend test suite.
|
||||
Ensures that the backend root is on sys.path so that
|
||||
`import utils.utils` (and similar flat imports) resolve correctly.
|
||||
|
||||
Responsibilities:
|
||||
1. Put the backend root on sys.path so `from models.inference import ...`
|
||||
(and similar flat imports) resolve in test modules — mirrors how the
|
||||
app itself is launched.
|
||||
2. Provide a hybrid ``studio_server`` session fixture for end-to-end tests
|
||||
(see ``test_studio_run.py``). The fixture supports two invocation modes:
|
||||
|
||||
a. **External server.** If ``UNSLOTH_E2E_BASE_URL`` is set, tests point
|
||||
at an already-running Studio instance. ``UNSLOTH_E2E_API_KEY`` must
|
||||
also be set. This is the fast-iteration mode: start the server once
|
||||
with ``unsloth studio run ...``, then run pytest against it many
|
||||
times with no per-run GGUF load cost.
|
||||
|
||||
b. **Fixture-managed server.** Otherwise, the fixture launches a fresh
|
||||
server via ``_start_server`` and tears it down at session end. This
|
||||
is the one-shot mode for CI or a clean-slate verification run.
|
||||
|
||||
The model / variant for mode (b) come from ``--unsloth-model`` /
|
||||
``--unsloth-gguf-variant`` pytest options, then ``UNSLOTH_E2E_MODEL`` /
|
||||
``UNSLOTH_E2E_VARIANT`` env vars, then the defaults in
|
||||
``test_studio_run.py``.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
# Add backend root to sys.path (mirrors how the app itself is launched)
|
||||
_backend_root = Path(__file__).resolve().parent.parent
|
||||
if str(_backend_root) not in sys.path:
|
||||
sys.path.insert(0, str(_backend_root))
|
||||
|
||||
|
||||
# ── Pytest CLI options ───────────────────────────────────────────────
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup(
|
||||
"unsloth-e2e",
|
||||
"Unsloth Studio end-to-end test options",
|
||||
)
|
||||
group.addoption(
|
||||
"--unsloth-model",
|
||||
action = "store",
|
||||
default = None,
|
||||
help = (
|
||||
"GGUF model id used when starting a server for e2e tests. "
|
||||
"Ignored if UNSLOTH_E2E_BASE_URL is set. Overrides "
|
||||
"UNSLOTH_E2E_MODEL env var. Defaults to test_studio_run.py's "
|
||||
"DEFAULT_MODEL."
|
||||
),
|
||||
)
|
||||
group.addoption(
|
||||
"--unsloth-gguf-variant",
|
||||
action = "store",
|
||||
default = None,
|
||||
help = (
|
||||
"GGUF variant used when starting a server for e2e tests. "
|
||||
"Ignored if UNSLOTH_E2E_BASE_URL is set. Overrides "
|
||||
"UNSLOTH_E2E_VARIANT env var. Defaults to test_studio_run.py's "
|
||||
"DEFAULT_VARIANT."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ── E2E server fixtures ──────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture(scope = "session")
|
||||
def studio_server(request):
|
||||
"""Yield ``(base_url, api_key)`` for e2e tests.
|
||||
|
||||
Resolution order:
|
||||
|
||||
1. If ``UNSLOTH_E2E_BASE_URL`` is set → point at that server,
|
||||
require ``UNSLOTH_E2E_API_KEY`` alongside (skip if missing).
|
||||
2. Otherwise → start a fresh ``unsloth studio run`` subprocess via
|
||||
the existing ``_start_server`` helper in ``test_studio_run.py``
|
||||
and tear it down on session teardown.
|
||||
|
||||
Session-scoped so the expensive GGUF load happens at most once per
|
||||
pytest invocation. Lazily instantiated — tests that don't request
|
||||
the fixture (e.g. the unit tests in ``test_anthropic_messages.py``
|
||||
or ``test_help_output``) do not trigger server startup.
|
||||
"""
|
||||
external_url = os.environ.get("UNSLOTH_E2E_BASE_URL")
|
||||
if external_url:
|
||||
api_key = os.environ.get("UNSLOTH_E2E_API_KEY")
|
||||
if not api_key:
|
||||
pytest.skip(
|
||||
"UNSLOTH_E2E_BASE_URL is set but UNSLOTH_E2E_API_KEY is "
|
||||
"missing — tests that require auth cannot run against an "
|
||||
"external server without it.",
|
||||
)
|
||||
yield external_url, api_key
|
||||
return
|
||||
|
||||
# Lazy import: pytest has already loaded test_studio_run into
|
||||
# sys.modules by the time any test requests this fixture, so this
|
||||
# is a cache hit, not a re-execution.
|
||||
import test_studio_run as _e2e
|
||||
|
||||
model = (
|
||||
request.config.getoption("--unsloth-model")
|
||||
or os.environ.get("UNSLOTH_E2E_MODEL")
|
||||
or _e2e.DEFAULT_MODEL
|
||||
)
|
||||
variant = (
|
||||
request.config.getoption("--unsloth-gguf-variant")
|
||||
or os.environ.get("UNSLOTH_E2E_VARIANT")
|
||||
or _e2e.DEFAULT_VARIANT
|
||||
)
|
||||
|
||||
proc, api_key = _e2e._start_server(model, variant)
|
||||
try:
|
||||
yield f"http://{_e2e.HOST}:{_e2e.PORT}", api_key
|
||||
finally:
|
||||
_e2e._kill_server(proc)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def base_url(studio_server):
|
||||
"""Base URL for the e2e Studio server (from ``studio_server``)."""
|
||||
return studio_server[0]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api_key(studio_server):
|
||||
"""API key for the e2e Studio server (from ``studio_server``)."""
|
||||
return studio_server[1]
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
"""
|
||||
End-to-end tests for ``unsloth studio run`` and API key authentication.
|
||||
|
||||
Starts a Studio server via the ``run`` subcommand, then exercises the
|
||||
usage examples shown on the API Keys page plus the Anthropic Messages API:
|
||||
Exercises the usage examples shown on the API Keys page plus the Anthropic
|
||||
Messages API:
|
||||
|
||||
1. curl -- basic chat completions (non-streaming)
|
||||
2. curl -- streaming chat completions
|
||||
|
|
@ -19,8 +19,26 @@ usage examples shown on the API Keys page plus the Anthropic Messages API:
|
|||
The test also validates the ``--help`` output and the server banner.
|
||||
|
||||
Usage:
|
||||
python test_studio_run.py # default model
|
||||
python test_studio_run.py --model unsloth/... # custom model
|
||||
|
||||
# Script mode — launches its own server via ``unsloth studio run``.
|
||||
python tests/test_studio_run.py
|
||||
python tests/test_studio_run.py --model unsloth/... --gguf-variant ...
|
||||
|
||||
# Pytest mode, external server — start a Studio server yourself,
|
||||
# then point pytest at it. Fastest iteration loop.
|
||||
unsloth studio run --model unsloth/Qwen3-1.7B-GGUF --gguf-variant UD-Q4_K_XL &
|
||||
export UNSLOTH_E2E_BASE_URL=http://127.0.0.1:8080
|
||||
export UNSLOTH_E2E_API_KEY=sk-unsloth-... # from the server banner
|
||||
pytest tests/test_studio_run.py -v
|
||||
|
||||
# Pytest mode, fixture-managed server — pytest launches and tears
|
||||
# down the server itself. One-shot verification, CI-friendly.
|
||||
pytest tests/test_studio_run.py -v \\
|
||||
--unsloth-model unsloth/Qwen3-1.7B-GGUF \\
|
||||
--unsloth-gguf-variant UD-Q4_K_XL
|
||||
|
||||
The ``base_url`` / ``api_key`` parameters on the test functions resolve
|
||||
via the ``studio_server`` session fixture in ``conftest.py``.
|
||||
|
||||
Requires a GPU and ~2 GB of disk for the GGUF download.
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue