1a57878f76
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 1m12s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m17s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 1m33s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 3m14s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
1. 未使用依赖清理:
- 从 requirements-base.txt 移除 cryptography 和 pyOpenSSL
2. pyflakes 警告清零 (apps/ + packages/ + tests/):
- 移除 17 处未使用的 import (F401)
- 修复 26 处未使用的局部变量 (F841):
* 有副作用的赋值转为裸调用
* 无副作用的赋值直接删除
- 修复 1 处未使用的异常变量 (F841)
- 修复 1 处空 except 块
3. 测试文件冗余清理:
- 删除 tests/integration/test_project_management.py (模块级 skip,测试不存在的模块)
- 删除 tests/integration/fixtures/duplication_routes_fixed.py (未被引用)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
601 lines
23 KiB
Python
601 lines
23 KiB
Python
"""
|
||
API 性能基线测试
|
||
|
||
为核心 API 接口添加性能基线测试,确保接口响应时间在合理范围内。
|
||
|
||
分类:
|
||
- 核心接口(core, 500ms):登录、获取当前用户、项目列表、素材列表、生成任务列表、订阅信息
|
||
- 普通接口(normal, 1000ms):创建项目、创建素材、模板列表、剪辑计划列表
|
||
- 重操作接口(heavy, 3000ms):获取上传签名、创建生成任务、去重上传
|
||
|
||
运行方式:
|
||
pytest tests/integration/test_api_performance.py -v
|
||
SKIP_PERF_TESTS=1 pytest tests/integration/test_api_performance.py -v # 跳过性能测试
|
||
pytest tests/integration/test_api_performance.py -m "not performance" # 同上
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import uuid
|
||
|
||
import pytest
|
||
from fastapi.testclient import TestClient
|
||
|
||
# 检测是否有可用的 PostgreSQL 数据库
|
||
_HAS_PG = False
|
||
try:
|
||
if os.environ.get("USE_IN_MEMORY_DB", "").lower() != "true":
|
||
import psycopg
|
||
|
||
conn = psycopg.connect(
|
||
os.environ.get(
|
||
"DATABASE_URL",
|
||
"postgresql+psycopg://postgres:postgres@localhost:5432/xiaoxia_saas",
|
||
).replace("postgresql+psycopg://", "postgresql://"),
|
||
connect_timeout=3,
|
||
)
|
||
conn.close()
|
||
_HAS_PG = True
|
||
except Exception:
|
||
pass
|
||
|
||
needs_pg = pytest.mark.skipif(not _HAS_PG, reason="Requires PostgreSQL database")
|
||
skip_perf = os.environ.get("SKIP_PERF_TESTS", "").lower() in ("1", "true", "yes")
|
||
|
||
from apps.api.main import app
|
||
|
||
client = TestClient(app)
|
||
|
||
|
||
# ── 辅助函数 ──────────────────────────────────────────────────────────────
|
||
|
||
|
||
def _register_and_login() -> tuple[str, str, str]:
|
||
"""
|
||
注册新用户并登录,返回 (access_token, user_id, project_id)。
|
||
用于需要鉴权的性能测试准备数据。
|
||
"""
|
||
unique = uuid.uuid4().hex[:8]
|
||
email = f"perf-{unique}@example.com"
|
||
username = f"perfuser-{unique}"
|
||
|
||
# 注册
|
||
reg_resp = client.post(
|
||
"/api/v1/auth/register",
|
||
json={
|
||
"email": email,
|
||
"password": "SecurePass123",
|
||
"username": username,
|
||
"display_name": "Perf Test User",
|
||
},
|
||
)
|
||
assert reg_resp.status_code in (200, 201), f"注册失败: {reg_resp.json()}"
|
||
|
||
# 登录
|
||
login_resp = client.post(
|
||
"/api/v1/auth/login",
|
||
json={"email": email, "password": "SecurePass123"},
|
||
)
|
||
assert login_resp.status_code == 200, f"登录失败: {login_resp.json()}"
|
||
data = login_resp.json()
|
||
token = data["access_token"]
|
||
user_id = data["user_id"]
|
||
|
||
# 创建一个项目(用于需要项目的接口)
|
||
proj_resp = client.post(
|
||
"/api/v1/projects",
|
||
headers={"Authorization": f"Bearer {token}"},
|
||
json={"name": f"perf-project-{unique}"},
|
||
)
|
||
assert proj_resp.status_code in (200, 201), f"创建项目失败: {proj_resp.json()}"
|
||
project_id = proj_resp.json()["id"]
|
||
|
||
return token, user_id, project_id
|
||
|
||
|
||
# ── Fixtures ──────────────────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def perf_test_user():
|
||
"""
|
||
模块级 fixture:为性能测试准备测试用户。
|
||
|
||
由于性能测试关注的是响应时间而非数据正确性,
|
||
使用同一个用户和同一份数据可以减少 setup 开销,
|
||
让性能测量更准确。
|
||
"""
|
||
if skip_perf:
|
||
pytest.skip("SKIP_PERF_TESTS=1,跳过性能测试")
|
||
if not _HAS_PG:
|
||
pytest.skip("Requires PostgreSQL database")
|
||
|
||
token, user_id, project_id = _register_and_login()
|
||
return {
|
||
"token": token,
|
||
"user_id": user_id,
|
||
"project_id": project_id,
|
||
"headers": {"Authorization": f"Bearer {token}"},
|
||
}
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
# 核心接口性能测试(阈值 500ms)
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
@pytest.mark.performance
|
||
@pytest.mark.perf_core
|
||
@needs_pg
|
||
class TestCoreApiPerformance:
|
||
"""
|
||
核心接口性能测试 —— 阈值 500ms
|
||
|
||
这些接口是用户高频使用的功能,必须保证快速响应。
|
||
"""
|
||
|
||
def test_login_performance(self, perf_assert):
|
||
"""POST /auth/login 登录接口性能"""
|
||
# 先注册一个用户
|
||
unique = uuid.uuid4().hex[:8]
|
||
email = f"perf-login-{unique}@example.com"
|
||
client.post(
|
||
"/api/v1/auth/register",
|
||
json={
|
||
"email": email,
|
||
"password": "SecurePass123",
|
||
"username": f"perflogin-{unique}",
|
||
"display_name": "Perf Login Test",
|
||
},
|
||
)
|
||
|
||
result = perf_assert.measure("core", "POST /auth/login")(
|
||
lambda: client.post(
|
||
"/api/v1/auth/login",
|
||
json={"email": email, "password": "SecurePass123"},
|
||
)
|
||
)
|
||
|
||
assert result.status_code == 200, f"登录接口返回状态码 {result.status_code},预期 200"
|
||
assert result.passed, (
|
||
f"登录接口性能不达标: 中位数 {result.median_ms:.1f}ms > 阈值 {result.threshold_ms}ms\n"
|
||
f" 详情: min={result.min_ms:.1f}ms, max={result.max_ms:.1f}ms, "
|
||
f"mean={result.mean_ms:.1f}ms"
|
||
)
|
||
|
||
def test_auth_me_performance(self, perf_test_user, perf_assert):
|
||
"""GET /auth/me 获取当前用户信息性能"""
|
||
headers = perf_test_user["headers"]
|
||
|
||
result = perf_assert.measure("core", "GET /auth/me")(lambda: client.get("/api/v1/auth/me", headers=headers))
|
||
|
||
assert result.status_code == 200, f"获取当前用户返回状态码 {result.status_code},预期 200"
|
||
assert result.passed, (
|
||
f"获取当前用户性能不达标: 中位数 {result.median_ms:.1f}ms > 阈值 {result.threshold_ms}ms\n"
|
||
f" 详情: min={result.min_ms:.1f}ms, max={result.max_ms:.1f}ms, "
|
||
f"mean={result.mean_ms:.1f}ms"
|
||
)
|
||
|
||
def test_projects_list_performance(self, perf_test_user, perf_assert):
|
||
"""GET /projects 项目列表性能"""
|
||
headers = perf_test_user["headers"]
|
||
|
||
result = perf_assert.measure("core", "GET /projects")(lambda: client.get("/api/v1/projects", headers=headers))
|
||
|
||
assert result.status_code == 200, f"项目列表返回状态码 {result.status_code},预期 200"
|
||
assert result.passed, (
|
||
f"项目列表性能不达标: 中位数 {result.median_ms:.1f}ms > 阈值 {result.threshold_ms}ms\n"
|
||
f" 详情: min={result.min_ms:.1f}ms, max={result.max_ms:.1f}ms, "
|
||
f"mean={result.mean_ms:.1f}ms"
|
||
)
|
||
|
||
def test_assets_list_performance(self, perf_test_user, perf_assert):
|
||
"""GET /assets 素材列表性能"""
|
||
headers = perf_test_user["headers"]
|
||
|
||
result = perf_assert.measure("core", "GET /assets")(lambda: client.get("/api/v1/assets", headers=headers))
|
||
|
||
assert result.status_code == 200, f"素材列表返回状态码 {result.status_code},预期 200"
|
||
assert result.passed, (
|
||
f"素材列表性能不达标: 中位数 {result.median_ms:.1f}ms > 阈值 {result.threshold_ms}ms\n"
|
||
f" 详情: min={result.min_ms:.1f}ms, max={result.max_ms:.1f}ms, "
|
||
f"mean={result.mean_ms:.1f}ms"
|
||
)
|
||
|
||
def test_generation_tasks_list_performance(self, perf_test_user, perf_assert):
|
||
"""GET /generation/tasks 生成任务列表性能"""
|
||
headers = perf_test_user["headers"]
|
||
|
||
result = perf_assert.measure("core", "GET /generation/tasks")(
|
||
lambda: client.get("/api/v1/generation/tasks", headers=headers)
|
||
)
|
||
|
||
assert result.status_code == 200, f"生成任务列表返回状态码 {result.status_code},预期 200"
|
||
assert result.passed, (
|
||
f"生成任务列表性能不达标: 中位数 {result.median_ms:.1f}ms > 阈值 {result.threshold_ms}ms\n"
|
||
f" 详情: min={result.min_ms:.1f}ms, max={result.max_ms:.1f}ms, "
|
||
f"mean={result.mean_ms:.1f}ms"
|
||
)
|
||
|
||
def test_subscription_current_performance(self, perf_test_user, perf_assert):
|
||
"""GET /subscription/current 订阅信息性能"""
|
||
headers = perf_test_user["headers"]
|
||
|
||
result = perf_assert.measure("core", "GET /subscription/current")(
|
||
lambda: client.get("/api/v1/subscription/current", headers=headers)
|
||
)
|
||
|
||
assert result.status_code == 200, f"订阅信息返回状态码 {result.status_code},预期 200"
|
||
assert result.passed, (
|
||
f"订阅信息性能不达标: 中位数 {result.median_ms:.1f}ms > 阈值 {result.threshold_ms}ms\n"
|
||
f" 详情: min={result.min_ms:.1f}ms, max={result.max_ms:.1f}ms, "
|
||
f"mean={result.mean_ms:.1f}ms"
|
||
)
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
# 普通接口性能测试(阈值 1000ms)
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
@pytest.mark.performance
|
||
@pytest.mark.perf_normal
|
||
@needs_pg
|
||
class TestNormalApiPerformance:
|
||
"""
|
||
普通接口性能测试 —— 阈值 1000ms
|
||
|
||
这些接口涉及写操作或较多业务逻辑,允许稍长的响应时间。
|
||
"""
|
||
|
||
def test_create_project_performance(self, perf_test_user, perf_assert):
|
||
"""POST /projects 创建项目性能"""
|
||
headers = perf_test_user["headers"]
|
||
counter = 0
|
||
|
||
def _create():
|
||
nonlocal counter
|
||
counter += 1
|
||
return client.post(
|
||
"/api/v1/projects",
|
||
headers=headers,
|
||
json={"name": f"perf-create-{uuid.uuid4().hex[:8]}"},
|
||
)
|
||
|
||
result = perf_assert.measure("normal", "POST /projects")(_create)
|
||
|
||
assert result.status_code in (200, 201), f"创建项目返回状态码 {result.status_code},预期 200/201"
|
||
assert result.passed, (
|
||
f"创建项目性能不达标: 中位数 {result.median_ms:.1f}ms > 阈值 {result.threshold_ms}ms\n"
|
||
f" 详情: min={result.min_ms:.1f}ms, max={result.max_ms:.1f}ms, "
|
||
f"mean={result.mean_ms:.1f}ms"
|
||
)
|
||
|
||
def test_templates_list_performance(self, perf_test_user, perf_assert):
|
||
"""GET /templates 模板列表性能"""
|
||
headers = perf_test_user["headers"]
|
||
|
||
result = perf_assert.measure("normal", "GET /templates")(
|
||
lambda: client.get("/api/v1/templates", headers=headers)
|
||
)
|
||
|
||
# 模板列表可能返回 200 或空列表,只要不是错误即可
|
||
assert result.status_code == 200, f"模板列表返回状态码 {result.status_code},预期 200"
|
||
assert result.passed, (
|
||
f"模板列表性能不达标: 中位数 {result.median_ms:.1f}ms > 阈值 {result.threshold_ms}ms\n"
|
||
f" 详情: min={result.min_ms:.1f}ms, max={result.max_ms:.1f}ms, "
|
||
f"mean={result.mean_ms:.1f}ms"
|
||
)
|
||
|
||
def test_edit_plans_list_performance(self, perf_test_user, perf_assert):
|
||
"""GET /edit-plans 剪辑计划列表性能"""
|
||
headers = perf_test_user["headers"]
|
||
|
||
result = perf_assert.measure("normal", "GET /edit-plans")(
|
||
lambda: client.get("/api/v1/edit-plans", headers=headers)
|
||
)
|
||
|
||
assert result.status_code == 200, f"剪辑计划列表返回状态码 {result.status_code},预期 200"
|
||
assert result.passed, (
|
||
f"剪辑计划列表性能不达标: 中位数 {result.median_ms:.1f}ms > 阈值 {result.threshold_ms}ms\n"
|
||
f" 详情: min={result.min_ms:.1f}ms, max={result.max_ms:.1f}ms, "
|
||
f"mean={result.mean_ms:.1f}ms"
|
||
)
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
# 重操作接口性能测试(阈值 3000ms)
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
@pytest.mark.performance
|
||
@pytest.mark.perf_heavy
|
||
@needs_pg
|
||
class TestHeavyApiPerformance:
|
||
"""
|
||
重操作接口性能测试 —— 阈值 3000ms
|
||
|
||
这些接口涉及外部服务调用(如 OSS)或复杂业务逻辑,
|
||
允许较长的响应时间,但仍需有上限。
|
||
"""
|
||
|
||
def test_upload_direct_prepare_performance(self, perf_test_user, perf_assert):
|
||
# OSS 未配置时跳过此测试
|
||
from app.config import settings
|
||
|
||
if not settings.OSS_ACCESS_KEY_ID or not settings.OSS_ACCESS_KEY_SECRET:
|
||
pytest.skip("OSS credentials not configured, skipping upload signature test")
|
||
|
||
"""POST /upload/direct/prepare 获取上传签名性能"""
|
||
headers = perf_test_user["headers"]
|
||
project_id = perf_test_user["project_id"]
|
||
|
||
# 获取素材库 ID
|
||
lib_resp = client.get("/api/v1/asset-libraries", headers=headers)
|
||
library_id = ""
|
||
if lib_resp.status_code == 200:
|
||
items = lib_resp.json().get("items", [])
|
||
if items:
|
||
library_id = items[0].get("id", "")
|
||
|
||
def _prepare_upload():
|
||
return client.post(
|
||
"/api/v1/upload/direct/prepare",
|
||
headers=headers,
|
||
json={
|
||
"filename": f"perf-test-{uuid.uuid4().hex[:8]}.mp4",
|
||
"file_size": 1024 * 1024, # 1MB
|
||
"mime_type": "video/mp4",
|
||
"project_id": project_id,
|
||
"library_id": library_id,
|
||
},
|
||
)
|
||
|
||
result = perf_assert.measure("heavy", "POST /upload/direct/prepare")(_prepare_upload)
|
||
|
||
# 上传签名接口可能因为 OSS 配置问题返回 503,这是预期的
|
||
# 只要不超时、不返回 500 即可
|
||
assert result.status_code in (
|
||
200,
|
||
201,
|
||
400,
|
||
503,
|
||
), f"获取上传签名返回状态码 {result.status_code},预期 200/201/400/503"
|
||
assert result.passed, (
|
||
f"获取上传签名性能不达标: 中位数 {result.median_ms:.1f}ms > 阈值 {result.threshold_ms}ms\n"
|
||
f" 详情: min={result.min_ms:.1f}ms, max={result.max_ms:.1f}ms, "
|
||
f"mean={result.mean_ms:.1f}ms"
|
||
)
|
||
|
||
def test_create_generation_task_performance(self, perf_test_user, perf_assert):
|
||
"""POST /generation/tasks 创建生成任务性能"""
|
||
headers = perf_test_user["headers"]
|
||
project_id = perf_test_user["project_id"]
|
||
|
||
# 获取素材库 ID
|
||
lib_resp = client.get("/api/v1/asset-libraries", headers=headers)
|
||
library_id = ""
|
||
if lib_resp.status_code == 200:
|
||
items = lib_resp.json().get("items", [])
|
||
if items:
|
||
library_id = items[0].get("id", "")
|
||
|
||
def _create_task():
|
||
return client.post(
|
||
"/api/v1/generation/tasks",
|
||
headers=headers,
|
||
json={
|
||
"project_id": project_id,
|
||
"asset_library_id": library_id,
|
||
"template_id": "",
|
||
"title_ids": [],
|
||
"voice_ids": [],
|
||
"asset_ids": [],
|
||
"strategy_id": "",
|
||
},
|
||
)
|
||
|
||
result = perf_assert.measure("heavy", "POST /generation/tasks")(_create_task)
|
||
|
||
# 创建生成任务可能因为缺少素材等返回 400,这是预期的
|
||
# 性能测试关注响应时间,不关注业务成功与否
|
||
assert result.status_code in (
|
||
200,
|
||
201,
|
||
400,
|
||
404,
|
||
), f"创建生成任务返回状态码 {result.status_code},预期 200/201/400/404"
|
||
assert result.passed, (
|
||
f"创建生成任务性能不达标: 中位数 {result.median_ms:.1f}ms > 阈值 {result.threshold_ms}ms\n"
|
||
f" 详情: min={result.min_ms:.1f}ms, max={result.max_ms:.1f}ms, "
|
||
f"mean={result.mean_ms:.1f}ms"
|
||
)
|
||
|
||
def test_duplication_upload_performance(self, perf_test_user, perf_assert):
|
||
"""POST /duplication/upload 去重上传性能"""
|
||
headers = perf_test_user["headers"]
|
||
|
||
# 准备一个小的测试文件(模拟视频文件)
|
||
test_content = b"fake video content for perf test" * 100
|
||
|
||
def _upload():
|
||
return client.post(
|
||
"/api/v1/duplication/upload",
|
||
headers=headers,
|
||
files={
|
||
"file": (
|
||
f"perf-dup-{uuid.uuid4().hex[:8]}.mp4",
|
||
test_content,
|
||
"video/mp4",
|
||
)
|
||
},
|
||
)
|
||
|
||
result = perf_assert.measure("heavy", "POST /duplication/upload")(_upload)
|
||
|
||
# 去重上传可能因为 OSS 配置问题返回 503,这是预期的
|
||
assert result.status_code in (
|
||
200,
|
||
201,
|
||
400,
|
||
503,
|
||
), f"去重上传返回状态码 {result.status_code},预期 200/201/400/503"
|
||
assert result.passed, (
|
||
f"去重上传性能不达标: 中位数 {result.median_ms:.1f}ms > 阈值 {result.threshold_ms}ms\n"
|
||
f" 详情: min={result.min_ms:.1f}ms, max={result.max_ms:.1f}ms, "
|
||
f"mean={result.mean_ms:.1f}ms"
|
||
)
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
# 性能测试汇总报告
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
@pytest.mark.performance
|
||
@needs_pg
|
||
def test_performance_summary(perf_test_user, perf_assert, capsys):
|
||
"""
|
||
汇总性能测试结果,输出完整报告。
|
||
|
||
这个测试会重新跑一遍所有接口的性能测试,
|
||
并在最后输出汇总报告,方便在 CI 中查看。
|
||
"""
|
||
headers = perf_test_user["headers"]
|
||
project_id = perf_test_user["project_id"]
|
||
|
||
# 获取素材库 ID
|
||
lib_resp = client.get("/api/v1/asset-libraries", headers=headers)
|
||
library_id = ""
|
||
if lib_resp.status_code == 200:
|
||
items = lib_resp.json().get("items", [])
|
||
if items:
|
||
library_id = items[0].get("id", "")
|
||
|
||
# ── 核心接口 ──
|
||
# 登录(需要新用户)
|
||
unique = uuid.uuid4().hex[:8]
|
||
email = f"perf-summary-{unique}@example.com"
|
||
client.post(
|
||
"/api/v1/auth/register",
|
||
json={
|
||
"email": email,
|
||
"password": "SecurePass123",
|
||
"username": f"perfsummary-{unique}",
|
||
"display_name": "Perf Summary Test",
|
||
},
|
||
)
|
||
|
||
perf_assert.measure("core", "POST /auth/login")(
|
||
lambda: client.post(
|
||
"/api/v1/auth/login",
|
||
json={"email": email, "password": "SecurePass123"},
|
||
)
|
||
)
|
||
|
||
perf_assert.measure("core", "GET /auth/me")(lambda: client.get("/api/v1/auth/me", headers=headers))
|
||
|
||
perf_assert.measure("core", "GET /projects")(lambda: client.get("/api/v1/projects", headers=headers))
|
||
|
||
perf_assert.measure("core", "GET /assets")(lambda: client.get("/api/v1/assets", headers=headers))
|
||
|
||
perf_assert.measure("core", "GET /generation/tasks")(
|
||
lambda: client.get("/api/v1/generation/tasks", headers=headers)
|
||
)
|
||
|
||
perf_assert.measure("core", "GET /subscription/current")(
|
||
lambda: client.get("/api/v1/subscription/current", headers=headers)
|
||
)
|
||
|
||
# ── 普通接口 ──
|
||
perf_assert.measure("normal", "POST /projects")(
|
||
lambda: client.post(
|
||
"/api/v1/projects",
|
||
headers=headers,
|
||
json={"name": f"perf-summary-{uuid.uuid4().hex[:6]}"},
|
||
)
|
||
)
|
||
|
||
perf_assert.measure("normal", "GET /templates")(lambda: client.get("/api/v1/templates", headers=headers))
|
||
|
||
perf_assert.measure("normal", "GET /edit-plans")(lambda: client.get("/api/v1/edit-plans", headers=headers))
|
||
|
||
# ── 重操作接口 ──
|
||
perf_assert.measure("heavy", "POST /upload/direct/prepare")(
|
||
lambda: client.post(
|
||
"/api/v1/upload/direct/prepare",
|
||
headers=headers,
|
||
json={
|
||
"filename": f"perf-summary-{uuid.uuid4().hex[:6]}.mp4",
|
||
"file_size": 1024 * 1024,
|
||
"mime_type": "video/mp4",
|
||
"project_id": project_id,
|
||
"library_id": library_id,
|
||
},
|
||
)
|
||
)
|
||
|
||
perf_assert.measure("heavy", "POST /generation/tasks")(
|
||
lambda: client.post(
|
||
"/api/v1/generation/tasks",
|
||
headers=headers,
|
||
json={
|
||
"project_id": project_id,
|
||
"asset_library_id": library_id,
|
||
"template_id": "",
|
||
"title_ids": [],
|
||
"voice_ids": [],
|
||
"asset_ids": [],
|
||
"strategy_id": "",
|
||
},
|
||
)
|
||
)
|
||
|
||
test_content = b"fake video for summary perf test" * 100
|
||
perf_assert.measure("heavy", "POST /duplication/upload")(
|
||
lambda: client.post(
|
||
"/api/v1/duplication/upload",
|
||
headers=headers,
|
||
files={
|
||
"file": (
|
||
f"perf-sum-{uuid.uuid4().hex[:6]}.mp4",
|
||
test_content,
|
||
"video/mp4",
|
||
)
|
||
},
|
||
)
|
||
)
|
||
|
||
# 输出报告
|
||
report = perf_assert.report()
|
||
with capsys.disabled():
|
||
print("\n" + report)
|
||
|
||
# 汇总断言(警告模式:不阻塞,但输出失败信息)
|
||
# 在 CI 中通过 continue-on-error 控制是否阻塞
|
||
passed_count = sum(1 for r in perf_assert.results if r.passed)
|
||
total_count = len(perf_assert.results)
|
||
|
||
# 输出统计信息,方便 CI 解析
|
||
with capsys.disabled():
|
||
print(f"\nPERF_STATS: total={total_count}, passed={passed_count}, " f"failed={total_count - passed_count}")
|
||
for r in perf_assert.results:
|
||
status = "PASS" if r.passed else "FAIL"
|
||
print(
|
||
f"PERF_RESULT: {status} | {r.name} | "
|
||
f"median={r.median_ms:.1f}ms | threshold={r.threshold_ms}ms | "
|
||
f"min={r.min_ms:.1f}ms | max={r.max_ms:.1f}ms | "
|
||
f"mean={r.mean_ms:.1f}ms | status_code={r.status_code}"
|
||
)
|
||
|
||
# 这里使用宽松断言:只要超过一半通过就不报错
|
||
# 具体的 CI 阻塞策略由 CI 配置控制(continue-on-error)
|
||
assert passed_count >= total_count // 2, (
|
||
f"性能测试通过率过低: {passed_count}/{total_count} " f"({passed_count/total_count*100:.0f}%),至少需要 50% 通过"
|
||
)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
pytest.main([__file__, "-v", "--tb=short"])
|