c7733a6974
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 1m6s
CI Build & Deploy Pipeline / Build Staging API Image (push) Has been cancelled
CI Build & Deploy Pipeline / Build Staging Web Image (push) Has been cancelled
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Has been cancelled
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI Build & Deploy Pipeline / Staging E2E Tests (push) Has been cancelled
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Has been cancelled
CI Build & Deploy Pipeline / Build Production API Image (push) Has been cancelled
CI Build & Deploy Pipeline / Build Production Web Image (push) Has been cancelled
CI Build & Deploy Pipeline / Build Production Worker Image (push) Has been cancelled
CI Build & Deploy Pipeline / Deploy Production (push) Has been cancelled
CI Build & Deploy Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
442 lines
14 KiB
Python
Executable File
442 lines
14 KiB
Python
Executable File
"""自动选素材逻辑单元测试 — 过滤不支持的编码格式."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
|
|
|
|
import pytest
|
|
|
|
from packages.domain import Asset, AssetLibrary, AssetLibraryKind, AssetStatus
|
|
|
|
|
|
def _make_asset(
|
|
asset_id: str,
|
|
*,
|
|
status=AssetStatus.READY,
|
|
mime_type: str = "video/mp4",
|
|
codec: str | None = None,
|
|
metadata: dict | None = None,
|
|
width: int = 1080,
|
|
height: int = 1920,
|
|
created_at=None,
|
|
) -> Asset:
|
|
"""构造测试用 Asset 对象."""
|
|
from datetime import datetime, timezone
|
|
|
|
return Asset(
|
|
id=asset_id,
|
|
project_id="proj-1",
|
|
library_id="lib-video",
|
|
name=f"{asset_id}.mp4",
|
|
storage_key=f"uploads/{asset_id}.mp4",
|
|
mime_type=mime_type,
|
|
status=status,
|
|
codec=codec,
|
|
metadata=metadata or {},
|
|
width=width,
|
|
height=height,
|
|
created_at=created_at or datetime.now(timezone.utc),
|
|
)
|
|
|
|
|
|
def _make_mock_repo(assets: list[Asset]):
|
|
"""构造 mock asset_repo."""
|
|
mock_repo = MagicMock()
|
|
mock_repo.find_by_library.return_value = assets
|
|
return mock_repo
|
|
|
|
|
|
def _make_mock_lib_repo():
|
|
"""构造 mock asset_library_repo,返回一个视频素材库."""
|
|
video_lib = AssetLibrary(
|
|
id="lib-video",
|
|
project_id="proj-1",
|
|
name="视频",
|
|
kind=AssetLibraryKind.VIDEO,
|
|
)
|
|
mock_repo = MagicMock()
|
|
mock_repo.find_by_project.return_value = [video_lib]
|
|
return mock_repo
|
|
|
|
|
|
class TestAutoSelectVideoAssetsCodecFilter:
|
|
"""自动选素材 — 编码格式不再过滤,渲染引擎统一转码."""
|
|
|
|
def test_h264_assets_selected(self):
|
|
"""H.264 编码素材 → 正常选中."""
|
|
from app.api.routes._helpers import auto_select_video_assets
|
|
|
|
assets = [
|
|
_make_asset("a1", codec="h264"),
|
|
_make_asset("a2", codec="h264"),
|
|
]
|
|
asset_repo = _make_mock_repo(assets)
|
|
lib_repo = _make_mock_lib_repo()
|
|
|
|
result = auto_select_video_assets(
|
|
project_id="proj-1",
|
|
asset_library_repo=lib_repo,
|
|
asset_repo=asset_repo,
|
|
)
|
|
|
|
assert len(result) == 2
|
|
assert "a1" in result
|
|
assert "a2" in result
|
|
|
|
def test_hevc_assets_not_filtered_by_codec_field(self):
|
|
"""HEVC 编码素材 → 不再被过滤,渲染引擎会统一转码."""
|
|
from app.api.routes._helpers import auto_select_video_assets
|
|
|
|
assets = [
|
|
_make_asset("a1", codec="h264"),
|
|
_make_asset("a2", codec="hevc"),
|
|
_make_asset("a3", codec="h265"),
|
|
]
|
|
asset_repo = _make_mock_repo(assets)
|
|
lib_repo = _make_mock_lib_repo()
|
|
|
|
result = auto_select_video_assets(
|
|
project_id="proj-1",
|
|
asset_library_repo=lib_repo,
|
|
asset_repo=asset_repo,
|
|
)
|
|
|
|
# 所有编码的素材都保留,不再按编码过滤
|
|
assert len(result) == 3
|
|
assert "a1" in result
|
|
assert "a2" in result
|
|
assert "a3" in result
|
|
|
|
def test_hevc_assets_not_filtered_by_metadata(self):
|
|
"""HEVC 编码素材(metadata 里有) → 不再被过滤."""
|
|
from app.api.routes._helpers import auto_select_video_assets
|
|
|
|
assets = [
|
|
_make_asset("a1", codec=None, metadata={"codec": "h264"}),
|
|
_make_asset("a2", codec=None, metadata={"codec": "hevc"}),
|
|
_make_asset("a3", codec=None, metadata={"codec": "vp9"}),
|
|
]
|
|
asset_repo = _make_mock_repo(assets)
|
|
lib_repo = _make_mock_lib_repo()
|
|
|
|
result = auto_select_video_assets(
|
|
project_id="proj-1",
|
|
asset_library_repo=lib_repo,
|
|
asset_repo=asset_repo,
|
|
)
|
|
|
|
# 所有编码的素材都保留
|
|
assert len(result) == 3
|
|
|
|
def test_codec_field_takes_precedence_over_metadata(self):
|
|
"""codec 字段优先于 metadata 里的 codec."""
|
|
from app.api.routes._helpers import auto_select_video_assets
|
|
|
|
# codec 字段是 h264,但 metadata 里是 hevc → 不影响,都能选中
|
|
assets = [
|
|
_make_asset("a1", codec="h264", metadata={"codec": "hevc"}),
|
|
]
|
|
asset_repo = _make_mock_repo(assets)
|
|
lib_repo = _make_mock_lib_repo()
|
|
|
|
result = auto_select_video_assets(
|
|
project_id="proj-1",
|
|
asset_library_repo=lib_repo,
|
|
asset_repo=asset_repo,
|
|
)
|
|
|
|
assert len(result) == 1
|
|
assert result[0] == "a1"
|
|
|
|
def test_vp9_av1_assets_not_filtered(self):
|
|
"""VP9 / AV1 编码素材 → 不再被过滤,渲染引擎会统一转码."""
|
|
from app.api.routes._helpers import auto_select_video_assets
|
|
|
|
assets = [
|
|
_make_asset("a1", codec="vp9"),
|
|
_make_asset("a2", codec="av1"),
|
|
_make_asset("a3", codec="h264"),
|
|
]
|
|
asset_repo = _make_mock_repo(assets)
|
|
lib_repo = _make_mock_lib_repo()
|
|
|
|
result = auto_select_video_assets(
|
|
project_id="proj-1",
|
|
asset_library_repo=lib_repo,
|
|
asset_repo=asset_repo,
|
|
)
|
|
|
|
# 所有编码的素材都保留
|
|
assert len(result) == 3
|
|
assert "a1" in result
|
|
assert "a2" in result
|
|
assert "a3" in result
|
|
|
|
def test_missing_codec_kept(self):
|
|
"""codec 字段和 metadata 里都没有 → 暂时放过(兼容存量数据)."""
|
|
from app.api.routes._helpers import auto_select_video_assets
|
|
|
|
assets = [
|
|
_make_asset("a1", codec=None, metadata={}),
|
|
_make_asset("a2", codec="h264"),
|
|
]
|
|
asset_repo = _make_mock_repo(assets)
|
|
lib_repo = _make_mock_lib_repo()
|
|
|
|
result = auto_select_video_assets(
|
|
project_id="proj-1",
|
|
asset_library_repo=lib_repo,
|
|
asset_repo=asset_repo,
|
|
)
|
|
|
|
assert len(result) == 2
|
|
|
|
def test_codec_case_insensitive(self):
|
|
"""不同编码格式的素材都保留,不再按编码过滤."""
|
|
from app.api.routes._helpers import auto_select_video_assets
|
|
|
|
assets = [
|
|
_make_asset("a1", codec="HEVC"),
|
|
_make_asset("a2", codec="H264"),
|
|
]
|
|
asset_repo = _make_mock_repo(assets)
|
|
lib_repo = _make_mock_lib_repo()
|
|
|
|
result = auto_select_video_assets(
|
|
project_id="proj-1",
|
|
asset_library_repo=lib_repo,
|
|
asset_repo=asset_repo,
|
|
)
|
|
|
|
# 所有编码的素材都保留
|
|
assert len(result) == 2
|
|
assert "a1" in result
|
|
assert "a2" in result
|
|
|
|
def test_only_ready_assets_selected(self):
|
|
"""只有 READY 状态的素材才会被选中."""
|
|
from app.api.routes._helpers import auto_select_video_assets
|
|
|
|
assets = [
|
|
_make_asset("a1", status=AssetStatus.READY, codec="h264"),
|
|
_make_asset("a2", status=AssetStatus.PROCESSING, codec="h264"),
|
|
_make_asset("a3", status=AssetStatus.ERROR, codec="h264"),
|
|
]
|
|
asset_repo = _make_mock_repo(assets)
|
|
lib_repo = _make_mock_lib_repo()
|
|
|
|
result = auto_select_video_assets(
|
|
project_id="proj-1",
|
|
asset_library_repo=lib_repo,
|
|
asset_repo=asset_repo,
|
|
)
|
|
|
|
assert len(result) == 1
|
|
assert result[0] == "a1"
|
|
|
|
def test_only_video_mime_selected(self):
|
|
"""只有 video/* mime_type 的素材才会被选中."""
|
|
from app.api.routes._helpers import auto_select_video_assets
|
|
|
|
assets = [
|
|
_make_asset("a1", mime_type="video/mp4", codec="h264"),
|
|
_make_asset("a2", mime_type="image/jpeg"),
|
|
_make_asset("a3", mime_type="audio/mpeg"),
|
|
]
|
|
asset_repo = _make_mock_repo(assets)
|
|
lib_repo = _make_mock_lib_repo()
|
|
|
|
result = auto_select_video_assets(
|
|
project_id="proj-1",
|
|
asset_library_repo=lib_repo,
|
|
asset_repo=asset_repo,
|
|
)
|
|
|
|
assert len(result) == 1
|
|
assert result[0] == "a1"
|
|
|
|
def test_empty_project_returns_empty(self):
|
|
"""空 project_id → 返回空列表."""
|
|
from app.api.routes._helpers import auto_select_video_assets
|
|
|
|
asset_repo = _make_mock_repo([])
|
|
lib_repo = _make_mock_lib_repo()
|
|
|
|
result = auto_select_video_assets(
|
|
project_id="",
|
|
asset_library_repo=lib_repo,
|
|
asset_repo=asset_repo,
|
|
)
|
|
|
|
assert result == []
|
|
|
|
def test_no_video_library_returns_empty(self):
|
|
"""没有视频素材库 → 返回空列表."""
|
|
from app.api.routes._helpers import auto_select_video_assets
|
|
|
|
image_lib = AssetLibrary(
|
|
id="lib-image",
|
|
project_id="proj-1",
|
|
name="图片",
|
|
kind=AssetLibraryKind.IMAGE,
|
|
)
|
|
lib_repo = MagicMock()
|
|
lib_repo.find_by_project.return_value = [image_lib]
|
|
|
|
asset_repo = _make_mock_repo([])
|
|
|
|
result = auto_select_video_assets(
|
|
project_id="proj-1",
|
|
asset_library_repo=lib_repo,
|
|
asset_repo=asset_repo,
|
|
)
|
|
|
|
assert result == []
|
|
|
|
def test_sorted_by_created_at_desc(self):
|
|
"""结果按创建时间倒序(新素材在前)."""
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from app.api.routes._helpers import auto_select_video_assets
|
|
|
|
now = datetime.now(timezone.utc)
|
|
assets = [
|
|
_make_asset("oldest", codec="h264", created_at=now - timedelta(hours=2)),
|
|
_make_asset("newest", codec="h264", created_at=now),
|
|
_make_asset("middle", codec="h264", created_at=now - timedelta(hours=1)),
|
|
]
|
|
asset_repo = _make_mock_repo(assets)
|
|
lib_repo = _make_mock_lib_repo()
|
|
|
|
result = auto_select_video_assets(
|
|
project_id="proj-1",
|
|
asset_library_repo=lib_repo,
|
|
asset_repo=asset_repo,
|
|
)
|
|
|
|
assert result == ["newest", "middle", "oldest"]
|
|
|
|
def test_landscape_videos_filtered_out(self):
|
|
"""横屏视频(width > height)被过滤掉."""
|
|
from app.api.routes._helpers import auto_select_video_assets
|
|
|
|
assets = [
|
|
_make_asset("portrait1", codec="h264", width=1080, height=1920),
|
|
_make_asset("landscape1", codec="h264", width=1920, height=1080),
|
|
_make_asset("portrait2", codec="h264", width=720, height=1280),
|
|
_make_asset("landscape2", codec="h264", width=1280, height=720),
|
|
]
|
|
asset_repo = _make_mock_repo(assets)
|
|
lib_repo = _make_mock_lib_repo()
|
|
|
|
result = auto_select_video_assets(
|
|
project_id="proj-1",
|
|
asset_library_repo=lib_repo,
|
|
asset_repo=asset_repo,
|
|
)
|
|
|
|
assert set(result) == {"portrait1", "portrait2"}
|
|
assert len(result) == 2
|
|
|
|
def test_square_videos_allowed(self):
|
|
"""正方形视频(width == height)不被过滤."""
|
|
from app.api.routes._helpers import auto_select_video_assets
|
|
|
|
assets = [
|
|
_make_asset("square", codec="h264", width=720, height=720),
|
|
_make_asset("portrait", codec="h264", width=1080, height=1920),
|
|
]
|
|
asset_repo = _make_mock_repo(assets)
|
|
lib_repo = _make_mock_lib_repo()
|
|
|
|
result = auto_select_video_assets(
|
|
project_id="proj-1",
|
|
asset_library_repo=lib_repo,
|
|
asset_repo=asset_repo,
|
|
)
|
|
|
|
assert "square" in result
|
|
assert "portrait" in result
|
|
|
|
def test_unknown_size_not_filtered(self):
|
|
"""尺寸未知的素材不被过滤(避免误杀存量数据)."""
|
|
from app.api.routes._helpers import auto_select_video_assets
|
|
|
|
assets = [
|
|
_make_asset("unknown", codec="h264", width=0, height=0),
|
|
_make_asset("portrait", codec="h264", width=1080, height=1920),
|
|
]
|
|
asset_repo = _make_mock_repo(assets)
|
|
lib_repo = _make_mock_lib_repo()
|
|
|
|
result = auto_select_video_assets(
|
|
project_id="proj-1",
|
|
asset_library_repo=lib_repo,
|
|
asset_repo=asset_repo,
|
|
)
|
|
|
|
assert "unknown" in result
|
|
assert "portrait" in result
|
|
|
|
def test_metadata_size_fallback(self):
|
|
"""width/height字段为空时从metadata兜底读取."""
|
|
from app.api.routes._helpers import auto_select_video_assets
|
|
|
|
assets = [
|
|
_make_asset(
|
|
"landscape_in_meta",
|
|
codec="h264",
|
|
width=0,
|
|
height=0,
|
|
metadata={"width": 1920, "height": 1080},
|
|
),
|
|
_make_asset(
|
|
"portrait_in_meta",
|
|
codec="h264",
|
|
width=0,
|
|
height=0,
|
|
metadata={"width": 1080, "height": 1920},
|
|
),
|
|
]
|
|
asset_repo = _make_mock_repo(assets)
|
|
lib_repo = _make_mock_lib_repo()
|
|
|
|
result = auto_select_video_assets(
|
|
project_id="proj-1",
|
|
asset_library_repo=lib_repo,
|
|
asset_repo=asset_repo,
|
|
)
|
|
|
|
assert "landscape_in_meta" not in result
|
|
assert "portrait_in_meta" in result
|
|
|
|
def test_only_landscape_filtered_codec_not_filtered(self):
|
|
"""只有横屏过滤生效,编码不再过滤."""
|
|
from app.api.routes._helpers import auto_select_video_assets
|
|
|
|
assets = [
|
|
_make_asset("good", codec="h264", width=1080, height=1920),
|
|
_make_asset("hevc_portrait", codec="hevc", width=1080, height=1920),
|
|
_make_asset("bad_landscape", codec="h264", width=1920, height=1080),
|
|
_make_asset("hevc_landscape", codec="hevc", width=1920, height=1080),
|
|
]
|
|
asset_repo = _make_mock_repo(assets)
|
|
lib_repo = _make_mock_lib_repo()
|
|
|
|
result = auto_select_video_assets(
|
|
project_id="proj-1",
|
|
asset_library_repo=lib_repo,
|
|
asset_repo=asset_repo,
|
|
)
|
|
|
|
# 只有横屏被过滤,编码不再过滤
|
|
assert len(result) == 2
|
|
assert "good" in result
|
|
assert "hevc_portrait" in result
|
|
assert "bad_landscape" not in result
|
|
assert "hevc_landscape" not in result
|