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.
138 lines
4.9 KiB
Python
138 lines
4.9 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
|
|
|
|
"""
|
|
Shared pytest configuration for the backend test suite.
|
|
|
|
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]
|