Files
xiaoxia-saas/tests/unit/test_render_metrics.py
T
CI Bot f481bcd396 feat: 新增渲染引擎灰度观测指标模块
新增 render_metrics.py,提供 Prometheus 指标埋点:
- render_task_total: 任务总数(按引擎/任务类型/状态)
- render_task_duration_seconds: 耗时直方图(12个bucket,1s~2h)
- render_error_total: 错误统计(按引擎/任务类型/错误类型)
- render_stream_copy_total: stream_copy命中率(hit/miss/fallback)
- render_active_tasks: 活跃任务数(Gauge)

配套 9 个单元测试,覆盖成功/失败/超时/活跃数增减/stream_copy/错误分类等场景
2026-07-13 14:37:19 +08:00

196 lines
7.8 KiB
Python
Executable File

"""渲染引擎灰度观测指标单元测试。"""
from __future__ import annotations
import subprocess
import time
import pytest
from prometheus_client import REGISTRY, CollectorRegistry, Counter, Gauge, Histogram
@pytest.fixture(autouse=True)
def _isolate_registry(monkeypatch):
"""每个测试使用独立的 CollectorRegistry,避免互相影响。"""
from video_processing import render_metrics
test_registry = CollectorRegistry()
# 用测试 registry 重新创建所有指标
task_total = Counter(
"render_task_total",
"Total number of render tasks",
["engine", "task_type", "status"],
registry=test_registry,
)
task_duration = Histogram(
"render_task_duration_seconds",
"Render task duration in seconds",
["engine", "task_type"],
buckets=(1, 5, 10, 30, 60, 120, 300, 600, 900, 1800, 3600, 7200),
registry=test_registry,
)
error_total = Counter(
"render_error_total",
"Total number of render errors",
["engine", "task_type", "error_type"],
registry=test_registry,
)
stream_copy_total = Counter(
"render_stream_copy_total",
"Total number of stream copy attempts and results",
["result", "reason"],
registry=test_registry,
)
active_tasks = Gauge(
"render_active_tasks",
"Number of render tasks currently in progress",
["engine", "task_type"],
registry=test_registry,
)
# 替换模块中的指标对象
monkeypatch.setattr(render_metrics, "RENDER_TASK_TOTAL", task_total)
monkeypatch.setattr(render_metrics, "RENDER_TASK_DURATION_SECONDS", task_duration)
monkeypatch.setattr(render_metrics, "RENDER_ERROR_TOTAL", error_total)
monkeypatch.setattr(render_metrics, "RENDER_STREAM_COPY_TOTAL", stream_copy_total)
monkeypatch.setattr(render_metrics, "RENDER_ACTIVE_TASKS", active_tasks)
yield test_registry
def _counter_value(counter: Counter, **labels) -> float:
"""获取 Counter 指定标签的当前值。"""
return counter.labels(**labels)._value.get()
def _gauge_value(gauge: Gauge, **labels) -> float:
"""获取 Gauge 指定标签的当前值。"""
return gauge.labels(**labels)._value.get()
def _histogram_sum(histogram: Histogram, **labels) -> float:
"""获取 Histogram 指定标签的观测总和。"""
return histogram.labels(**labels)._sum.get()
def test_render_task_metrics_success():
"""成功任务应记录 success 状态 + 耗时 + 活跃数正确增减。"""
from video_processing.render_metrics import (
RENDER_ACTIVE_TASKS,
RENDER_TASK_DURATION_SECONDS,
RENDER_TASK_TOTAL,
render_task_metrics,
)
assert _counter_value(RENDER_TASK_TOTAL, engine="unified", task_type="compose_video", status="success") == 0
assert _gauge_value(RENDER_ACTIVE_TASKS, engine="unified", task_type="compose_video") == 0
with render_task_metrics(engine="unified", task_type="compose_video"):
assert _gauge_value(RENDER_ACTIVE_TASKS, engine="unified", task_type="compose_video") == 1
time.sleep(0.01)
assert _gauge_value(RENDER_ACTIVE_TASKS, engine="unified", task_type="compose_video") == 0
assert _counter_value(RENDER_TASK_TOTAL, engine="unified", task_type="compose_video", status="success") == 1
assert _counter_value(RENDER_TASK_TOTAL, engine="unified", task_type="compose_video", status="failure") == 0
assert _histogram_sum(RENDER_TASK_DURATION_SECONDS, engine="unified", task_type="compose_video") > 0
def test_render_task_metrics_failure():
"""失败任务应记录 failure 状态 + 对应错误类型。"""
from video_processing.render_metrics import (
RENDER_ERROR_TOTAL,
RENDER_TASK_TOTAL,
render_task_metrics,
)
with pytest.raises(ValueError, match="test error"):
with render_task_metrics(engine="legacy", task_type="edit_plan"):
raise ValueError("test error")
assert _counter_value(RENDER_TASK_TOTAL, engine="legacy", task_type="edit_plan", status="failure") == 1
assert _counter_value(RENDER_TASK_TOTAL, engine="legacy", task_type="edit_plan", status="success") == 0
assert _counter_value(RENDER_ERROR_TOTAL, engine="legacy", task_type="edit_plan", error_type="validation") == 1
def test_render_task_metrics_ffmpeg_error():
"""FFmpeg 执行失败应分类为 ffmpeg_error。"""
from video_processing.render_metrics import RENDER_ERROR_TOTAL, render_task_metrics
with pytest.raises(subprocess.CalledProcessError):
with render_task_metrics(engine="unified", task_type="generate_video"):
raise subprocess.CalledProcessError(returncode=1, cmd=["ffmpeg"])
assert _counter_value(RENDER_ERROR_TOTAL, engine="unified", task_type="generate_video", error_type="ffmpeg_error") == 1
def test_render_task_metrics_timeout():
"""超时异常应分类为 timeout。"""
from video_processing.render_metrics import RENDER_ERROR_TOTAL, render_task_metrics
with pytest.raises(subprocess.TimeoutExpired):
with render_task_metrics(engine="unified", task_type="compose_video"):
raise subprocess.TimeoutExpired(cmd=["ffmpeg"], timeout=10)
assert _counter_value(RENDER_ERROR_TOTAL, engine="unified", task_type="compose_video", error_type="timeout") == 1
def test_render_task_metrics_active_tasks_decrement_on_error():
"""任务异常退出时,活跃任务数也应正确递减。"""
from video_processing.render_metrics import RENDER_ACTIVE_TASKS, render_task_metrics
assert _gauge_value(RENDER_ACTIVE_TASKS, engine="legacy", task_type="compose_video") == 0
with pytest.raises(RuntimeError):
with render_task_metrics(engine="legacy", task_type="compose_video"):
raise RuntimeError("boom")
assert _gauge_value(RENDER_ACTIVE_TASKS, engine="legacy", task_type="compose_video") == 0
def test_record_stream_copy():
"""stream_copy 命中/跳过/回退应分别计数。"""
from video_processing.render_metrics import RENDER_STREAM_COPY_TOTAL, record_stream_copy
assert _counter_value(RENDER_STREAM_COPY_TOTAL, result="hit", reason="成功") == 0
assert _counter_value(RENDER_STREAM_COPY_TOTAL, result="miss", reason="编码不匹配") == 0
record_stream_copy("hit", "成功")
record_stream_copy("hit", "成功")
record_stream_copy("miss", "编码不匹配")
record_stream_copy("fallback", "ffmpeg失败")
assert _counter_value(RENDER_STREAM_COPY_TOTAL, result="hit", reason="成功") == 2
assert _counter_value(RENDER_STREAM_COPY_TOTAL, result="miss", reason="编码不匹配") == 1
assert _counter_value(RENDER_STREAM_COPY_TOTAL, result="fallback", reason="ffmpeg失败") == 1
def test_classify_error_unknown():
"""未知异常应分类为 unknown。"""
from video_processing.render_metrics import _classify_error
assert _classify_error(RuntimeError("something")) == "unknown"
def test_classify_error_io():
"""IO 异常应分类为 io_error。"""
from video_processing.render_metrics import _classify_error
assert _classify_error(OSError("disk full")) == "io_error"
def test_engine_and_task_type_labels():
"""不同 engine 和 task_type 应独立计数。"""
from video_processing.render_metrics import RENDER_TASK_TOTAL, render_task_metrics
with render_task_metrics(engine="unified", task_type="compose_video"):
pass
with render_task_metrics(engine="legacy", task_type="compose_video"):
pass
with render_task_metrics(engine="unified", task_type="edit_plan"):
pass
assert _counter_value(RENDER_TASK_TOTAL, engine="unified", task_type="compose_video", status="success") == 1
assert _counter_value(RENDER_TASK_TOTAL, engine="legacy", task_type="compose_video", status="success") == 1
assert _counter_value(RENDER_TASK_TOTAL, engine="unified", task_type="edit_plan", status="success") == 1