debb3a5d67
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 10s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 42s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (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 / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 56s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 32s
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
349 lines
13 KiB
Python
349 lines
13 KiB
Python
"""Tests for FFmpeg encoding optimization (#1758).
|
||
|
||
验证:
|
||
1. 集中编码常量正确定义,支持环境变量覆盖
|
||
2. 所有渲染路径(_execute_ffmpeg / _render_pass_through / normalize_video / processor)
|
||
使用统一的编码参数
|
||
3. preset 从 medium → fast,确保渲染速度提升
|
||
4. threads=0 自动检测 CPU 核心数
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import importlib
|
||
import os
|
||
import sys
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
import pytest
|
||
|
||
# ── 常量定义与默认值 ─────────────────────────────────────────────────────────
|
||
|
||
|
||
class TestEncodingConstants:
|
||
"""测试集中编码常量的定义与默认值."""
|
||
|
||
def test_default_preset_is_fast(self):
|
||
"""默认 preset 应为 fast(非 medium),确保速度提升."""
|
||
from shared.ffmpeg_utils import FFMPEG_ENCODE_PRESET
|
||
|
||
assert FFMPEG_ENCODE_PRESET == "fast"
|
||
|
||
def test_default_crf_is_23(self):
|
||
"""默认 CRF 保持 23,画质不变."""
|
||
from shared.ffmpeg_utils import FFMPEG_ENCODE_CRF
|
||
|
||
assert FFMPEG_ENCODE_CRF == "23"
|
||
|
||
def test_default_threads_is_auto(self):
|
||
"""默认线程数为 0(自动检测 CPU 核心数)."""
|
||
from shared.ffmpeg_utils import FFMPEG_ENCODE_THREADS
|
||
|
||
assert FFMPEG_ENCODE_THREADS == "0"
|
||
|
||
def test_preset_is_not_medium(self):
|
||
"""确保 preset 不再是 medium(验证改动生效)."""
|
||
from shared.ffmpeg_utils import FFMPEG_ENCODE_PRESET
|
||
|
||
assert FFMPEG_ENCODE_PRESET != "medium"
|
||
|
||
|
||
# ── 环境变量覆盖 ─────────────────────────────────────────────────────────────
|
||
|
||
|
||
class TestEnvironmentVariableOverride:
|
||
"""测试环境变量覆盖编码参数."""
|
||
|
||
def _reload_module(self):
|
||
"""重新加载模块以读取环境变量."""
|
||
import shared.ffmpeg_utils
|
||
|
||
return importlib.reload(shared.ffmpeg_utils)
|
||
|
||
def test_override_preset_via_env(self, monkeypatch):
|
||
"""通过环境变量覆盖 preset."""
|
||
monkeypatch.setenv("FFMPEG_ENCODE_PRESET", "ultrafast")
|
||
mod = self._reload_module()
|
||
assert mod.FFMPEG_ENCODE_PRESET == "ultrafast"
|
||
# 恢复
|
||
monkeypatch.delenv("FFMPEG_ENCODE_PRESET", raising=False)
|
||
self._reload_module()
|
||
|
||
def test_override_crf_via_env(self, monkeypatch):
|
||
"""通过环境变量覆盖 CRF."""
|
||
monkeypatch.setenv("FFMPEG_ENCODE_CRF", "18")
|
||
mod = self._reload_module()
|
||
assert mod.FFMPEG_ENCODE_CRF == "18"
|
||
monkeypatch.delenv("FFMPEG_ENCODE_CRF", raising=False)
|
||
self._reload_module()
|
||
|
||
def test_override_threads_via_env(self, monkeypatch):
|
||
"""通过环境变量覆盖线程数."""
|
||
monkeypatch.setenv("FFMPEG_ENCODE_THREADS", "4")
|
||
mod = self._reload_module()
|
||
assert mod.FFMPEG_ENCODE_THREADS == "4"
|
||
monkeypatch.delenv("FFMPEG_ENCODE_THREADS", raising=False)
|
||
self._reload_module()
|
||
|
||
|
||
# ── Worker 层 re-export ──────────────────────────────────────────────────────
|
||
|
||
|
||
class TestWorkerReExport:
|
||
"""测试 worker 层 ffmpeg_utils 正确 re-export 编码常量."""
|
||
|
||
def test_worker_reexports_preset(self):
|
||
"""worker ffmpeg_utils 应 re-export FFMPEG_ENCODE_PRESET."""
|
||
from video_processing.ffmpeg_utils import FFMPEG_ENCODE_PRESET
|
||
|
||
assert FFMPEG_ENCODE_PRESET == "fast"
|
||
|
||
def test_worker_reexports_crf(self):
|
||
"""worker ffmpeg_utils 应 re-export FFMPEG_ENCODE_CRF."""
|
||
from video_processing.ffmpeg_utils import FFMPEG_ENCODE_CRF
|
||
|
||
assert FFMPEG_ENCODE_CRF == "23"
|
||
|
||
def test_worker_reexports_threads(self):
|
||
"""worker ffmpeg_utils 应 re-export FFMPEG_ENCODE_THREADS."""
|
||
from video_processing.ffmpeg_utils import FFMPEG_ENCODE_THREADS
|
||
|
||
assert FFMPEG_ENCODE_THREADS == "0"
|
||
|
||
|
||
# ── _execute_ffmpeg 编码参数 ─────────────────────────────────────────────────
|
||
|
||
|
||
class TestExecuteFfmpegEncoding:
|
||
"""测试 _execute_ffmpeg 方法使用正确的编码参数."""
|
||
|
||
def _get_execute_command(self):
|
||
"""提取 _execute_ffmpeg 构建的 FFmpeg 命令."""
|
||
from video_processing.unified_render_service import UnifiedRenderService
|
||
|
||
service = UnifiedRenderService.__new__(UnifiedRenderService)
|
||
service.plan = MagicMock()
|
||
service.plan.id = "test-plan"
|
||
|
||
captured_cmd = []
|
||
|
||
def mock_run_ffmpeg(cmd, **kwargs):
|
||
captured_cmd.extend(cmd)
|
||
|
||
with patch(
|
||
"video_processing.unified_render_service.run_ffmpeg",
|
||
side_effect=mock_run_ffmpeg,
|
||
):
|
||
service._execute_ffmpeg(
|
||
"test_filter_complex",
|
||
["-i", "input.mp4"],
|
||
__import__("pathlib").Path("/tmp/output.mp4"),
|
||
)
|
||
|
||
return captured_cmd
|
||
|
||
def test_execute_uses_fast_preset(self):
|
||
"""_execute_ffmpeg 应使用 fast preset."""
|
||
cmd = self._get_execute_command()
|
||
idx = cmd.index("-preset")
|
||
assert cmd[idx + 1] == "fast"
|
||
|
||
def test_execute_uses_crf_23(self):
|
||
"""_execute_ffmpeg 应使用 CRF 23."""
|
||
cmd = self._get_execute_command()
|
||
idx = cmd.index("-crf")
|
||
assert cmd[idx + 1] == "23"
|
||
|
||
def test_execute_uses_auto_threads(self):
|
||
"""_execute_ffmpeg 应启用多线程."""
|
||
cmd = self._get_execute_command()
|
||
idx = cmd.index("-threads")
|
||
assert cmd[idx + 1] == "0"
|
||
|
||
def test_execute_no_medium_preset(self):
|
||
"""_execute_ffmpeg 不应再使用 medium preset."""
|
||
cmd = self._get_execute_command()
|
||
assert "medium" not in cmd
|
||
|
||
|
||
# ── _render_pass_through 编码参数 ────────────────────────────────────────────
|
||
|
||
|
||
class TestRenderPassThroughEncoding:
|
||
"""测试 _render_pass_through 方法使用正确的编码参数."""
|
||
|
||
def test_passthrough_command_contains_fast_preset(self):
|
||
"""_render_pass_through 命令应包含 fast preset."""
|
||
# 通过源码检查确认参数已替换
|
||
import inspect
|
||
|
||
from video_processing.unified_render_service import UnifiedRenderService
|
||
|
||
source = inspect.getsource(UnifiedRenderService._render_pass_through)
|
||
assert "FFMPEG_ENCODE_PRESET" in source
|
||
assert "FFMPEG_ENCODE_THREADS" in source
|
||
assert '"medium"' not in source
|
||
|
||
def test_passthrough_command_contains_threads(self):
|
||
"""_render_pass_through 命令应包含 threads 参数."""
|
||
import inspect
|
||
|
||
from video_processing.unified_render_service import UnifiedRenderService
|
||
|
||
source = inspect.getsource(UnifiedRenderService._render_pass_through)
|
||
assert "FFMPEG_ENCODE_THREADS" in source
|
||
|
||
|
||
# ── normalize_video 编码参数 ─────────────────────────────────────────────────
|
||
|
||
|
||
class TestNormalizeVideoEncoding:
|
||
"""测试 normalize_video 使用正确的编码参数."""
|
||
|
||
def test_normalize_uses_fast_preset(self):
|
||
"""normalize_video 应使用 fast preset."""
|
||
import inspect
|
||
|
||
from video_processing.ffmpeg_utils import normalize_video
|
||
|
||
source = inspect.getsource(normalize_video)
|
||
assert "FFMPEG_ENCODE_PRESET" in source
|
||
assert '"medium"' not in source
|
||
|
||
def test_normalize_uses_configurable_crf(self):
|
||
"""normalize_video CRF 应使用可配置常量."""
|
||
import inspect
|
||
|
||
from video_processing.ffmpeg_utils import normalize_video
|
||
|
||
source = inspect.getsource(normalize_video)
|
||
assert "FFMPEG_ENCODE_CRF" in source
|
||
|
||
def test_normalize_uses_threads(self):
|
||
"""normalize_video 应启用多线程."""
|
||
import inspect
|
||
|
||
from video_processing.ffmpeg_utils import normalize_video
|
||
|
||
source = inspect.getsource(normalize_video)
|
||
assert "FFMPEG_ENCODE_THREADS" in source
|
||
|
||
|
||
# ── random_edge_crop 编码参数 ────────────────────────────────────────────────
|
||
|
||
|
||
class TestRandomEdgeCropEncoding:
|
||
"""测试 random_edge_crop 函数使用统一编码参数."""
|
||
|
||
def test_crop_uses_unified_preset(self):
|
||
"""random_edge_crop 应使用统一 preset 常量(非硬编码 fast)."""
|
||
import inspect
|
||
|
||
from video_processing.ffmpeg_utils import random_edge_crop
|
||
|
||
source = inspect.getsource(random_edge_crop)
|
||
assert "FFMPEG_ENCODE_PRESET" in source
|
||
|
||
def test_crop_uses_unified_crf(self):
|
||
"""random_edge_crop 应使用统一 CRF 常量(非硬编码 18)."""
|
||
import inspect
|
||
|
||
from video_processing.ffmpeg_utils import random_edge_crop
|
||
|
||
source = inspect.getsource(random_edge_crop)
|
||
assert "FFMPEG_ENCODE_CRF" in source
|
||
assert '"18"' not in source
|
||
|
||
|
||
# ── processor.py 编码参数 ────────────────────────────────────────────────────
|
||
|
||
|
||
class TestProcessorEncoding:
|
||
"""测试 VideoProcessor 使用统一编码参数."""
|
||
|
||
def test_processor_uses_unified_preset(self):
|
||
"""VideoProcessor.concatenate_videos 应使用统一 preset."""
|
||
import inspect
|
||
|
||
from video_processing.processor import VideoProcessor
|
||
|
||
source = inspect.getsource(VideoProcessor.concatenate_videos)
|
||
assert "FFMPEG_ENCODE_PRESET" in source
|
||
assert 'preset="medium"' not in source
|
||
|
||
def test_processor_uses_unified_crf(self):
|
||
"""VideoProcessor.concatenate_videos 应使用统一 CRF."""
|
||
import inspect
|
||
|
||
from video_processing.processor import VideoProcessor
|
||
|
||
source = inspect.getsource(VideoProcessor.concatenate_videos)
|
||
assert "FFMPEG_ENCODE_CRF" in source
|
||
assert "crf=23" not in source
|
||
|
||
|
||
# ── 编码参数一致性验证 ────────────────────────────────────────────────────────
|
||
|
||
|
||
class TestEncodingConsistency:
|
||
"""验证所有渲染路径使用同一套编码参数."""
|
||
|
||
def test_all_paths_share_same_preset_source(self):
|
||
"""所有路径的 preset 均来自 FFMPEG_ENCODE_PRESET."""
|
||
import inspect
|
||
|
||
from video_processing.ffmpeg_utils import normalize_video, random_edge_crop
|
||
from video_processing.processor import VideoProcessor
|
||
from video_processing.unified_render_service import UnifiedRenderService
|
||
|
||
sources = [
|
||
inspect.getsource(UnifiedRenderService._execute_ffmpeg),
|
||
inspect.getsource(UnifiedRenderService._render_pass_through),
|
||
inspect.getsource(normalize_video),
|
||
inspect.getsource(random_edge_crop),
|
||
inspect.getsource(VideoProcessor.concatenate_videos),
|
||
]
|
||
|
||
for i, src in enumerate(sources):
|
||
assert "FFMPEG_ENCODE_PRESET" in src, f"渲染路径 #{i} 未使用 FFMPEG_ENCODE_PRESET"
|
||
|
||
def test_no_hardcoded_medium_preset_anywhere(self):
|
||
"""所有渲染代码中不应再有硬编码的 medium preset."""
|
||
import inspect
|
||
|
||
from video_processing.ffmpeg_utils import normalize_video
|
||
from video_processing.unified_render_service import UnifiedRenderService
|
||
|
||
for fn in [
|
||
UnifiedRenderService._execute_ffmpeg,
|
||
UnifiedRenderService._render_pass_through,
|
||
normalize_video,
|
||
]:
|
||
src = inspect.getsource(fn)
|
||
assert '"medium"' not in src, f"{fn.__name__} 仍有硬编码 medium preset"
|
||
|
||
def test_no_hardcoded_crf_23_anywhere(self):
|
||
"""所有渲染代码中不应再有硬编码的 CRF 23."""
|
||
import inspect
|
||
|
||
from video_processing.ffmpeg_utils import normalize_video
|
||
from video_processing.unified_render_service import UnifiedRenderService
|
||
|
||
for fn in [
|
||
UnifiedRenderService._execute_ffmpeg,
|
||
UnifiedRenderService._render_pass_through,
|
||
normalize_video,
|
||
]:
|
||
src = inspect.getsource(fn)
|
||
# 检查没有硬编码的 "23" 作为 CRF 值
|
||
# 注意:行中可能有其他 "23",只检查紧跟 -crf 之后的值
|
||
lines = src.split("\n")
|
||
for j, line in enumerate(lines):
|
||
if '"-crf"' in line or "'-crf'" in line:
|
||
# 下一个非空行应该是常量引用而非硬编码数字
|
||
for k in range(j + 1, min(j + 3, len(lines))):
|
||
stripped = lines[k].strip().strip(",").strip('"').strip("'")
|
||
if stripped:
|
||
assert stripped != "23", f"{fn.__name__} 仍有硬编码 CRF 23"
|
||
break
|