Files
unslothai--unsloth/studio/backend/tests/test_logging_middleware.py
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:59:56 +08:00

245 lines
7.7 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
import asyncio
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from starlette.staticfiles import StaticFiles
from loggers import handlers as hmod
from loggers.handlers import LoggingMiddleware
class _LogCapture:
def __init__(self):
self.events = []
def info(self, event, **kw):
self.events.append(("info", event, kw))
def error(self, event, **kw):
self.events.append(("error", event, kw))
@pytest.fixture
def logs(monkeypatch):
capture = _LogCapture()
monkeypatch.setattr(hmod, "logger", capture)
return capture
def _http_scope(path, method = "GET"):
return {"type": "http", "path": path, "method": method}
async def _noop_receive():
return {"type": "http.disconnect"}
def _run(coro):
return asyncio.run(coro)
def test_success_logs_status_and_forwards_chunks(logs):
async def app(scope, receive, send):
await send({"type": "http.response.start", "status": 206, "headers": []})
await send({"type": "http.response.body", "body": b"a", "more_body": True})
await send({"type": "http.response.body", "body": b"", "more_body": False})
seen = []
async def send(message):
seen.append(message)
_run(LoggingMiddleware(app)(_http_scope("/api/health"), _noop_receive, send))
assert [m["type"] for m in seen] == [
"http.response.start",
"http.response.body",
"http.response.body",
]
assert logs.events[0][1] == "request_completed"
assert logs.events[0][2]["status_code"] == 206
def test_excluded_asset_success_skips_log(logs):
async def app(scope, receive, send):
await send({"type": "http.response.start", "status": 200, "headers": []})
await send({"type": "http.response.body", "body": b"ok"})
async def send(message):
pass
for path in ("/assets/index.css", "/huggingface.svg", "/font.woff2"):
_run(LoggingMiddleware(app)(_http_scope(path), _noop_receive, send))
assert logs.events == []
def test_exception_logs_real_status_and_reraises(logs):
async def app(scope, receive, send):
await send({"type": "http.response.start", "status": 418, "headers": []})
raise RuntimeError("stream failed")
async def send(message):
pass
with pytest.raises(RuntimeError, match = "stream failed"):
_run(LoggingMiddleware(app)(_http_scope("/api/health"), _noop_receive, send))
assert logs.events[0][1] == "request_failed"
assert logs.events[0][2]["status_code"] == 418
assert logs.events[0][2]["error"] == "stream failed"
assert "process_time_ms" in logs.events[0][2]
def test_cancelled_error_propagates_without_error_log(logs):
async def app(scope, receive, send):
raise asyncio.CancelledError()
async def send(message):
pass
with pytest.raises(asyncio.CancelledError):
_run(LoggingMiddleware(app)(_http_scope("/api/health"), _noop_receive, send))
assert logs.events == []
def test_non_http_scope_passes_through(logs):
seen = []
async def app(scope, receive, send):
seen.append(scope["type"])
async def send(message):
pass
_run(LoggingMiddleware(app)({"type": "websocket", "path": "/ws"}, _noop_receive, send))
assert seen == ["websocket"]
assert logs.events == []
def test_duplicate_get_within_window_deduped(logs, monkeypatch):
monkeypatch.setattr(hmod, "_ACCESS_LOG_DEDUP_MS", 1000)
async def app(scope, receive, send):
await send({"type": "http.response.start", "status": 200, "headers": []})
await send({"type": "http.response.body", "body": b"ok"})
async def send(message):
pass
mw = LoggingMiddleware(app)
for _ in range(3):
_run(mw(_http_scope("/api/chat/projects"), _noop_receive, send))
# Only the first of the identical GET/200 burst is logged.
assert len(logs.events) == 1
assert logs.events[0][1] == "request_completed"
def test_mutations_and_errors_are_never_deduped(logs, monkeypatch):
monkeypatch.setattr(hmod, "_ACCESS_LOG_DEDUP_MS", 1000)
async def post_ok(scope, receive, send):
await send({"type": "http.response.start", "status": 200, "headers": []})
await send({"type": "http.response.body", "body": b"ok"})
async def get_404(scope, receive, send):
await send({"type": "http.response.start", "status": 404, "headers": []})
await send({"type": "http.response.body", "body": b""})
async def send(message):
pass
mw = LoggingMiddleware(post_ok)
for _ in range(2):
_run(mw(_http_scope("/api/chat/threads", method = "POST"), _noop_receive, send))
mw_404 = LoggingMiddleware(get_404)
for _ in range(2):
_run(mw_404(_http_scope("/api/models"), _noop_receive, send))
# 2 mutations + 2 errors all logged (dedup only touches GET/2xx).
assert len(logs.events) == 4
def test_quiet_poll_paths_use_longer_heartbeat_window(logs, monkeypatch):
# Burst dedup off, quiet-poll heartbeat on: only liveness paths collapse.
monkeypatch.setattr(hmod, "_ACCESS_LOG_DEDUP_MS", 0)
monkeypatch.setattr(hmod, "_QUIET_POLL_DEDUP_MS", 1000)
async def app(scope, receive, send):
await send({"type": "http.response.start", "status": 200, "headers": []})
await send({"type": "http.response.body", "body": b"ok"})
async def send(message):
pass
mw = LoggingMiddleware(app)
for _ in range(3):
_run(mw(_http_scope("/api/inference/monitor"), _noop_receive, send)) # quiet
for _ in range(3):
_run(mw(_http_scope("/api/chat/projects"), _noop_receive, send)) # normal
paths = [e[2]["path"] for e in logs.events]
assert paths.count("/api/inference/monitor") == 1 # collapsed to one heartbeat
assert paths.count("/api/chat/projects") == 3 # base dedup off -> all logged
def test_distinct_query_strings_are_not_deduped(logs, monkeypatch):
monkeypatch.setattr(hmod, "_ACCESS_LOG_DEDUP_MS", 1000)
async def app(scope, receive, send):
await send({"type": "http.response.start", "status": 200, "headers": []})
await send({"type": "http.response.body", "body": b"ok"})
async def send(message):
pass
def scope(query):
return {
"type": "http",
"path": "/api/models/browse-folders",
"method": "GET",
"query_string": query,
}
mw = LoggingMiddleware(app)
_run(mw(scope(b"path=/tmp/a"), _noop_receive, send))
_run(mw(scope(b"path=/tmp/b"), _noop_receive, send)) # distinct query -> logs
_run(mw(scope(b"path=/tmp/a"), _noop_receive, send)) # repeat of first -> deduped
# Two distinct query strings log; the immediate repeat of the first does not.
assert len(logs.events) == 2
def test_fastapi_static_asset_success_skips_log(tmp_path, logs):
assets_dir = tmp_path / "assets"
assets_dir.mkdir()
(assets_dir / "app.css").write_text("body { color: black; }", encoding = "utf-8")
app = FastAPI()
app.add_middleware(LoggingMiddleware)
@app.get("/api/health")
async def health():
return {"ok": True}
app.mount("/assets", StaticFiles(directory = assets_dir), name = "assets")
client = TestClient(app)
response = client.get("/api/health")
assert response.status_code == 200
assert logs.events[0][1] == "request_completed"
assert logs.events[0][2]["path"] == "/api/health"
log_count = len(logs.events)
response = client.get("/assets/app.css")
assert response.status_code == 200
assert response.text == "body { color: black; }"
assert len(logs.events) == log_count