You've already forked FrameTour-RenderWorker
test
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -31,4 +31,6 @@ target/
|
|||||||
.venv
|
.venv
|
||||||
venv/
|
venv/
|
||||||
cython_debug/
|
cython_debug/
|
||||||
.env
|
.env
|
||||||
|
.serena
|
||||||
|
.claude
|
||||||
335
Jenkinsfile
vendored
Normal file
335
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
environment {
|
||||||
|
// 环境变量
|
||||||
|
PYTHON_VERSION = '3.9'
|
||||||
|
VENV_NAME = 'venv'
|
||||||
|
TEST_REPORTS_DIR = 'test-reports'
|
||||||
|
COVERAGE_DIR = 'coverage-reports'
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Preparation') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
// 清理工作空间
|
||||||
|
echo 'Cleaning workspace...'
|
||||||
|
deleteDir()
|
||||||
|
|
||||||
|
// 检出代码
|
||||||
|
checkout scm
|
||||||
|
|
||||||
|
// 创建报告目录
|
||||||
|
sh """
|
||||||
|
mkdir -p ${TEST_REPORTS_DIR}
|
||||||
|
mkdir -p ${COVERAGE_DIR}
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Environment Setup') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
echo 'Setting up Python environment...'
|
||||||
|
sh """
|
||||||
|
# 创建虚拟环境
|
||||||
|
python${PYTHON_VERSION} -m venv ${VENV_NAME}
|
||||||
|
|
||||||
|
# 激活虚拟环境并安装依赖
|
||||||
|
. ${VENV_NAME}/bin/activate
|
||||||
|
|
||||||
|
# 升级pip
|
||||||
|
pip install --upgrade pip
|
||||||
|
|
||||||
|
# 安装项目依赖
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 安装测试依赖
|
||||||
|
pip install -r requirements-test.txt
|
||||||
|
|
||||||
|
# 验证FFmpeg可用性
|
||||||
|
which ffmpeg || echo "Warning: FFmpeg not found, integration tests may be skipped"
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Code Quality Check') {
|
||||||
|
parallel {
|
||||||
|
stage('Linting') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
echo 'Running code linting...'
|
||||||
|
sh """
|
||||||
|
. ${VENV_NAME}/bin/activate
|
||||||
|
|
||||||
|
# 运行flake8检查
|
||||||
|
flake8 entity/ services/ --output-file=${TEST_REPORTS_DIR}/flake8-report.txt --tee || true
|
||||||
|
|
||||||
|
# 运行black格式检查
|
||||||
|
black --check --diff entity/ services/ > ${TEST_REPORTS_DIR}/black-report.txt || true
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
// 发布lint报告
|
||||||
|
publishHTML([
|
||||||
|
allowMissing: false,
|
||||||
|
alwaysLinkToLastBuild: true,
|
||||||
|
keepAll: true,
|
||||||
|
reportDir: TEST_REPORTS_DIR,
|
||||||
|
reportFiles: 'flake8-report.txt,black-report.txt',
|
||||||
|
reportName: 'Code Quality Report'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Type Checking') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
echo 'Running type checking...'
|
||||||
|
sh """
|
||||||
|
. ${VENV_NAME}/bin/activate
|
||||||
|
|
||||||
|
# 运行mypy类型检查
|
||||||
|
mypy entity/ services/ --html-report ${TEST_REPORTS_DIR}/mypy-html --txt-report ${TEST_REPORTS_DIR}/mypy-txt || true
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Unit Tests') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
echo 'Running unit tests...'
|
||||||
|
sh """
|
||||||
|
. ${VENV_NAME}/bin/activate
|
||||||
|
|
||||||
|
# 运行单元测试
|
||||||
|
pytest tests/test_effects/ tests/test_ffmpeg_builder/ \\
|
||||||
|
--junitxml=${TEST_REPORTS_DIR}/unit-tests.xml \\
|
||||||
|
--html=${TEST_REPORTS_DIR}/unit-tests.html \\
|
||||||
|
--self-contained-html \\
|
||||||
|
--cov=entity \\
|
||||||
|
--cov=services \\
|
||||||
|
--cov-report=xml:${COVERAGE_DIR}/unit-coverage.xml \\
|
||||||
|
--cov-branch \\
|
||||||
|
-v \\
|
||||||
|
-m "not integration"
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
// 发布单元测试结果
|
||||||
|
junit "${TEST_REPORTS_DIR}/unit-tests.xml"
|
||||||
|
|
||||||
|
// 发布HTML测试报告
|
||||||
|
publishHTML([
|
||||||
|
allowMissing: false,
|
||||||
|
alwaysLinkToLastBuild: true,
|
||||||
|
keepAll: true,
|
||||||
|
reportDir: TEST_REPORTS_DIR,
|
||||||
|
reportFiles: 'unit-tests.html',
|
||||||
|
reportName: 'Unit Tests Report'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Integration Tests') {
|
||||||
|
when {
|
||||||
|
// 只在有FFmpeg的环境中运行集成测试
|
||||||
|
expression {
|
||||||
|
return sh(
|
||||||
|
script: 'which ffmpeg',
|
||||||
|
returnStatus: true
|
||||||
|
) == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
echo 'Running integration tests...'
|
||||||
|
sh """
|
||||||
|
. ${VENV_NAME}/bin/activate
|
||||||
|
|
||||||
|
# 运行集成测试
|
||||||
|
pytest tests/test_integration/ \\
|
||||||
|
--junitxml=${TEST_REPORTS_DIR}/integration-tests.xml \\
|
||||||
|
--html=${TEST_REPORTS_DIR}/integration-tests.html \\
|
||||||
|
--self-contained-html \\
|
||||||
|
--cov=entity \\
|
||||||
|
--cov=services \\
|
||||||
|
--cov-report=xml:${COVERAGE_DIR}/integration-coverage.xml \\
|
||||||
|
--cov-branch \\
|
||||||
|
--timeout=300 \\
|
||||||
|
-v \\
|
||||||
|
-m "integration"
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
// 发布集成测试结果
|
||||||
|
junit "${TEST_REPORTS_DIR}/integration-tests.xml"
|
||||||
|
|
||||||
|
// 发布HTML集成测试报告
|
||||||
|
publishHTML([
|
||||||
|
allowMissing: false,
|
||||||
|
alwaysLinkToLastBuild: true,
|
||||||
|
keepAll: true,
|
||||||
|
reportDir: TEST_REPORTS_DIR,
|
||||||
|
reportFiles: 'integration-tests.html',
|
||||||
|
reportName: 'Integration Tests Report'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Complete Test Suite') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
echo 'Running complete test suite with coverage...'
|
||||||
|
sh """
|
||||||
|
. ${VENV_NAME}/bin/activate
|
||||||
|
|
||||||
|
# 运行完整测试套件
|
||||||
|
pytest tests/ \\
|
||||||
|
--junitxml=${TEST_REPORTS_DIR}/all-tests.xml \\
|
||||||
|
--html=${TEST_REPORTS_DIR}/all-tests.html \\
|
||||||
|
--self-contained-html \\
|
||||||
|
--cov=entity \\
|
||||||
|
--cov=services \\
|
||||||
|
--cov-report=xml:${COVERAGE_DIR}/coverage.xml \\
|
||||||
|
--cov-report=term-missing \\
|
||||||
|
--cov-branch \\
|
||||||
|
--cov-fail-under=70 \\
|
||||||
|
-v
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
// 发布完整测试结果
|
||||||
|
junit "${TEST_REPORTS_DIR}/all-tests.xml"
|
||||||
|
|
||||||
|
// 发布代码覆盖率报告
|
||||||
|
publishCoverage([
|
||||||
|
adapters: [
|
||||||
|
[
|
||||||
|
mergeToOneReport: true,
|
||||||
|
path: "${COVERAGE_DIR}/coverage.xml"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
sourceFileResolver: [
|
||||||
|
[
|
||||||
|
level: 'NEVER_STORE'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
// 发布完整测试HTML报告
|
||||||
|
publishHTML([
|
||||||
|
allowMissing: false,
|
||||||
|
alwaysLinkToLastBuild: true,
|
||||||
|
keepAll: true,
|
||||||
|
reportDir: TEST_REPORTS_DIR,
|
||||||
|
reportFiles: 'all-tests.html',
|
||||||
|
reportName: 'Complete Test Report'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Performance Tests') {
|
||||||
|
when {
|
||||||
|
// 只在主分支或发布分支运行性能测试
|
||||||
|
anyOf {
|
||||||
|
branch 'master'
|
||||||
|
branch 'main'
|
||||||
|
branch 'release/*'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
echo 'Running performance tests...'
|
||||||
|
sh """
|
||||||
|
. ${VENV_NAME}/bin/activate
|
||||||
|
|
||||||
|
# 运行性能测试
|
||||||
|
RUN_STRESS_TESTS=1 pytest tests/test_integration/test_ffmpeg_execution.py::TestFFmpegExecution::test_stress_test_large_effects_chain \\
|
||||||
|
--benchmark-json=${TEST_REPORTS_DIR}/benchmark.json \\
|
||||||
|
--html=${TEST_REPORTS_DIR}/performance-tests.html \\
|
||||||
|
--self-contained-html \\
|
||||||
|
-v \\
|
||||||
|
-m "stress" || true
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
// 发布性能测试报告
|
||||||
|
publishHTML([
|
||||||
|
allowMissing: true,
|
||||||
|
alwaysLinkToLastBuild: true,
|
||||||
|
keepAll: true,
|
||||||
|
reportDir: TEST_REPORTS_DIR,
|
||||||
|
reportFiles: 'performance-tests.html',
|
||||||
|
reportName: 'Performance Tests Report'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
// 清理虚拟环境
|
||||||
|
sh """
|
||||||
|
rm -rf ${VENV_NAME}
|
||||||
|
"""
|
||||||
|
|
||||||
|
// 归档测试报告
|
||||||
|
archiveArtifacts artifacts: "${TEST_REPORTS_DIR}/**/*", allowEmptyArchive: true
|
||||||
|
archiveArtifacts artifacts: "${COVERAGE_DIR}/**/*", allowEmptyArchive: true
|
||||||
|
}
|
||||||
|
|
||||||
|
success {
|
||||||
|
echo 'All tests passed successfully!'
|
||||||
|
|
||||||
|
// 发送成功通知
|
||||||
|
script {
|
||||||
|
if (env.BRANCH_NAME == 'master' || env.BRANCH_NAME == 'main') {
|
||||||
|
// 这里可以添加Slack、邮件或其他通知
|
||||||
|
echo 'Sending success notification...'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
failure {
|
||||||
|
echo 'Tests failed!'
|
||||||
|
|
||||||
|
// 发送失败通知
|
||||||
|
script {
|
||||||
|
// 这里可以添加Slack、邮件或其他通知
|
||||||
|
echo 'Sending failure notification...'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unstable {
|
||||||
|
echo 'Tests completed with warnings!'
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup {
|
||||||
|
// 清理临时文件
|
||||||
|
deleteDir()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
pytest.ini
Normal file
53
pytest.ini
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
[tool:pytest]
|
||||||
|
testpaths = tests
|
||||||
|
python_files = test_*.py
|
||||||
|
python_classes = Test*
|
||||||
|
python_functions = test_*
|
||||||
|
|
||||||
|
# 标记定义
|
||||||
|
markers =
|
||||||
|
integration: marks tests as integration tests (may be slow)
|
||||||
|
unit: marks tests as unit tests (fast)
|
||||||
|
slow: marks tests as slow running
|
||||||
|
stress: marks tests as stress tests
|
||||||
|
|
||||||
|
# 输出配置
|
||||||
|
addopts =
|
||||||
|
-v
|
||||||
|
--tb=short
|
||||||
|
--strict-markers
|
||||||
|
--strict-config
|
||||||
|
--cov=entity
|
||||||
|
--cov=services
|
||||||
|
--cov-report=xml:coverage.xml
|
||||||
|
--cov-report=html:htmlcov
|
||||||
|
--cov-report=term-missing
|
||||||
|
--cov-branch
|
||||||
|
|
||||||
|
# 过滤警告
|
||||||
|
filterwarnings =
|
||||||
|
ignore::DeprecationWarning
|
||||||
|
ignore::PendingDeprecationWarning
|
||||||
|
|
||||||
|
# 最小覆盖率要求
|
||||||
|
[tool:coverage:run]
|
||||||
|
source = entity,services
|
||||||
|
omit =
|
||||||
|
*/tests/*
|
||||||
|
*/test_*
|
||||||
|
*/__pycache__/*
|
||||||
|
*/venv/*
|
||||||
|
*/env/*
|
||||||
|
|
||||||
|
[tool:coverage:report]
|
||||||
|
exclude_lines =
|
||||||
|
pragma: no cover
|
||||||
|
def __repr__
|
||||||
|
if self.debug:
|
||||||
|
if settings.DEBUG
|
||||||
|
raise AssertionError
|
||||||
|
raise NotImplementedError
|
||||||
|
if 0:
|
||||||
|
if __name__ == .__main__.:
|
||||||
|
class .*\bProtocol\):
|
||||||
|
@(abc\.)?abstractmethod
|
||||||
23
requirements-test.txt
Normal file
23
requirements-test.txt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# 测试依赖
|
||||||
|
pytest>=7.0.0
|
||||||
|
pytest-cov>=4.0.0
|
||||||
|
pytest-mock>=3.10.0
|
||||||
|
pytest-xdist>=3.0.0 # 并行测试
|
||||||
|
pytest-timeout>=2.1.0
|
||||||
|
pytest-html>=3.1.0 # HTML报告
|
||||||
|
|
||||||
|
# 代码质量
|
||||||
|
flake8>=5.0.0
|
||||||
|
black>=22.0.0
|
||||||
|
mypy>=1.0.0
|
||||||
|
|
||||||
|
# 覆盖率报告
|
||||||
|
coverage[toml]>=6.0.0
|
||||||
|
|
||||||
|
# 性能测试
|
||||||
|
pytest-benchmark>=4.0.0
|
||||||
|
|
||||||
|
# 测试辅助
|
||||||
|
factory-boy>=3.2.0 # 测试数据工厂
|
||||||
|
freezegun>=1.2.0 # 时间模拟
|
||||||
|
responses>=0.22.0 # HTTP模拟
|
||||||
267
run_tests.py
Normal file
267
run_tests.py
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
测试运行器脚本
|
||||||
|
支持不同类型的测试运行和报告生成
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import argparse
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(cmd, cwd=None):
|
||||||
|
"""运行命令并返回结果"""
|
||||||
|
print(f"Running: {' '.join(cmd)}")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
cwd=cwd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False
|
||||||
|
)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
return result.returncode == 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error running command: {e}", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_dependencies():
|
||||||
|
"""检查依赖是否安装"""
|
||||||
|
print("Checking dependencies...")
|
||||||
|
|
||||||
|
# 检查pytest
|
||||||
|
if not run_command([sys.executable, "-c", "import pytest"]):
|
||||||
|
print("pytest not found. Installing test dependencies...")
|
||||||
|
if not run_command([sys.executable, "-m", "pip", "install", "-r", "requirements-test.txt"]):
|
||||||
|
print("Failed to install test dependencies", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查FFmpeg
|
||||||
|
ffmpeg_available = run_command(["ffmpeg", "-version"])
|
||||||
|
if not ffmpeg_available:
|
||||||
|
print("Warning: FFmpeg not found. Integration tests may be skipped.")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def run_unit_tests(args):
|
||||||
|
"""运行单元测试"""
|
||||||
|
print("\n=== Running Unit Tests ===")
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
sys.executable, "-m", "pytest",
|
||||||
|
"tests/test_effects/",
|
||||||
|
"tests/test_ffmpeg_builder/",
|
||||||
|
"-v",
|
||||||
|
"-m", "not integration"
|
||||||
|
]
|
||||||
|
|
||||||
|
if args.coverage:
|
||||||
|
cmd.extend([
|
||||||
|
"--cov=entity",
|
||||||
|
"--cov=services",
|
||||||
|
"--cov-report=xml:coverage.xml",
|
||||||
|
"--cov-report=html:htmlcov",
|
||||||
|
"--cov-report=term-missing",
|
||||||
|
"--cov-branch"
|
||||||
|
])
|
||||||
|
|
||||||
|
if args.xml_report:
|
||||||
|
cmd.extend(["--junitxml=unit-tests.xml"])
|
||||||
|
|
||||||
|
if args.html_report:
|
||||||
|
cmd.extend(["--html=unit-tests.html", "--self-contained-html"])
|
||||||
|
|
||||||
|
return run_command(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def run_integration_tests(args):
|
||||||
|
"""运行集成测试"""
|
||||||
|
print("\n=== Running Integration Tests ===")
|
||||||
|
|
||||||
|
# 检查FFmpeg
|
||||||
|
if not run_command(["ffmpeg", "-version"]):
|
||||||
|
print("FFmpeg not available, skipping integration tests")
|
||||||
|
return True
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
sys.executable, "-m", "pytest",
|
||||||
|
"tests/test_integration/",
|
||||||
|
"-v",
|
||||||
|
"-m", "integration",
|
||||||
|
"--timeout=300"
|
||||||
|
]
|
||||||
|
|
||||||
|
if args.coverage:
|
||||||
|
cmd.extend([
|
||||||
|
"--cov=entity",
|
||||||
|
"--cov=services",
|
||||||
|
"--cov-report=xml:integration-coverage.xml",
|
||||||
|
"--cov-report=html:integration-htmlcov",
|
||||||
|
"--cov-branch"
|
||||||
|
])
|
||||||
|
|
||||||
|
if args.xml_report:
|
||||||
|
cmd.extend(["--junitxml=integration-tests.xml"])
|
||||||
|
|
||||||
|
if args.html_report:
|
||||||
|
cmd.extend(["--html=integration-tests.html", "--self-contained-html"])
|
||||||
|
|
||||||
|
return run_command(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def run_all_tests(args):
|
||||||
|
"""运行所有测试"""
|
||||||
|
print("\n=== Running All Tests ===")
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
sys.executable, "-m", "pytest",
|
||||||
|
"tests/",
|
||||||
|
"-v"
|
||||||
|
]
|
||||||
|
|
||||||
|
if args.coverage:
|
||||||
|
cmd.extend([
|
||||||
|
"--cov=entity",
|
||||||
|
"--cov=services",
|
||||||
|
"--cov-report=xml:coverage.xml",
|
||||||
|
"--cov-report=html:htmlcov",
|
||||||
|
"--cov-report=term-missing",
|
||||||
|
"--cov-branch"
|
||||||
|
])
|
||||||
|
|
||||||
|
if args.fail_under:
|
||||||
|
cmd.extend([f"--cov-fail-under={args.fail_under}"])
|
||||||
|
|
||||||
|
if args.xml_report:
|
||||||
|
cmd.extend(["--junitxml=all-tests.xml"])
|
||||||
|
|
||||||
|
if args.html_report:
|
||||||
|
cmd.extend(["--html=all-tests.html", "--self-contained-html"])
|
||||||
|
|
||||||
|
return run_command(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def run_effect_tests(effect_name=None):
|
||||||
|
"""运行特定特效测试"""
|
||||||
|
if effect_name:
|
||||||
|
print(f"\n=== Running {effect_name} Effect Tests ===")
|
||||||
|
cmd = [
|
||||||
|
sys.executable, "-m", "pytest",
|
||||||
|
f"tests/test_effects/test_{effect_name}_effect.py",
|
||||||
|
"-v"
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
print("\n=== Running All Effect Tests ===")
|
||||||
|
cmd = [
|
||||||
|
sys.executable, "-m", "pytest",
|
||||||
|
"tests/test_effects/",
|
||||||
|
"-v"
|
||||||
|
]
|
||||||
|
|
||||||
|
return run_command(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def run_stress_tests():
|
||||||
|
"""运行压力测试"""
|
||||||
|
print("\n=== Running Stress Tests ===")
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
env['RUN_STRESS_TESTS'] = '1'
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
sys.executable, "-m", "pytest",
|
||||||
|
"tests/test_integration/",
|
||||||
|
"-v",
|
||||||
|
"-m", "stress",
|
||||||
|
"--timeout=600"
|
||||||
|
]
|
||||||
|
|
||||||
|
return subprocess.run(cmd, env=env).returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def create_test_video():
|
||||||
|
"""创建测试视频文件"""
|
||||||
|
print("\n=== Creating Test Video Files ===")
|
||||||
|
|
||||||
|
if not run_command(["ffmpeg", "-version"]):
|
||||||
|
print("FFmpeg not available, cannot create test videos")
|
||||||
|
return False
|
||||||
|
|
||||||
|
test_data_dir = Path("tests/test_data/videos")
|
||||||
|
test_data_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 创建短视频文件
|
||||||
|
video_path = test_data_dir / "sample.mp4"
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg", "-y",
|
||||||
|
"-f", "lavfi",
|
||||||
|
"-i", "testsrc=duration=5:size=640x480:rate=25",
|
||||||
|
"-c:v", "libx264",
|
||||||
|
"-preset", "ultrafast",
|
||||||
|
"-crf", "23",
|
||||||
|
str(video_path)
|
||||||
|
]
|
||||||
|
|
||||||
|
if run_command(cmd):
|
||||||
|
print(f"Created test video: {video_path}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("Failed to create test video")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="RenderWorker Test Runner")
|
||||||
|
parser.add_argument("test_type", choices=[
|
||||||
|
"unit", "integration", "all", "effects", "stress", "setup"
|
||||||
|
], help="Type of tests to run")
|
||||||
|
|
||||||
|
parser.add_argument("--effect", help="Specific effect to test (for effects command)")
|
||||||
|
parser.add_argument("--coverage", action="store_true", help="Generate coverage report")
|
||||||
|
parser.add_argument("--xml-report", action="store_true", help="Generate XML test report")
|
||||||
|
parser.add_argument("--html-report", action="store_true", help="Generate HTML test report")
|
||||||
|
parser.add_argument("--fail-under", type=int, default=70, help="Minimum coverage percentage")
|
||||||
|
parser.add_argument("--no-deps-check", action="store_true", help="Skip dependency check")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# 检查依赖
|
||||||
|
if not args.no_deps_check and not check_dependencies():
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 运行相应的测试
|
||||||
|
success = True
|
||||||
|
|
||||||
|
if args.test_type == "unit":
|
||||||
|
success = run_unit_tests(args)
|
||||||
|
elif args.test_type == "integration":
|
||||||
|
success = run_integration_tests(args)
|
||||||
|
elif args.test_type == "all":
|
||||||
|
success = run_all_tests(args)
|
||||||
|
elif args.test_type == "effects":
|
||||||
|
success = run_effect_tests(args.effect)
|
||||||
|
elif args.test_type == "stress":
|
||||||
|
success = run_stress_tests()
|
||||||
|
elif args.test_type == "setup":
|
||||||
|
success = create_test_video()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("\n✅ Tests completed successfully!")
|
||||||
|
if args.coverage:
|
||||||
|
print("📊 Coverage report generated in htmlcov/")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print("\n❌ Tests failed!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
313
tests/README.md
Normal file
313
tests/README.md
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
# RenderWorker 测试文档
|
||||||
|
|
||||||
|
本目录包含 RenderWorker 项目的完整测试套件,用于验证特效参数生成和 FFmpeg 渲染功能。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── conftest.py # pytest 配置和公共 fixtures
|
||||||
|
├── test_data/ # 测试数据
|
||||||
|
│ ├── videos/ # 测试视频文件
|
||||||
|
│ ├── templates/ # 测试模板数据
|
||||||
|
│ └── expected_outputs/ # 预期输出结果
|
||||||
|
├── test_effects/ # 特效单元测试
|
||||||
|
│ ├── test_base.py # 基础类测试
|
||||||
|
│ ├── test_zoom_effect.py # 缩放特效测试
|
||||||
|
│ ├── test_speed_effect.py # 变速特效测试
|
||||||
|
│ └── ... # 其他特效测试
|
||||||
|
├── test_ffmpeg_builder/ # FFmpeg 命令构建测试
|
||||||
|
│ └── test_ffmpeg_command_builder.py
|
||||||
|
├── test_integration/ # 集成测试
|
||||||
|
│ └── test_ffmpeg_execution.py # FFmpeg 执行测试
|
||||||
|
└── utils/ # 测试工具
|
||||||
|
└── test_helpers.py # 测试辅助函数
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 安装测试依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements-test.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 运行测试
|
||||||
|
|
||||||
|
使用测试运行器脚本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行所有测试
|
||||||
|
python run_tests.py all --coverage
|
||||||
|
|
||||||
|
# 只运行单元测试
|
||||||
|
python run_tests.py unit --coverage --html-report
|
||||||
|
|
||||||
|
# 只运行集成测试
|
||||||
|
python run_tests.py integration
|
||||||
|
|
||||||
|
# 运行特定特效测试
|
||||||
|
python run_tests.py effects --effect zoom
|
||||||
|
|
||||||
|
# 运行压力测试
|
||||||
|
python run_tests.py stress
|
||||||
|
```
|
||||||
|
|
||||||
|
或直接使用 pytest:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行所有测试
|
||||||
|
pytest tests/ -v --cov=entity --cov=services
|
||||||
|
|
||||||
|
# 只运行单元测试
|
||||||
|
pytest tests/test_effects/ tests/test_ffmpeg_builder/ -v
|
||||||
|
|
||||||
|
# 只运行集成测试
|
||||||
|
pytest tests/test_integration/ -v -m integration
|
||||||
|
|
||||||
|
# 生成覆盖率报告
|
||||||
|
pytest tests/ --cov=entity --cov=services --cov-report=html
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试类型说明
|
||||||
|
|
||||||
|
### 单元测试 (Unit Tests)
|
||||||
|
|
||||||
|
位置:`tests/test_effects/`, `tests/test_ffmpeg_builder/`
|
||||||
|
|
||||||
|
测试内容:
|
||||||
|
- 各个特效处理器的参数验证
|
||||||
|
- FFmpeg 滤镜参数生成
|
||||||
|
- 特效注册表功能
|
||||||
|
- FFmpeg 命令构建逻辑
|
||||||
|
|
||||||
|
特点:
|
||||||
|
- 运行速度快
|
||||||
|
- 不依赖外部工具
|
||||||
|
- 测试覆盖率高
|
||||||
|
|
||||||
|
### 集成测试 (Integration Tests)
|
||||||
|
|
||||||
|
位置:`tests/test_integration/`
|
||||||
|
|
||||||
|
测试内容:
|
||||||
|
- 实际 FFmpeg 命令执行
|
||||||
|
- 视频文件处理验证
|
||||||
|
- 特效组合效果测试
|
||||||
|
- 错误处理验证
|
||||||
|
|
||||||
|
依赖:
|
||||||
|
- 需要系统安装 FFmpeg
|
||||||
|
- 需要测试视频文件
|
||||||
|
|
||||||
|
### 压力测试 (Stress Tests)
|
||||||
|
|
||||||
|
测试内容:
|
||||||
|
- 大量特效链处理
|
||||||
|
- 长时间运行稳定性
|
||||||
|
- 资源使用情况
|
||||||
|
|
||||||
|
运行条件:
|
||||||
|
- 设置环境变量 `RUN_STRESS_TESTS=1`
|
||||||
|
- 较长的超时时间
|
||||||
|
|
||||||
|
## 测试配置
|
||||||
|
|
||||||
|
### pytest 配置
|
||||||
|
|
||||||
|
在 `pytest.ini` 中配置:
|
||||||
|
- 测试路径和文件模式
|
||||||
|
- 覆盖率设置
|
||||||
|
- 标记定义
|
||||||
|
- 输出格式
|
||||||
|
|
||||||
|
### 环境变量
|
||||||
|
|
||||||
|
- `RUN_STRESS_TESTS`: 启用压力测试
|
||||||
|
- `FFMPEG_PATH`: 自定义 FFmpeg 路径
|
||||||
|
|
||||||
|
## 特效测试详解
|
||||||
|
|
||||||
|
### 缩放特效 (ZoomEffect)
|
||||||
|
|
||||||
|
测试用例:
|
||||||
|
- 有效参数验证:`"0,2.0,3.0"`(开始时间,缩放因子,持续时间)
|
||||||
|
- 无效参数处理:负值、非数字、参数不足
|
||||||
|
- 静态缩放:持续时间为 0
|
||||||
|
- 动态缩放:指定时间段内的缩放
|
||||||
|
- 位置 JSON 解析:自定义缩放中心点
|
||||||
|
|
||||||
|
生成滤镜格式:
|
||||||
|
```
|
||||||
|
# 静态缩放
|
||||||
|
[0:v]trim=start=0,zoompan=z=2.0:x=iw/2:y=ih/2:d=1[v_eff1]
|
||||||
|
|
||||||
|
# 动态缩放
|
||||||
|
[0:v]zoompan=z=if(between(t\,0\,3.0)\,2.0\,1):x=iw/2:y=ih/2:d=1[v_eff1]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 变速特效 (SpeedEffect)
|
||||||
|
|
||||||
|
测试用例:
|
||||||
|
- 加速效果:`"2.0"`(2倍速)
|
||||||
|
- 减速效果:`"0.5"`(0.5倍速)
|
||||||
|
- 无效参数:零值、负值、非数字
|
||||||
|
- 默认处理:空参数或 1.0 倍速
|
||||||
|
|
||||||
|
生成滤镜格式:
|
||||||
|
```
|
||||||
|
[0:v]setpts=2.0*PTS[v_eff1]
|
||||||
|
```
|
||||||
|
|
||||||
|
## FFmpeg 命令构建测试
|
||||||
|
|
||||||
|
### 测试场景
|
||||||
|
|
||||||
|
1. **单文件复制**:直接复制无需编码
|
||||||
|
2. **多文件拼接**:使用 concat 滤镜
|
||||||
|
3. **特效处理**:复杂滤镜链构建
|
||||||
|
4. **错误处理**:缺失文件、无效参数
|
||||||
|
|
||||||
|
### 命令验证
|
||||||
|
|
||||||
|
使用 `FFmpegValidator` 验证:
|
||||||
|
- 命令结构完整性
|
||||||
|
- 输入输出文件存在
|
||||||
|
- 滤镜语法正确性
|
||||||
|
- 流标识符格式
|
||||||
|
|
||||||
|
## 集成测试详解
|
||||||
|
|
||||||
|
### 真实 FFmpeg 执行
|
||||||
|
|
||||||
|
测试流程:
|
||||||
|
1. 创建测试视频文件
|
||||||
|
2. 构建 FFmpeg 命令
|
||||||
|
3. 执行命令并检查返回码
|
||||||
|
4. 验证输出文件存在且有效
|
||||||
|
|
||||||
|
### 测试用例
|
||||||
|
|
||||||
|
- 简单复制操作
|
||||||
|
- 单特效处理
|
||||||
|
- 多特效组合
|
||||||
|
- 视频拼接
|
||||||
|
- 错误情况处理
|
||||||
|
|
||||||
|
### 性能测试
|
||||||
|
|
||||||
|
监控指标:
|
||||||
|
- 执行时间
|
||||||
|
- 内存使用
|
||||||
|
- 文件大小
|
||||||
|
- 处理速度
|
||||||
|
|
||||||
|
## 代码覆盖率
|
||||||
|
|
||||||
|
目标覆盖率:≥ 70%
|
||||||
|
|
||||||
|
覆盖范围:
|
||||||
|
- `entity/` 目录下的所有模块
|
||||||
|
- `services/` 目录下的所有模块
|
||||||
|
|
||||||
|
报告格式:
|
||||||
|
- XML 格式:用于 Jenkins CI/CD
|
||||||
|
- HTML 格式:用于本地查看
|
||||||
|
- 终端输出:实时查看缺失覆盖
|
||||||
|
|
||||||
|
## CI/CD 集成
|
||||||
|
|
||||||
|
### Jenkins Pipeline
|
||||||
|
|
||||||
|
配置文件:`Jenkinsfile`
|
||||||
|
|
||||||
|
阶段:
|
||||||
|
1. 环境准备
|
||||||
|
2. 代码质量检查
|
||||||
|
3. 单元测试
|
||||||
|
4. 集成测试
|
||||||
|
5. 完整测试套件
|
||||||
|
6. 性能测试
|
||||||
|
|
||||||
|
报告生成:
|
||||||
|
- JUnit XML 测试报告
|
||||||
|
- Cobertura 覆盖率报告
|
||||||
|
- HTML 测试和覆盖率报告
|
||||||
|
|
||||||
|
### 自动化触发
|
||||||
|
|
||||||
|
- 代码提交时运行单元测试
|
||||||
|
- PR 创建时运行完整测试
|
||||||
|
- 主分支更新时运行包含性能测试的完整套件
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **FFmpeg 未找到**
|
||||||
|
```bash
|
||||||
|
# 安装 FFmpeg
|
||||||
|
sudo apt-get install ffmpeg # Ubuntu/Debian
|
||||||
|
brew install ffmpeg # macOS
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **测试视频创建失败**
|
||||||
|
```bash
|
||||||
|
# 手动创建测试视频
|
||||||
|
python run_tests.py setup
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **覆盖率过低**
|
||||||
|
- 检查是否有未测试的代码路径
|
||||||
|
- 添加边界条件测试
|
||||||
|
- 验证测试是否实际运行
|
||||||
|
|
||||||
|
4. **集成测试超时**
|
||||||
|
- 增加超时时间:`--timeout=600`
|
||||||
|
- 使用更小的测试文件
|
||||||
|
- 检查系统资源使用情况
|
||||||
|
|
||||||
|
### 调试技巧
|
||||||
|
|
||||||
|
1. **查看详细输出**:
|
||||||
|
```bash
|
||||||
|
pytest tests/ -v -s
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **只运行失败的测试**:
|
||||||
|
```bash
|
||||||
|
pytest tests/ --lf
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **停在第一个失败**:
|
||||||
|
```bash
|
||||||
|
pytest tests/ -x
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **查看覆盖率详情**:
|
||||||
|
```bash
|
||||||
|
pytest tests/ --cov=entity --cov-report=term-missing
|
||||||
|
```
|
||||||
|
|
||||||
|
## 贡献指南
|
||||||
|
|
||||||
|
### 添加新测试
|
||||||
|
|
||||||
|
1. 在相应目录创建测试文件
|
||||||
|
2. 使用描述性的测试函数名
|
||||||
|
3. 添加适当的测试标记
|
||||||
|
4. 更新文档说明
|
||||||
|
|
||||||
|
### 测试最佳实践
|
||||||
|
|
||||||
|
1. **独立性**:每个测试应该独立运行
|
||||||
|
2. **可重复性**:测试结果应该一致
|
||||||
|
3. **清晰性**:测试意图应该明确
|
||||||
|
4. **完整性**:覆盖正常和异常情况
|
||||||
|
|
||||||
|
### 代码质量
|
||||||
|
|
||||||
|
- 遵循 PEP 8 代码风格
|
||||||
|
- 使用类型注解
|
||||||
|
- 添加适当的文档字符串
|
||||||
|
- 使用有意义的变量名
|
||||||
90
tests/conftest.py
Normal file
90
tests/conftest.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
"""pytest配置文件"""
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from entity.render_task import RenderTask
|
||||||
|
from config.settings import Config
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def temp_dir():
|
||||||
|
"""创建临时目录"""
|
||||||
|
temp_path = tempfile.mkdtemp()
|
||||||
|
yield temp_path
|
||||||
|
shutil.rmtree(temp_path, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_config():
|
||||||
|
"""测试用配置"""
|
||||||
|
return Config(
|
||||||
|
encoder_args="-c:v h264",
|
||||||
|
video_args="",
|
||||||
|
template_dir="tests/test_data/templates",
|
||||||
|
api_endpoint="http://test.local",
|
||||||
|
access_key="test_key"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_render_task():
|
||||||
|
"""示例渲染任务"""
|
||||||
|
return RenderTask(
|
||||||
|
task_id="test_task_001",
|
||||||
|
template_id="test_template",
|
||||||
|
output_path="test_output.mp4",
|
||||||
|
frame_rate=25,
|
||||||
|
effects=["zoom:0,2.0,3.0", "ospeed:1.5"],
|
||||||
|
ext_data={
|
||||||
|
"posJson": '{"ltX": 100, "ltY": 100, "rbX": 200, "rbY": 200, "imgWidth": 300, "imgHeight": 300}'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_video_file(temp_dir):
|
||||||
|
"""创建测试用的样本视频文件路径"""
|
||||||
|
video_path = os.path.join(temp_dir, "sample.mp4")
|
||||||
|
# 这里只返回路径,实际测试中可能需要真实视频文件
|
||||||
|
return video_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def ffmpeg_available():
|
||||||
|
"""检查FFmpeg是否可用"""
|
||||||
|
import subprocess
|
||||||
|
try:
|
||||||
|
subprocess.run(["ffmpeg", "-version"], capture_output=True, check=True)
|
||||||
|
return True
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_ext_data():
|
||||||
|
"""模拟扩展数据"""
|
||||||
|
return {
|
||||||
|
"posJson": '{"ltX": 50, "ltY": 50, "rbX": 150, "rbY": 150, "imgWidth": 200, "imgHeight": 200}',
|
||||||
|
"templateData": {"width": 1920, "height": 1080}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# 标记:只在FFmpeg可用时运行集成测试
|
||||||
|
def pytest_collection_modifyitems(config, items):
|
||||||
|
"""根据FFmpeg可用性修改测试收集"""
|
||||||
|
try:
|
||||||
|
import subprocess
|
||||||
|
subprocess.run(["ffmpeg", "-version"], capture_output=True, check=True)
|
||||||
|
ffmpeg_available = True
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
|
ffmpeg_available = False
|
||||||
|
|
||||||
|
if not ffmpeg_available:
|
||||||
|
skip_ffmpeg = pytest.mark.skip(reason="FFmpeg not available")
|
||||||
|
for item in items:
|
||||||
|
if "integration" in item.keywords:
|
||||||
|
item.add_marker(skip_ffmpeg)
|
||||||
15
tests/test_data/README.md
Normal file
15
tests/test_data/README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 测试数据说明
|
||||||
|
|
||||||
|
这个目录包含测试所需的样本数据:
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
- `videos/` - 测试用视频文件
|
||||||
|
- `templates/` - 测试用模板数据
|
||||||
|
- `expected_outputs/` - 预期输出结果
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
- 小尺寸视频文件用于快速测试
|
||||||
|
- 模板文件包含各种特效配置
|
||||||
|
- 预期输出用于验证测试结果
|
||||||
|
|
||||||
|
注意:实际的视频文件可能较大,建议使用小尺寸测试文件。
|
||||||
188
tests/test_effects/test_base.py
Normal file
188
tests/test_effects/test_base.py
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
"""测试特效基础类和注册表"""
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
from entity.effects.base import EffectProcessor, EffectRegistry
|
||||||
|
|
||||||
|
|
||||||
|
class TestEffectProcessor:
|
||||||
|
"""测试特效处理器基类"""
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
"""测试初始化"""
|
||||||
|
processor = Mock(spec=EffectProcessor)
|
||||||
|
processor.__init__("test_params", {"key": "value"})
|
||||||
|
|
||||||
|
assert processor.params == "test_params"
|
||||||
|
assert processor.ext_data == {"key": "value"}
|
||||||
|
assert processor.frame_rate == 25
|
||||||
|
|
||||||
|
def test_init_default_ext_data(self):
|
||||||
|
"""测试默认扩展数据"""
|
||||||
|
processor = Mock(spec=EffectProcessor)
|
||||||
|
processor.__init__("test_params")
|
||||||
|
|
||||||
|
assert processor.ext_data == {}
|
||||||
|
|
||||||
|
def test_parse_params_empty(self):
|
||||||
|
"""测试解析空参数"""
|
||||||
|
processor = EffectProcessor("", {})
|
||||||
|
result = processor.parse_params()
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
def test_parse_params_single(self):
|
||||||
|
"""测试解析单个参数"""
|
||||||
|
processor = EffectProcessor("123", {})
|
||||||
|
result = processor.parse_params()
|
||||||
|
assert result == ["123"]
|
||||||
|
|
||||||
|
def test_parse_params_multiple(self):
|
||||||
|
"""测试解析多个参数"""
|
||||||
|
processor = EffectProcessor("1,2.5,test", {})
|
||||||
|
result = processor.parse_params()
|
||||||
|
assert result == ["1", "2.5", "test"]
|
||||||
|
|
||||||
|
def test_get_pos_json_empty(self):
|
||||||
|
"""测试获取空位置JSON"""
|
||||||
|
processor = EffectProcessor("", {})
|
||||||
|
result = processor.get_pos_json()
|
||||||
|
assert result == {}
|
||||||
|
|
||||||
|
def test_get_pos_json_valid(self):
|
||||||
|
"""测试获取有效位置JSON"""
|
||||||
|
ext_data = {
|
||||||
|
"posJson": '{"ltX": 100, "ltY": 200, "rbX": 300, "rbY": 400}'
|
||||||
|
}
|
||||||
|
processor = EffectProcessor("", ext_data)
|
||||||
|
result = processor.get_pos_json()
|
||||||
|
|
||||||
|
expected = {"ltX": 100, "ltY": 200, "rbX": 300, "rbY": 400}
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
def test_get_pos_json_invalid(self):
|
||||||
|
"""测试获取无效位置JSON"""
|
||||||
|
ext_data = {"posJson": "invalid_json"}
|
||||||
|
processor = EffectProcessor("", ext_data)
|
||||||
|
result = processor.get_pos_json()
|
||||||
|
assert result == {}
|
||||||
|
|
||||||
|
def test_get_pos_json_default_value(self):
|
||||||
|
"""测试默认位置JSON值"""
|
||||||
|
ext_data = {"posJson": "{}"}
|
||||||
|
processor = EffectProcessor("", ext_data)
|
||||||
|
result = processor.get_pos_json()
|
||||||
|
assert result == {}
|
||||||
|
|
||||||
|
|
||||||
|
class TestEffectRegistry:
|
||||||
|
"""测试特效注册表"""
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
"""测试初始化"""
|
||||||
|
registry = EffectRegistry()
|
||||||
|
assert registry._processors == {}
|
||||||
|
|
||||||
|
def test_register_valid_processor(self):
|
||||||
|
"""测试注册有效处理器"""
|
||||||
|
registry = EffectRegistry()
|
||||||
|
|
||||||
|
class TestEffect(EffectProcessor):
|
||||||
|
def validate_params(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def generate_filter_args(self, video_input, effect_index):
|
||||||
|
return [], video_input
|
||||||
|
|
||||||
|
def get_effect_name(self):
|
||||||
|
return "test"
|
||||||
|
|
||||||
|
registry.register("test_effect", TestEffect)
|
||||||
|
assert "test_effect" in registry._processors
|
||||||
|
assert registry._processors["test_effect"] == TestEffect
|
||||||
|
|
||||||
|
def test_register_invalid_processor(self):
|
||||||
|
"""测试注册无效处理器"""
|
||||||
|
registry = EffectRegistry()
|
||||||
|
|
||||||
|
class InvalidEffect:
|
||||||
|
pass
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="must be a subclass of EffectProcessor"):
|
||||||
|
registry.register("invalid", InvalidEffect)
|
||||||
|
|
||||||
|
def test_get_processor_exists(self):
|
||||||
|
"""测试获取存在的处理器"""
|
||||||
|
registry = EffectRegistry()
|
||||||
|
|
||||||
|
class TestEffect(EffectProcessor):
|
||||||
|
def validate_params(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def generate_filter_args(self, video_input, effect_index):
|
||||||
|
return [], video_input
|
||||||
|
|
||||||
|
def get_effect_name(self):
|
||||||
|
return "test"
|
||||||
|
|
||||||
|
registry.register("test_effect", TestEffect)
|
||||||
|
processor = registry.get_processor("test_effect", "params", {"data": "value"})
|
||||||
|
|
||||||
|
assert isinstance(processor, TestEffect)
|
||||||
|
assert processor.params == "params"
|
||||||
|
assert processor.ext_data == {"data": "value"}
|
||||||
|
|
||||||
|
def test_get_processor_not_exists(self):
|
||||||
|
"""测试获取不存在的处理器"""
|
||||||
|
registry = EffectRegistry()
|
||||||
|
processor = registry.get_processor("nonexistent")
|
||||||
|
assert processor is None
|
||||||
|
|
||||||
|
def test_list_effects_empty(self):
|
||||||
|
"""测试列出空特效"""
|
||||||
|
registry = EffectRegistry()
|
||||||
|
effects = registry.list_effects()
|
||||||
|
assert effects == []
|
||||||
|
|
||||||
|
def test_list_effects_with_processors(self):
|
||||||
|
"""测试列出已注册特效"""
|
||||||
|
registry = EffectRegistry()
|
||||||
|
|
||||||
|
class TestEffect(EffectProcessor):
|
||||||
|
def validate_params(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def generate_filter_args(self, video_input, effect_index):
|
||||||
|
return [], video_input
|
||||||
|
|
||||||
|
def get_effect_name(self):
|
||||||
|
return "test"
|
||||||
|
|
||||||
|
registry.register("effect1", TestEffect)
|
||||||
|
registry.register("effect2", TestEffect)
|
||||||
|
|
||||||
|
effects = registry.list_effects()
|
||||||
|
assert set(effects) == {"effect1", "effect2"}
|
||||||
|
|
||||||
|
def test_parse_effect_string_with_params(self):
|
||||||
|
"""测试解析带参数的特效字符串"""
|
||||||
|
registry = EffectRegistry()
|
||||||
|
name, params = registry.parse_effect_string("zoom:0,2.0,3.0")
|
||||||
|
|
||||||
|
assert name == "zoom"
|
||||||
|
assert params == "0,2.0,3.0"
|
||||||
|
|
||||||
|
def test_parse_effect_string_without_params(self):
|
||||||
|
"""测试解析无参数的特效字符串"""
|
||||||
|
registry = EffectRegistry()
|
||||||
|
name, params = registry.parse_effect_string("zoom")
|
||||||
|
|
||||||
|
assert name == "zoom"
|
||||||
|
assert params == ""
|
||||||
|
|
||||||
|
def test_parse_effect_string_multiple_colons(self):
|
||||||
|
"""测试解析多个冒号的特效字符串"""
|
||||||
|
registry = EffectRegistry()
|
||||||
|
name, params = registry.parse_effect_string("effect:param1:param2")
|
||||||
|
|
||||||
|
assert name == "effect"
|
||||||
|
assert params == "param1:param2"
|
||||||
169
tests/test_effects/test_speed_effect.py
Normal file
169
tests/test_effects/test_speed_effect.py
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
"""测试变速特效"""
|
||||||
|
import pytest
|
||||||
|
from entity.effects.speed import SpeedEffect
|
||||||
|
from tests.utils.test_helpers import EffectTestHelper
|
||||||
|
|
||||||
|
|
||||||
|
class TestSpeedEffect:
|
||||||
|
"""测试变速特效处理器"""
|
||||||
|
|
||||||
|
def test_validate_params_valid_cases(self):
|
||||||
|
"""测试有效参数验证"""
|
||||||
|
test_cases = [
|
||||||
|
{
|
||||||
|
"params": "2.0",
|
||||||
|
"expected": True,
|
||||||
|
"description": "2倍速"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": "0.5",
|
||||||
|
"expected": True,
|
||||||
|
"description": "0.5倍速(慢速)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": "1.0",
|
||||||
|
"expected": True,
|
||||||
|
"description": "正常速度"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": "10.0",
|
||||||
|
"expected": True,
|
||||||
|
"description": "10倍速"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": "",
|
||||||
|
"expected": True,
|
||||||
|
"description": "空参数(默认不变速)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
effect = SpeedEffect()
|
||||||
|
results = EffectTestHelper.test_effect_params_validation(effect, test_cases)
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
assert result["passed"], f"Failed case: {result['description']} - {result}"
|
||||||
|
|
||||||
|
def test_validate_params_invalid_cases(self):
|
||||||
|
"""测试无效参数验证"""
|
||||||
|
test_cases = [
|
||||||
|
{
|
||||||
|
"params": "0",
|
||||||
|
"expected": False,
|
||||||
|
"description": "零速度"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": "-1.0",
|
||||||
|
"expected": False,
|
||||||
|
"description": "负速度"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": "abc",
|
||||||
|
"expected": False,
|
||||||
|
"description": "非数字参数"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": "1.0,2.0",
|
||||||
|
"expected": False,
|
||||||
|
"description": "多余参数"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
effect = SpeedEffect()
|
||||||
|
results = EffectTestHelper.test_effect_params_validation(effect, test_cases)
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
assert result["passed"], f"Failed case: {result['description']} - {result}"
|
||||||
|
|
||||||
|
def test_generate_filter_args_speed_change(self):
|
||||||
|
"""测试变速滤镜生成"""
|
||||||
|
effect = SpeedEffect("2.0") # 2倍速
|
||||||
|
result = EffectTestHelper.test_filter_generation(effect, "[0:v]", 1)
|
||||||
|
|
||||||
|
assert result["success"], f"Filter generation failed: {result.get('error')}"
|
||||||
|
assert result["filter_count"] == 1
|
||||||
|
assert result["valid_syntax"]
|
||||||
|
assert result["output_stream"] == "[v_eff1]"
|
||||||
|
|
||||||
|
# 检查滤镜内容
|
||||||
|
filter_str = result["filters"][0]
|
||||||
|
assert "setpts=2.0*PTS" in filter_str
|
||||||
|
assert "[0:v]" in filter_str
|
||||||
|
assert "[v_eff1]" in filter_str
|
||||||
|
|
||||||
|
def test_generate_filter_args_slow_motion(self):
|
||||||
|
"""测试慢动作滤镜生成"""
|
||||||
|
effect = SpeedEffect("0.5") # 0.5倍速(慢动作)
|
||||||
|
result = EffectTestHelper.test_filter_generation(effect, "[input]", 3)
|
||||||
|
|
||||||
|
assert result["success"]
|
||||||
|
assert result["filter_count"] == 1
|
||||||
|
assert result["valid_syntax"]
|
||||||
|
assert result["output_stream"] == "[v_eff3]"
|
||||||
|
|
||||||
|
filter_str = result["filters"][0]
|
||||||
|
assert "setpts=0.5*PTS" in filter_str
|
||||||
|
assert "[input]" in filter_str
|
||||||
|
assert "[v_eff3]" in filter_str
|
||||||
|
|
||||||
|
def test_generate_filter_args_no_change(self):
|
||||||
|
"""测试无变速效果"""
|
||||||
|
test_cases = [
|
||||||
|
{"params": "", "description": "空参数"},
|
||||||
|
{"params": "1", "description": "1倍速"},
|
||||||
|
{"params": "1.0", "description": "1.0倍速"}
|
||||||
|
]
|
||||||
|
|
||||||
|
for case in test_cases:
|
||||||
|
effect = SpeedEffect(case["params"])
|
||||||
|
result = EffectTestHelper.test_filter_generation(effect, "[0:v]", 1)
|
||||||
|
|
||||||
|
assert result["success"], f"Failed for {case['description']}"
|
||||||
|
assert result["filter_count"] == 0, f"Should not generate filter for {case['description']}"
|
||||||
|
assert result["output_stream"] == "[0:v]", f"Output should equal input for {case['description']}"
|
||||||
|
|
||||||
|
def test_generate_filter_args_invalid_params(self):
|
||||||
|
"""测试无效参数的滤镜生成"""
|
||||||
|
effect = SpeedEffect("invalid")
|
||||||
|
result = EffectTestHelper.test_filter_generation(effect, "[0:v]", 1)
|
||||||
|
|
||||||
|
assert result["success"]
|
||||||
|
assert result["filter_count"] == 0
|
||||||
|
assert result["output_stream"] == "[0:v]"
|
||||||
|
|
||||||
|
def test_get_effect_name(self):
|
||||||
|
"""测试获取特效名称"""
|
||||||
|
effect = SpeedEffect()
|
||||||
|
assert effect.get_effect_name() == "ospeed"
|
||||||
|
|
||||||
|
def test_various_speed_factors(self):
|
||||||
|
"""测试各种速度因子"""
|
||||||
|
speed_factors = ["0.1", "0.25", "0.75", "1.5", "3.0", "5.0"]
|
||||||
|
|
||||||
|
for speed in speed_factors:
|
||||||
|
effect = SpeedEffect(speed)
|
||||||
|
result = EffectTestHelper.test_filter_generation(effect, "[test]", 10)
|
||||||
|
|
||||||
|
if speed == "1.0":
|
||||||
|
# 1倍速不应该生成滤镜
|
||||||
|
assert result["filter_count"] == 0
|
||||||
|
assert result["output_stream"] == "[test]"
|
||||||
|
else:
|
||||||
|
# 其他速度应该生成滤镜
|
||||||
|
assert result["success"], f"Failed for speed {speed}"
|
||||||
|
assert result["filter_count"] == 1
|
||||||
|
assert result["valid_syntax"]
|
||||||
|
assert f"setpts={speed}*PTS" in result["filters"][0]
|
||||||
|
|
||||||
|
def test_effect_chaining(self):
|
||||||
|
"""测试特效链式处理"""
|
||||||
|
# 模拟在特效链中的使用
|
||||||
|
effect = SpeedEffect("2.0")
|
||||||
|
|
||||||
|
# 第一个特效
|
||||||
|
result1 = EffectTestHelper.test_filter_generation(effect, "[0:v]", 1)
|
||||||
|
assert result1["output_stream"] == "[v_eff1]"
|
||||||
|
|
||||||
|
# 作为链中的第二个特效
|
||||||
|
result2 = EffectTestHelper.test_filter_generation(effect, "[v_eff1]", 2)
|
||||||
|
assert result2["output_stream"] == "[v_eff2]"
|
||||||
|
assert "[v_eff1]" in result2["filters"][0]
|
||||||
194
tests/test_effects/test_zoom_effect.py
Normal file
194
tests/test_effects/test_zoom_effect.py
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
"""测试缩放特效"""
|
||||||
|
import pytest
|
||||||
|
from entity.effects.zoom import ZoomEffect
|
||||||
|
from tests.utils.test_helpers import EffectTestHelper, FFmpegValidator
|
||||||
|
|
||||||
|
|
||||||
|
class TestZoomEffect:
|
||||||
|
"""测试缩放特效处理器"""
|
||||||
|
|
||||||
|
def test_validate_params_valid_cases(self):
|
||||||
|
"""测试有效参数验证"""
|
||||||
|
test_cases = [
|
||||||
|
{
|
||||||
|
"params": "0,2.0,3.0",
|
||||||
|
"expected": True,
|
||||||
|
"description": "标准缩放参数"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": "1.5,1.5,0",
|
||||||
|
"expected": True,
|
||||||
|
"description": "静态缩放(duration=0)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": "0,1.0,5.0",
|
||||||
|
"expected": True,
|
||||||
|
"description": "无缩放效果(factor=1.0)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": "10,0.5,2.0",
|
||||||
|
"expected": True,
|
||||||
|
"description": "缩小效果"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
effect = ZoomEffect()
|
||||||
|
results = EffectTestHelper.test_effect_params_validation(effect, test_cases)
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
assert result["passed"], f"Failed case: {result['description']} - {result}"
|
||||||
|
|
||||||
|
def test_validate_params_invalid_cases(self):
|
||||||
|
"""测试无效参数验证"""
|
||||||
|
test_cases = [
|
||||||
|
{
|
||||||
|
"params": "",
|
||||||
|
"expected": False,
|
||||||
|
"description": "空参数"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": "1,2",
|
||||||
|
"expected": False,
|
||||||
|
"description": "参数不足"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": "-1,2.0,3.0",
|
||||||
|
"expected": False,
|
||||||
|
"description": "负开始时间"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": "0,0,3.0",
|
||||||
|
"expected": False,
|
||||||
|
"description": "零缩放因子"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": "0,-2.0,3.0",
|
||||||
|
"expected": False,
|
||||||
|
"description": "负缩放因子"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": "0,2.0,-1.0",
|
||||||
|
"expected": False,
|
||||||
|
"description": "负持续时间"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": "abc,2.0,3.0",
|
||||||
|
"expected": False,
|
||||||
|
"description": "非数字参数"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
effect = ZoomEffect()
|
||||||
|
results = EffectTestHelper.test_effect_params_validation(effect, test_cases)
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
assert result["passed"], f"Failed case: {result['description']} - {result}"
|
||||||
|
|
||||||
|
def test_generate_filter_args_static_zoom(self):
|
||||||
|
"""测试静态缩放滤镜生成"""
|
||||||
|
effect = ZoomEffect("0,2.0,0") # duration=0表示静态缩放
|
||||||
|
result = EffectTestHelper.test_filter_generation(effect, "[0:v]", 1)
|
||||||
|
|
||||||
|
assert result["success"], f"Filter generation failed: {result.get('error')}"
|
||||||
|
assert result["filter_count"] == 1
|
||||||
|
assert result["valid_syntax"]
|
||||||
|
assert result["output_stream"] == "[v_eff1]"
|
||||||
|
|
||||||
|
# 检查滤镜内容
|
||||||
|
filter_str = result["filters"][0]
|
||||||
|
assert "trim=start=0" in filter_str
|
||||||
|
assert "zoompan=z=2.0" in filter_str
|
||||||
|
assert "[v_eff1]" in filter_str
|
||||||
|
|
||||||
|
def test_generate_filter_args_dynamic_zoom(self):
|
||||||
|
"""测试动态缩放滤镜生成"""
|
||||||
|
effect = ZoomEffect("1.0,1.5,2.0") # 从1秒开始,持续2秒的1.5倍缩放
|
||||||
|
result = EffectTestHelper.test_filter_generation(effect, "[0:v]", 2)
|
||||||
|
|
||||||
|
assert result["success"], f"Filter generation failed: {result.get('error')}"
|
||||||
|
assert result["filter_count"] == 1
|
||||||
|
assert result["valid_syntax"]
|
||||||
|
assert result["output_stream"] == "[v_eff2]"
|
||||||
|
|
||||||
|
# 检查滤镜内容
|
||||||
|
filter_str = result["filters"][0]
|
||||||
|
assert "zoompan=z=" in filter_str
|
||||||
|
assert "between(t\\\\,1.0\\\\,3.0)" in filter_str # 检查时间范围
|
||||||
|
assert "[v_eff2]" in filter_str
|
||||||
|
|
||||||
|
def test_generate_filter_args_no_zoom(self):
|
||||||
|
"""测试无缩放效果(factor=1.0)"""
|
||||||
|
effect = ZoomEffect("0,1.0,3.0") # 缩放因子为1.0,应该不生成滤镜
|
||||||
|
result = EffectTestHelper.test_filter_generation(effect, "[0:v]", 1)
|
||||||
|
|
||||||
|
assert result["success"]
|
||||||
|
assert result["filter_count"] == 0
|
||||||
|
assert result["output_stream"] == "[0:v]" # 输出应该等于输入
|
||||||
|
|
||||||
|
def test_generate_filter_args_invalid_params(self):
|
||||||
|
"""测试无效参数的滤镜生成"""
|
||||||
|
effect = ZoomEffect("invalid,params")
|
||||||
|
result = EffectTestHelper.test_filter_generation(effect, "[0:v]", 1)
|
||||||
|
|
||||||
|
assert result["success"]
|
||||||
|
assert result["filter_count"] == 0
|
||||||
|
assert result["output_stream"] == "[0:v]" # 无效参数时应该返回原输入
|
||||||
|
|
||||||
|
def test_get_zoom_center_default(self):
|
||||||
|
"""测试默认缩放中心点"""
|
||||||
|
effect = ZoomEffect("0,2.0,3.0")
|
||||||
|
center_x, center_y = effect._get_zoom_center()
|
||||||
|
|
||||||
|
assert center_x == "iw/2"
|
||||||
|
assert center_y == "ih/2"
|
||||||
|
|
||||||
|
def test_get_zoom_center_with_pos_json(self):
|
||||||
|
"""测试基于posJson的缩放中心点"""
|
||||||
|
ext_data = {
|
||||||
|
"posJson": '{"ltX": 100, "ltY": 100, "rbX": 200, "rbY": 200, "imgWidth": 400, "imgHeight": 300}'
|
||||||
|
}
|
||||||
|
effect = ZoomEffect("0,2.0,3.0", ext_data)
|
||||||
|
center_x, center_y = effect._get_zoom_center()
|
||||||
|
|
||||||
|
# 中心点应该是矩形的中心
|
||||||
|
# center_x_ratio = (100 + 200) / (2 * 400) = 0.375
|
||||||
|
# center_y_ratio = (100 + 200) / (2 * 300) = 0.5
|
||||||
|
assert "iw*0.375" in center_x
|
||||||
|
assert "ih*0.5" in center_y
|
||||||
|
|
||||||
|
def test_get_zoom_center_invalid_pos_json(self):
|
||||||
|
"""测试无效posJson时的缩放中心点"""
|
||||||
|
ext_data = {
|
||||||
|
"posJson": '{"imgWidth": 0, "imgHeight": 0}' # 无效尺寸
|
||||||
|
}
|
||||||
|
effect = ZoomEffect("0,2.0,3.0", ext_data)
|
||||||
|
center_x, center_y = effect._get_zoom_center()
|
||||||
|
|
||||||
|
# 应该回退到默认中心点
|
||||||
|
assert center_x == "iw/2"
|
||||||
|
assert center_y == "ih/2"
|
||||||
|
|
||||||
|
def test_get_effect_name(self):
|
||||||
|
"""测试获取特效名称"""
|
||||||
|
effect = ZoomEffect()
|
||||||
|
assert effect.get_effect_name() == "zoom"
|
||||||
|
|
||||||
|
def test_complex_zoom_scenario(self):
|
||||||
|
"""测试复杂缩放场景"""
|
||||||
|
# 带有复杂posJson数据的动态缩放
|
||||||
|
ext_data = {
|
||||||
|
"posJson": '{"ltX": 50, "ltY": 75, "rbX": 350, "rbY": 225, "imgWidth": 400, "imgHeight": 300}'
|
||||||
|
}
|
||||||
|
effect = ZoomEffect("2.5,3.0,1.5", ext_data)
|
||||||
|
result = EffectTestHelper.test_filter_generation(effect, "[input]", 5)
|
||||||
|
|
||||||
|
assert result["success"]
|
||||||
|
assert result["filter_count"] == 1
|
||||||
|
assert result["valid_syntax"]
|
||||||
|
assert result["output_stream"] == "[v_eff5]"
|
||||||
|
|
||||||
|
filter_str = result["filters"][0]
|
||||||
|
assert "zoompan=z=" in filter_str
|
||||||
|
assert "between(t\\\\,2.5\\\\,4.0)" in filter_str # 2.5 + 1.5 = 4.0
|
||||||
|
assert "[input]" in filter_str
|
||||||
|
assert "[v_eff5]" in filter_str
|
||||||
244
tests/test_ffmpeg_builder/test_ffmpeg_command_builder.py
Normal file
244
tests/test_ffmpeg_builder/test_ffmpeg_command_builder.py
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
"""测试FFmpeg命令构建器"""
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
from entity.ffmpeg_command_builder import FFmpegCommandBuilder
|
||||||
|
from entity.render_task import RenderTask
|
||||||
|
from config.settings import Config
|
||||||
|
from tests.utils.test_helpers import MockRenderTask, FFmpegValidator
|
||||||
|
|
||||||
|
|
||||||
|
class TestFFmpegCommandBuilder:
|
||||||
|
"""测试FFmpeg命令构建器"""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config(self):
|
||||||
|
"""模拟配置"""
|
||||||
|
return Config(
|
||||||
|
encoder_args="-c:v h264",
|
||||||
|
video_args="-preset fast",
|
||||||
|
template_dir="tests/test_data/templates",
|
||||||
|
api_endpoint="http://test.local",
|
||||||
|
access_key="test_key"
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def simple_task(self):
|
||||||
|
"""简单的渲染任务"""
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.input_files = ["input1.mp4", "input2.mp4"]
|
||||||
|
task.output_path = "output.mp4"
|
||||||
|
task.effects = []
|
||||||
|
return task
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def task_with_effects(self):
|
||||||
|
"""带特效的渲染任务"""
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.input_files = ["input.mp4"]
|
||||||
|
task.output_path = "output.mp4"
|
||||||
|
task.effects = ["zoom:0,2.0,3.0", "ospeed:1.5"]
|
||||||
|
task.ext_data = {
|
||||||
|
"posJson": '{"ltX": 100, "ltY": 100, "rbX": 200, "rbY": 200, "imgWidth": 300, "imgHeight": 300}'
|
||||||
|
}
|
||||||
|
return task
|
||||||
|
|
||||||
|
def test_init(self, simple_task, mock_config):
|
||||||
|
"""测试初始化"""
|
||||||
|
builder = FFmpegCommandBuilder(simple_task, mock_config)
|
||||||
|
assert builder.task == simple_task
|
||||||
|
assert builder.config == mock_config
|
||||||
|
|
||||||
|
def test_build_copy_command(self, simple_task, mock_config):
|
||||||
|
"""测试构建复制命令"""
|
||||||
|
builder = FFmpegCommandBuilder(simple_task, mock_config)
|
||||||
|
command = builder._build_copy_command()
|
||||||
|
|
||||||
|
# 验证命令结构
|
||||||
|
validation = FFmpegValidator.validate_ffmpeg_command(command)
|
||||||
|
assert validation["valid"], f"Invalid command: {validation['errors']}"
|
||||||
|
assert validation["has_input"]
|
||||||
|
assert validation["has_output"]
|
||||||
|
|
||||||
|
# 验证具体内容
|
||||||
|
assert command[0] == "ffmpeg"
|
||||||
|
assert "-i" in command
|
||||||
|
assert "input1.mp4" in command
|
||||||
|
assert "output.mp4" in command
|
||||||
|
assert "-c" in command and "copy" in command
|
||||||
|
|
||||||
|
def test_build_concat_command_multiple_files(self, mock_config):
|
||||||
|
"""测试构建多文件拼接命令"""
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.input_files = ["file1.mp4", "file2.mp4", "file3.mp4"]
|
||||||
|
task.output_path = "concat_output.mp4"
|
||||||
|
|
||||||
|
builder = FFmpegCommandBuilder(task, mock_config)
|
||||||
|
command = builder._build_concat_command()
|
||||||
|
|
||||||
|
validation = FFmpegValidator.validate_ffmpeg_command(command)
|
||||||
|
assert validation["valid"]
|
||||||
|
assert validation["has_filter"]
|
||||||
|
|
||||||
|
# 验证拼接相关参数
|
||||||
|
assert "concat=n=3:v=1:a=1" in " ".join(command)
|
||||||
|
assert all(f"file{i}.mp4" in command for i in range(1, 4))
|
||||||
|
|
||||||
|
def test_build_encode_command_with_effects(self, task_with_effects, mock_config):
|
||||||
|
"""测试构建带特效的编码命令"""
|
||||||
|
builder = FFmpegCommandBuilder(task_with_effects, mock_config)
|
||||||
|
command = builder._build_encode_command()
|
||||||
|
|
||||||
|
validation = FFmpegValidator.validate_ffmpeg_command(command)
|
||||||
|
assert validation["valid"]
|
||||||
|
assert validation["has_filter"]
|
||||||
|
|
||||||
|
command_str = " ".join(command)
|
||||||
|
# 应该包含特效相关的滤镜
|
||||||
|
assert "-filter_complex" in command
|
||||||
|
# 应该包含编码参数
|
||||||
|
assert "-c:v" in command and "h264" in command
|
||||||
|
|
||||||
|
def test_add_effects_single_effect(self, mock_config):
|
||||||
|
"""测试添加单个特效"""
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.effects = ["zoom:0,2.0,3.0"]
|
||||||
|
task.ext_data = {"posJson": "{}"}
|
||||||
|
|
||||||
|
builder = FFmpegCommandBuilder(task, mock_config)
|
||||||
|
filter_args = []
|
||||||
|
|
||||||
|
result_input, result_index = builder._add_effects(filter_args, "[0:v]", 1)
|
||||||
|
|
||||||
|
# 验证结果
|
||||||
|
assert len(filter_args) > 0 # 应该有滤镜被添加
|
||||||
|
assert result_input == "[v_eff1]" # 输出流应该更新
|
||||||
|
assert result_index == 2 # 索引应该递增
|
||||||
|
|
||||||
|
def test_add_effects_multiple_effects(self, mock_config):
|
||||||
|
"""测试添加多个特效"""
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.effects = ["zoom:0,2.0,3.0", "ospeed:1.5"]
|
||||||
|
task.ext_data = {"posJson": "{}"}
|
||||||
|
|
||||||
|
builder = FFmpegCommandBuilder(task, mock_config)
|
||||||
|
filter_args = []
|
||||||
|
|
||||||
|
result_input, result_index = builder._add_effects(filter_args, "[0:v]", 1)
|
||||||
|
|
||||||
|
# 验证特效链
|
||||||
|
assert len(filter_args) >= 2 # 应该有两个特效的滤镜
|
||||||
|
assert result_input == "[v_eff2]" # 最终输出流
|
||||||
|
assert result_index == 3 # 索引应该递增两次
|
||||||
|
|
||||||
|
def test_add_effects_invalid_effect(self, mock_config):
|
||||||
|
"""测试添加无效特效"""
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.effects = ["invalid_effect:params"]
|
||||||
|
|
||||||
|
builder = FFmpegCommandBuilder(task, mock_config)
|
||||||
|
filter_args = []
|
||||||
|
|
||||||
|
result_input, result_index = builder._add_effects(filter_args, "[0:v]", 1)
|
||||||
|
|
||||||
|
# 无效特效应该被忽略
|
||||||
|
assert result_input == "[0:v]" # 输入流不变
|
||||||
|
assert result_index == 1 # 索引不变
|
||||||
|
|
||||||
|
def test_add_effects_no_effects(self, mock_config):
|
||||||
|
"""测试无特效情况"""
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.effects = []
|
||||||
|
|
||||||
|
builder = FFmpegCommandBuilder(task, mock_config)
|
||||||
|
filter_args = []
|
||||||
|
|
||||||
|
result_input, result_index = builder._add_effects(filter_args, "[0:v]", 1)
|
||||||
|
|
||||||
|
assert result_input == "[0:v]" # 输入流不变
|
||||||
|
assert result_index == 1 # 索引不变
|
||||||
|
assert len(filter_args) == 0 # 无滤镜添加
|
||||||
|
|
||||||
|
def test_build_command_copy_mode(self, simple_task, mock_config):
|
||||||
|
"""测试构建复制模式命令"""
|
||||||
|
simple_task.input_files = ["single_file.mp4"]
|
||||||
|
|
||||||
|
builder = FFmpegCommandBuilder(simple_task, mock_config)
|
||||||
|
command = builder.build_command()
|
||||||
|
|
||||||
|
validation = FFmpegValidator.validate_ffmpeg_command(command)
|
||||||
|
assert validation["valid"]
|
||||||
|
|
||||||
|
# 应该是复制模式
|
||||||
|
command_str = " ".join(command)
|
||||||
|
assert "-c copy" in command_str
|
||||||
|
|
||||||
|
def test_build_command_concat_mode(self, mock_config):
|
||||||
|
"""测试构建拼接模式命令"""
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.input_files = ["file1.mp4", "file2.mp4"]
|
||||||
|
task.effects = []
|
||||||
|
|
||||||
|
builder = FFmpegCommandBuilder(task, mock_config)
|
||||||
|
command = builder.build_command()
|
||||||
|
|
||||||
|
validation = FFmpegValidator.validate_ffmpeg_command(command)
|
||||||
|
assert validation["valid"]
|
||||||
|
assert validation["has_filter"]
|
||||||
|
|
||||||
|
# 应该包含拼接滤镜
|
||||||
|
command_str = " ".join(command)
|
||||||
|
assert "concat=" in command_str
|
||||||
|
|
||||||
|
def test_build_command_encode_mode(self, task_with_effects, mock_config):
|
||||||
|
"""测试构建编码模式命令"""
|
||||||
|
builder = FFmpegCommandBuilder(task_with_effects, mock_config)
|
||||||
|
command = builder.build_command()
|
||||||
|
|
||||||
|
validation = FFmpegValidator.validate_ffmpeg_command(command)
|
||||||
|
assert validation["valid"]
|
||||||
|
assert validation["has_filter"]
|
||||||
|
|
||||||
|
# 应该包含编码参数和特效滤镜
|
||||||
|
command_str = " ".join(command)
|
||||||
|
assert "-c:v h264" in command_str
|
||||||
|
|
||||||
|
@patch('entity.effects.registry.get_processor')
|
||||||
|
def test_effect_processor_integration(self, mock_get_processor, mock_config):
|
||||||
|
"""测试与特效处理器的集成"""
|
||||||
|
# 模拟特效处理器
|
||||||
|
mock_processor = Mock()
|
||||||
|
mock_processor.frame_rate = 25
|
||||||
|
mock_processor.generate_filter_args.return_value = (
|
||||||
|
["[0:v]zoompan=z=2.0:x=iw/2:y=ih/2:d=1[v_eff1]"],
|
||||||
|
"[v_eff1]"
|
||||||
|
)
|
||||||
|
mock_get_processor.return_value = mock_processor
|
||||||
|
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.effects = ["zoom:0,2.0,3.0"]
|
||||||
|
task.frame_rate = 30
|
||||||
|
|
||||||
|
builder = FFmpegCommandBuilder(task, mock_config)
|
||||||
|
filter_args = []
|
||||||
|
|
||||||
|
builder._add_effects(filter_args, "[0:v]", 1)
|
||||||
|
|
||||||
|
# 验证处理器被正确调用
|
||||||
|
mock_processor.generate_filter_args.assert_called_once_with("[0:v]", 1)
|
||||||
|
assert mock_processor.frame_rate == 30 # 帧率应该被设置
|
||||||
|
|
||||||
|
def test_error_handling_missing_input(self, mock_config):
|
||||||
|
"""测试缺少输入文件的错误处理"""
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.input_files = []
|
||||||
|
|
||||||
|
builder = FFmpegCommandBuilder(task, mock_config)
|
||||||
|
|
||||||
|
# 构建命令时应该处理错误情况
|
||||||
|
# 具体的错误处理依赖于实现
|
||||||
|
command = builder.build_command()
|
||||||
|
|
||||||
|
# 验证至少返回了基本的ffmpeg命令结构
|
||||||
|
assert isinstance(command, list)
|
||||||
|
assert len(command) > 0
|
||||||
282
tests/test_integration/test_ffmpeg_execution.py
Normal file
282
tests/test_integration/test_ffmpeg_execution.py
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
"""FFmpeg执行集成测试"""
|
||||||
|
import pytest
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from entity.ffmpeg_command_builder import FFmpegCommandBuilder
|
||||||
|
from entity.render_task import RenderTask
|
||||||
|
from config.settings import Config
|
||||||
|
from services.render_service import DefaultRenderService
|
||||||
|
from tests.utils.test_helpers import MockRenderTask, create_test_video_file
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
class TestFFmpegExecution:
|
||||||
|
"""FFmpeg执行集成测试"""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_config(self, temp_dir):
|
||||||
|
"""测试配置"""
|
||||||
|
return Config(
|
||||||
|
encoder_args="-c:v libx264",
|
||||||
|
video_args="-preset ultrafast -crf 23",
|
||||||
|
template_dir=temp_dir,
|
||||||
|
api_endpoint="http://test.local",
|
||||||
|
access_key="test_key"
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_video(self, temp_dir):
|
||||||
|
"""创建测试视频文件"""
|
||||||
|
video_path = os.path.join(temp_dir, "test_input.mp4")
|
||||||
|
success = create_test_video_file(video_path, duration=3, resolution="320x240")
|
||||||
|
if not success:
|
||||||
|
pytest.skip("Cannot create test video file")
|
||||||
|
return video_path
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def render_service(self, test_config):
|
||||||
|
"""渲染服务实例"""
|
||||||
|
return DefaultRenderService()
|
||||||
|
|
||||||
|
def test_simple_copy_execution(self, sample_video, temp_dir, test_config):
|
||||||
|
"""测试简单复制执行"""
|
||||||
|
output_path = os.path.join(temp_dir, "copy_output.mp4")
|
||||||
|
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.input_files = [sample_video]
|
||||||
|
task.output_path = output_path
|
||||||
|
task.effects = []
|
||||||
|
|
||||||
|
builder = FFmpegCommandBuilder(task, test_config)
|
||||||
|
command = builder.build_command()
|
||||||
|
|
||||||
|
# 执行命令
|
||||||
|
try:
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True, timeout=30)
|
||||||
|
assert result.returncode == 0, f"FFmpeg failed: {result.stderr}"
|
||||||
|
assert os.path.exists(output_path), "Output file was not created"
|
||||||
|
assert os.path.getsize(output_path) > 0, "Output file is empty"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pytest.fail("FFmpeg execution timed out")
|
||||||
|
|
||||||
|
def test_zoom_effect_execution(self, sample_video, temp_dir, test_config):
|
||||||
|
"""测试缩放特效执行"""
|
||||||
|
output_path = os.path.join(temp_dir, "zoom_output.mp4")
|
||||||
|
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.input_files = [sample_video]
|
||||||
|
task.output_path = output_path
|
||||||
|
task.effects = ["zoom:0,2.0,2.0"] # 2倍缩放,持续2秒
|
||||||
|
task.ext_data = {"posJson": "{}"}
|
||||||
|
|
||||||
|
builder = FFmpegCommandBuilder(task, test_config)
|
||||||
|
command = builder.build_command()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
||||||
|
assert result.returncode == 0, f"FFmpeg failed: {result.stderr}"
|
||||||
|
assert os.path.exists(output_path), "Output file was not created"
|
||||||
|
assert os.path.getsize(output_path) > 0, "Output file is empty"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pytest.fail("FFmpeg execution timed out")
|
||||||
|
|
||||||
|
def test_speed_effect_execution(self, sample_video, temp_dir, test_config):
|
||||||
|
"""测试变速特效执行"""
|
||||||
|
output_path = os.path.join(temp_dir, "speed_output.mp4")
|
||||||
|
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.input_files = [sample_video]
|
||||||
|
task.output_path = output_path
|
||||||
|
task.effects = ["ospeed:2.0"] # 2倍速
|
||||||
|
task.ext_data = {}
|
||||||
|
|
||||||
|
builder = FFmpegCommandBuilder(task, test_config)
|
||||||
|
command = builder.build_command()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
||||||
|
assert result.returncode == 0, f"FFmpeg failed: {result.stderr}"
|
||||||
|
assert os.path.exists(output_path), "Output file was not created"
|
||||||
|
assert os.path.getsize(output_path) > 0, "Output file is empty"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pytest.fail("FFmpeg execution timed out")
|
||||||
|
|
||||||
|
def test_multiple_effects_execution(self, sample_video, temp_dir, test_config):
|
||||||
|
"""测试多特效组合执行"""
|
||||||
|
output_path = os.path.join(temp_dir, "multi_effects_output.mp4")
|
||||||
|
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.input_files = [sample_video]
|
||||||
|
task.output_path = output_path
|
||||||
|
task.effects = ["zoom:0,1.5,1.0", "ospeed:1.5"] # 缩放+变速
|
||||||
|
task.ext_data = {"posJson": "{}"}
|
||||||
|
|
||||||
|
builder = FFmpegCommandBuilder(task, test_config)
|
||||||
|
command = builder.build_command()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
||||||
|
assert result.returncode == 0, f"FFmpeg failed: {result.stderr}"
|
||||||
|
assert os.path.exists(output_path), "Output file was not created"
|
||||||
|
assert os.path.getsize(output_path) > 0, "Output file is empty"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pytest.fail("FFmpeg execution timed out")
|
||||||
|
|
||||||
|
def test_concat_execution(self, temp_dir, test_config):
|
||||||
|
"""测试视频拼接执行"""
|
||||||
|
# 创建两个测试视频
|
||||||
|
video1_path = os.path.join(temp_dir, "video1.mp4")
|
||||||
|
video2_path = os.path.join(temp_dir, "video2.mp4")
|
||||||
|
output_path = os.path.join(temp_dir, "concat_output.mp4")
|
||||||
|
|
||||||
|
success1 = create_test_video_file(video1_path, duration=2, resolution="320x240")
|
||||||
|
success2 = create_test_video_file(video2_path, duration=2, resolution="320x240")
|
||||||
|
|
||||||
|
if not (success1 and success2):
|
||||||
|
pytest.skip("Cannot create test video files")
|
||||||
|
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.input_files = [video1_path, video2_path]
|
||||||
|
task.output_path = output_path
|
||||||
|
task.effects = []
|
||||||
|
|
||||||
|
builder = FFmpegCommandBuilder(task, test_config)
|
||||||
|
command = builder.build_command()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
||||||
|
assert result.returncode == 0, f"FFmpeg failed: {result.stderr}"
|
||||||
|
assert os.path.exists(output_path), "Output file was not created"
|
||||||
|
assert os.path.getsize(output_path) > 0, "Output file is empty"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pytest.fail("FFmpeg execution timed out")
|
||||||
|
|
||||||
|
def test_invalid_effect_execution(self, sample_video, temp_dir, test_config):
|
||||||
|
"""测试无效特效的执行处理"""
|
||||||
|
output_path = os.path.join(temp_dir, "invalid_effect_output.mp4")
|
||||||
|
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.input_files = [sample_video]
|
||||||
|
task.output_path = output_path
|
||||||
|
task.effects = ["invalid_effect:params", "zoom:0,2.0,1.0"] # 混合有效和无效特效
|
||||||
|
task.ext_data = {"posJson": "{}"}
|
||||||
|
|
||||||
|
builder = FFmpegCommandBuilder(task, test_config)
|
||||||
|
command = builder.build_command()
|
||||||
|
|
||||||
|
# 应该忽略无效特效,继续处理有效特效
|
||||||
|
try:
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
||||||
|
assert result.returncode == 0, f"FFmpeg failed: {result.stderr}"
|
||||||
|
assert os.path.exists(output_path), "Output file was not created"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pytest.fail("FFmpeg execution timed out")
|
||||||
|
|
||||||
|
def test_render_service_integration(self, sample_video, temp_dir, test_config, render_service):
|
||||||
|
"""测试渲染服务集成"""
|
||||||
|
output_path = os.path.join(temp_dir, "service_output.mp4")
|
||||||
|
|
||||||
|
# 创建真实的RenderTask(不是Mock)
|
||||||
|
task_data = {
|
||||||
|
"task_id": "integration_test",
|
||||||
|
"template_id": "test_template",
|
||||||
|
"input_files": [sample_video],
|
||||||
|
"output_path": output_path,
|
||||||
|
"effects": ["zoom:0,1.8,2.0"],
|
||||||
|
"ext_data": {"posJson": "{}"},
|
||||||
|
"frame_rate": 25
|
||||||
|
}
|
||||||
|
|
||||||
|
# 这里需要根据实际的RenderTask构造方法调整
|
||||||
|
task = MockRenderTask(**task_data)
|
||||||
|
|
||||||
|
# 使用渲染服务执行
|
||||||
|
try:
|
||||||
|
# 这里的方法调用需要根据实际的渲染服务接口调整
|
||||||
|
# success = render_service.render(task, test_config)
|
||||||
|
# assert success, "Render service failed"
|
||||||
|
|
||||||
|
# 临时直接使用FFmpegCommandBuilder测试
|
||||||
|
builder = FFmpegCommandBuilder(task, test_config)
|
||||||
|
command = builder.build_command()
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
||||||
|
|
||||||
|
assert result.returncode == 0, f"Render failed: {result.stderr}"
|
||||||
|
assert os.path.exists(output_path), "Output file was not created"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Render service integration failed: {e}")
|
||||||
|
|
||||||
|
def test_error_handling_missing_input(self, temp_dir, test_config):
|
||||||
|
"""测试缺失输入文件的错误处理"""
|
||||||
|
missing_file = os.path.join(temp_dir, "nonexistent.mp4")
|
||||||
|
output_path = os.path.join(temp_dir, "error_output.mp4")
|
||||||
|
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.input_files = [missing_file]
|
||||||
|
task.output_path = output_path
|
||||||
|
task.effects = []
|
||||||
|
|
||||||
|
builder = FFmpegCommandBuilder(task, test_config)
|
||||||
|
command = builder.build_command()
|
||||||
|
|
||||||
|
# 应该失败,因为输入文件不存在
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True, timeout=30)
|
||||||
|
assert result.returncode != 0, "FFmpeg should fail with missing input file"
|
||||||
|
|
||||||
|
def test_performance_multiple_effects(self, sample_video, temp_dir, test_config):
|
||||||
|
"""测试多特效性能"""
|
||||||
|
output_path = os.path.join(temp_dir, "performance_output.mp4")
|
||||||
|
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.input_files = [sample_video]
|
||||||
|
task.output_path = output_path
|
||||||
|
# 多个特效组合
|
||||||
|
task.effects = [
|
||||||
|
"zoom:0,1.5,1.0",
|
||||||
|
"ospeed:1.2",
|
||||||
|
"zoom:1.5,2.0,1.0"
|
||||||
|
]
|
||||||
|
task.ext_data = {"posJson": "{}"}
|
||||||
|
|
||||||
|
builder = FFmpegCommandBuilder(task, test_config)
|
||||||
|
command = builder.build_command()
|
||||||
|
|
||||||
|
import time
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True, timeout=120)
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
|
||||||
|
assert result.returncode == 0, f"FFmpeg failed: {result.stderr}"
|
||||||
|
assert os.path.exists(output_path), "Output file was not created"
|
||||||
|
assert execution_time < 60, f"Execution took too long: {execution_time}s"
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pytest.fail("FFmpeg execution timed out")
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not os.environ.get('RUN_STRESS_TESTS'), reason="Stress tests disabled")
|
||||||
|
def test_stress_test_large_effects_chain(self, sample_video, temp_dir, test_config):
|
||||||
|
"""压力测试:大量特效链"""
|
||||||
|
output_path = os.path.join(temp_dir, "stress_output.mp4")
|
||||||
|
|
||||||
|
task = MockRenderTask()
|
||||||
|
task.input_files = [sample_video]
|
||||||
|
task.output_path = output_path
|
||||||
|
# 创建大量特效
|
||||||
|
task.effects = [f"zoom:{i*0.5},1.{i+5},{i*0.2+0.5}" for i in range(10)]
|
||||||
|
task.ext_data = {"posJson": "{}"}
|
||||||
|
|
||||||
|
builder = FFmpegCommandBuilder(task, test_config)
|
||||||
|
command = builder.build_command()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True, timeout=300)
|
||||||
|
assert result.returncode == 0, f"Stress test failed: {result.stderr}"
|
||||||
|
assert os.path.exists(output_path), "Output file was not created"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pytest.fail("Stress test timed out")
|
||||||
202
tests/utils/test_helpers.py
Normal file
202
tests/utils/test_helpers.py
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
"""测试辅助工具"""
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from entity.render_task import RenderTask
|
||||||
|
from entity.effects.base import EffectProcessor
|
||||||
|
from config.settings import Config
|
||||||
|
|
||||||
|
|
||||||
|
class MockRenderTask:
|
||||||
|
"""模拟渲染任务,用于测试"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
task_id: str = "test_task",
|
||||||
|
template_id: str = "test_template",
|
||||||
|
effects: List[str] = None,
|
||||||
|
ext_data: Dict[str, Any] = None,
|
||||||
|
frame_rate: int = 25,
|
||||||
|
output_path: str = "test_output.mp4"
|
||||||
|
):
|
||||||
|
self.task_id = task_id
|
||||||
|
self.template_id = template_id
|
||||||
|
self.effects = effects or []
|
||||||
|
self.ext_data = ext_data or {}
|
||||||
|
self.frame_rate = frame_rate
|
||||||
|
self.output_path = output_path
|
||||||
|
self.input_files = []
|
||||||
|
self.overlays = []
|
||||||
|
self.use_center_cut = False
|
||||||
|
self.use_zoom_cut = False
|
||||||
|
self.audio_file = None
|
||||||
|
|
||||||
|
|
||||||
|
class FFmpegValidator:
|
||||||
|
"""FFmpeg命令验证器"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_filter_syntax(filter_str: str) -> bool:
|
||||||
|
"""验证滤镜语法是否正确"""
|
||||||
|
try:
|
||||||
|
# 基本语法检查
|
||||||
|
if not filter_str:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查是否包含基本的滤镜结构
|
||||||
|
if '[' in filter_str and ']' in filter_str:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 检查常见的滤镜格式
|
||||||
|
common_filters = ['zoompan', 'setpts', 'trim', 'scale', 'crop']
|
||||||
|
return any(f in filter_str for f in common_filters)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_stream_identifier(stream_id: str) -> bool:
|
||||||
|
"""验证流标识符格式"""
|
||||||
|
if not stream_id:
|
||||||
|
return False
|
||||||
|
return stream_id.startswith('[') and stream_id.endswith(']')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_ffmpeg_command(command: List[str]) -> Dict[str, Any]:
|
||||||
|
"""验证完整的FFmpeg命令"""
|
||||||
|
result = {
|
||||||
|
"valid": False,
|
||||||
|
"has_input": False,
|
||||||
|
"has_output": False,
|
||||||
|
"has_filter": False,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
|
||||||
|
if not command or command[0] != "ffmpeg":
|
||||||
|
result["errors"].append("Command must start with 'ffmpeg'")
|
||||||
|
return result
|
||||||
|
|
||||||
|
# 检查输入文件
|
||||||
|
if "-i" in command:
|
||||||
|
result["has_input"] = True
|
||||||
|
else:
|
||||||
|
result["errors"].append("No input file specified")
|
||||||
|
|
||||||
|
# 检查输出文件
|
||||||
|
if len(command) > 1 and not command[-1].startswith("-"):
|
||||||
|
result["has_output"] = True
|
||||||
|
else:
|
||||||
|
result["errors"].append("No output file specified")
|
||||||
|
|
||||||
|
# 检查滤镜
|
||||||
|
if "-filter_complex" in command or "-vf" in command:
|
||||||
|
result["has_filter"] = True
|
||||||
|
|
||||||
|
result["valid"] = (
|
||||||
|
result["has_input"] and
|
||||||
|
result["has_output"] and
|
||||||
|
len(result["errors"]) == 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class EffectTestHelper:
|
||||||
|
"""特效测试辅助类"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_test_effect(effect_class, params: str = "", ext_data: Dict[str, Any] = None):
|
||||||
|
"""创建测试用特效实例"""
|
||||||
|
return effect_class(params, ext_data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_effect_params_validation(effect: EffectProcessor, test_cases: List[Dict[str, Any]]):
|
||||||
|
"""批量测试特效参数验证"""
|
||||||
|
results = []
|
||||||
|
for case in test_cases:
|
||||||
|
effect.params = case.get("params", "")
|
||||||
|
effect.ext_data = case.get("ext_data", {})
|
||||||
|
|
||||||
|
is_valid = effect.validate_params()
|
||||||
|
expected = case.get("expected", True)
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"params": effect.params,
|
||||||
|
"expected": expected,
|
||||||
|
"actual": is_valid,
|
||||||
|
"passed": is_valid == expected,
|
||||||
|
"description": case.get("description", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_filter_generation(effect: EffectProcessor, video_input: str = "[0:v]", effect_index: int = 1):
|
||||||
|
"""测试滤镜生成"""
|
||||||
|
try:
|
||||||
|
filters, output_stream = effect.generate_filter_args(video_input, effect_index)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"filters": filters,
|
||||||
|
"output_stream": output_stream,
|
||||||
|
"filter_count": len(filters),
|
||||||
|
"valid_syntax": all(FFmpegValidator.validate_filter_syntax(f) for f in filters),
|
||||||
|
"valid_output": FFmpegValidator.validate_stream_identifier(output_stream) if output_stream != video_input else True
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
result = {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"filters": [],
|
||||||
|
"output_stream": "",
|
||||||
|
"filter_count": 0,
|
||||||
|
"valid_syntax": False,
|
||||||
|
"valid_output": False
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def create_test_video_file(output_path: str, duration: int = 5, resolution: str = "640x480") -> bool:
|
||||||
|
"""创建测试用视频文件"""
|
||||||
|
try:
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg", "-y", # 覆盖输出文件
|
||||||
|
"-f", "lavfi", # 使用libavfilter输入
|
||||||
|
"-i", f"testsrc=duration={duration}:size={resolution}:rate=25",
|
||||||
|
"-c:v", "libx264",
|
||||||
|
"-preset", "ultrafast",
|
||||||
|
"-crf", "23",
|
||||||
|
output_path
|
||||||
|
]
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def create_sample_template_data() -> Dict[str, Any]:
|
||||||
|
"""创建示例模板数据"""
|
||||||
|
return {
|
||||||
|
"templateId": "test_template_001",
|
||||||
|
"name": "测试模板",
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"id": "part1",
|
||||||
|
"type": "video",
|
||||||
|
"duration": 10.0,
|
||||||
|
"effects": ["zoom:0,2.0,3.0", "ospeed:1.5"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"width": 1920,
|
||||||
|
"height": 1080,
|
||||||
|
"frameRate": 25
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user