test: 按模块重写测试代码,删除旧平铺结构
按"测试规则"重写 tests/:一个模块一个目录、用例按数据→过程→验证三段书写、 不保留全局 conftest.py、测试过程只调用真实生产代码。 结构(73 个文件、30 个模块目录、477 用例): - tests/nodes/ 15 个模块目录(srt/whisper/ass/ffmpeg/frame_extract/vlm/ subtitle_ocr/llm/llm_filter/subtitle_cleanup/subtitle_correction/ proper_nouns/adaptive_pool/vad_profiler/echo); - tests/app/ 11 个模块目录(db/scheduler/batch/maintenance/registry/seed/ storage/config/logging/main/routers 三组 API); - tests/sdk/test_models、tests/web/test_crop、tests/shared(公共设施)。 测试数据随模块目录入库(tests/**/data/),删除根级 testdata/;.gitignore 的 data/ 改为 /data/,否则会连带忽略 tests/**/data/ 导致测试数据无法入库。 顺带发现并修复三个真实缺陷: - nodes/srt.py:相邻条目缺少空行时把下一条时间轴吞进正文(静默错位), 改为正文行遇时间戳行即报错; - src/wov_app/scheduler.py:_file_size 只捕获 OSError,含 \x00 的产物 URI 抛 ValueError 导致任务误判失败,改为同时捕获; - nodes/subtitle_correction.py:生产代码依赖测试包解析 SRT, 改用生产模块 nodes/srt.py。 真实模型/服务集成测试按外部状态跳过:新增 tests/shared/gpu_memory.py (运行时探测显存、CUDA OOM 转跳过)与 tests/shared/llm_service.py (无 Key / 余额 / 限流转跳过)。全量 477 passed。
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""真实 LLM 服务的可用性判定(供真实 LLM 集成测试共用)。
|
||||
|
||||
真实 LLM 集成测试有两个外部前提:配置了 `LLM_API_KEY`、且服务端可用。
|
||||
两者都属于**外部环境状态**(密钥、余额、配额、网络),不是被测代码的行为;
|
||||
按测试规则,这类外部状态缺失时应跳过而不是把缺陷计到代码头上。
|
||||
|
||||
本模块把该判定收敛到一处,避免各测试各自实现、语义漂移:
|
||||
|
||||
- `require_llm_credentials()`:无 Key 时直接跳过;
|
||||
- `skip_on_service_unavailable()`:把鉴权/余额/限流类 HTTP 错误(401/402/403/429)
|
||||
转成跳过(附带原因),其余错误(如 5xx、网络异常、返回结构错误)照常失败,
|
||||
以保证真实回归仍能被发现。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import urllib.error
|
||||
from contextlib import contextmanager
|
||||
|
||||
import pytest
|
||||
|
||||
# 视为"服务端不可用/账号不可用"的 HTTP 状态码:
|
||||
# 401 未授权、402 需要付费(余额/额度耗尽)、403 禁止访问、429 限流。
|
||||
_UNAVAILABLE_CODES = frozenset({401, 402, 403, 429})
|
||||
|
||||
|
||||
def require_llm_credentials() -> None:
|
||||
"""未配置 LLM_API_KEY 时跳过真实 LLM 集成测试。"""
|
||||
if not os.getenv("LLM_API_KEY"):
|
||||
pytest.skip("未配置 LLM_API_KEY,跳过真实 LLM 集成测试")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def skip_on_service_unavailable():
|
||||
"""执行真实 LLM 调用;鉴权/余额/限流类错误转为跳过,其余错误向上抛出。
|
||||
|
||||
这样既不会因账户余额或临时限流把测试套件判红(外部状态问题),
|
||||
也不会掩盖真正的回归(返回结构错误、代码异常仍会失败)。
|
||||
"""
|
||||
try:
|
||||
yield
|
||||
except urllib.error.HTTPError as exc:
|
||||
if exc.code in _UNAVAILABLE_CODES:
|
||||
pytest.skip(f"LLM 服务/账号当前不可用(HTTP {exc.code}),跳过真实集成测试")
|
||||
raise
|
||||
|
||||
|
||||
def probe_llm_or_skip(model: str | None = None) -> None:
|
||||
"""向真实 LLM 接口发一次最小请求,服务/账号不可用时跳过测试。
|
||||
|
||||
用途:被测函数(如 `nodes/subtitle_correction.correct_entry`)出于生产
|
||||
需要会把调用异常吞掉并返回空串,测试因此无法从返回值区分"服务不可用"
|
||||
与"模型没有泛化"。此探针在断言之前把外部状态问题显式暴露出来并跳过,
|
||||
使断言只针对真实的能力回归。
|
||||
|
||||
探测失败判定与 `skip_on_service_unavailable` 一致(401/402/403/429 跳过);
|
||||
其余错误(5xx、网络、返回结构异常)向上抛出,仍视为需要修复的问题。
|
||||
"""
|
||||
import json as _json
|
||||
import urllib.request
|
||||
|
||||
require_llm_credentials()
|
||||
api_base = os.getenv("LLM_API_BASE", "https://api.siliconflow.cn/v1/chat/completions")
|
||||
api_key = os.getenv("LLM_API_KEY", "")
|
||||
body = {
|
||||
"model": model or os.getenv("LLM_MODEL", "Qwen/Qwen3.5-35B-A3B"),
|
||||
"messages": [{"role": "user", "content": "ping"}],
|
||||
"max_tokens": 1,
|
||||
"enable_thinking": False,
|
||||
}
|
||||
request = urllib.request.Request(
|
||||
api_base,
|
||||
data=_json.dumps(body).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
|
||||
method="POST",
|
||||
)
|
||||
with skip_on_service_unavailable():
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
_json.loads(response.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError:
|
||||
raise
|
||||
Reference in New Issue
Block a user