Files
xiaoxia-saas/tests/unit/test_1256_random_asset_selection.py
xiaoxia 316f01b3f0
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m6s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m10s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m2s
CI/CD Pipeline / Unit Tests (push) Successful in 15m50s
CI/CD Pipeline / Integration Tests (push) Successful in 5m20s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m42s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 14m29s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m17s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m41s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m22s
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 49s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 56s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m59s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
feat: 预览生成支持随机选素材 + 随机截取片段 (#1256) (#1268)
2026-08-07 19:50:01 +08:00

201 lines
8.6 KiB
Python

"""Tests for #1256: random asset selection and random start_time in preview generation."""
from unittest.mock import MagicMock
import pytest
from packages.domain.edit_plan_clip import EditPlanClip
from packages.domain.plan_generator_utils import (
_calc_random_start_time,
distribute_assets,
)
class TestAssignAssetWithStartTime:
"""Test assign_asset() with optional start_time parameter."""
def test_assign_asset_without_start_time(self):
"""Backward compatible: assign_asset without start_time keeps default 0.0"""
clip = EditPlanClip.create(plan_id="p1", clip_type="main", order=1, duration=5.0)
clip.assign_asset("asset1")
assert clip.asset_id == "asset1"
assert clip.start_time == 0.0
def test_assign_asset_with_valid_start_time(self):
"""assign_asset with valid start_time sets it correctly"""
clip = EditPlanClip.create(plan_id="p1", clip_type="main", order=1, duration=5.0)
clip.assign_asset("asset1", start_time=3.5)
assert clip.asset_id == "asset1"
assert clip.start_time == 3.5
def test_assign_asset_with_zero_start_time(self):
"""assign_asset with start_time=0.0 sets it to 0.0"""
clip = EditPlanClip.create(plan_id="p1", clip_type="main", order=1, duration=5.0, start_time=5.0)
clip.assign_asset("asset1", start_time=0.0)
assert clip.start_time == 0.0
def test_assign_asset_with_none_start_time(self):
"""assign_asset with start_time=None keeps existing start_time"""
clip = EditPlanClip.create(plan_id="p1", clip_type="main", order=1, duration=5.0, start_time=2.0)
clip.assign_asset("asset1", start_time=None)
assert clip.start_time == 2.0 # unchanged
def test_assign_asset_with_negative_start_time(self):
"""assign_asset with negative start_time is ignored"""
clip = EditPlanClip.create(plan_id="p1", clip_type="main", order=1, duration=5.0, start_time=2.0)
clip.assign_asset("asset1", start_time=-1.0)
assert clip.start_time == 2.0 # unchanged, negative ignored
class TestCalcRandomStartTime:
"""Test _calc_random_start_time helper function."""
def test_returns_none_when_no_durations(self):
"""Returns None when asset_durations is None"""
result = _calc_random_start_time("asset1", 5.0, None)
assert result is None
def test_returns_none_when_asset_not_in_durations(self):
"""Returns None when asset_id not in durations dict"""
result = _calc_random_start_time("asset1", 5.0, {"other": 30.0})
assert result is None
def test_returns_zero_when_duration_too_short(self):
"""Returns 0.0 when asset duration <= clip duration"""
result = _calc_random_start_time("asset1", 10.0, {"asset1": 5.0})
assert result == 0.0
def test_returns_valid_random_start_time(self):
"""Returns start_time within valid range"""
import random
random.seed(42)
result = _calc_random_start_time("asset1", 5.0, {"asset1": 30.0})
# max_start = 30.0 - 5.0 = 25.0
assert 0.0 <= result <= 25.0
def test_returns_zero_when_duration_zero(self):
"""Returns None when asset duration is 0"""
result = _calc_random_start_time("asset1", 5.0, {"asset1": 0.0})
assert result is None
class TestDistributeAssetsRandom:
"""Test distribute_assets() with random_selection parameter."""
def test_one_take_without_random(self):
"""ONE_TAKE without random: assets assigned in order"""
clips = [EditPlanClip.create(plan_id="p1", clip_type="main", order=i, duration=5.0) for i in range(3)]
assets = ["a1", "a2", "a3", "a4"]
distribute_assets(clips, assets, "one_take", random_selection=False)
assert clips[0].asset_id == "a1"
assert clips[1].asset_id == "a2"
assert clips[2].asset_id == "a3"
def test_one_take_with_random(self):
"""ONE_TAKE with random: assets assigned in random order"""
clips = [EditPlanClip.create(plan_id="p1", clip_type="main", order=i, duration=5.0) for i in range(3)]
assets = ["a1", "a2", "a3", "a4"]
# Run multiple times to verify randomness
results = set()
for _ in range(10):
distribute_assets(clips, assets, "one_take", random_selection=True)
results.add(tuple(c.asset_id for c in clips))
# Should have multiple different orderings
assert len(results) > 1, "Random selection should produce different orderings"
def test_with_asset_durations_sets_start_time(self):
"""distribute_assets with asset_durations sets random start_time"""
clips = [EditPlanClip.create(plan_id="p1", clip_type="main", order=0, duration=5.0)]
durations = {"a1": 30.0}
distribute_assets(clips, ["a1"], "one_take", asset_durations=durations)
assert clips[0].asset_id == "a1"
# start_time should be set (0 <= start_time <= 25.0)
assert 0.0 <= clips[0].start_time <= 25.0
def test_pip_mode_with_random(self):
"""PIP mode with random selection works correctly"""
clips = [
EditPlanClip.create(plan_id="p1", clip_type="main", order=0, duration=5.0),
EditPlanClip.create(plan_id="p1", clip_type="overlay", order=1, duration=5.0),
EditPlanClip.create(plan_id="p1", clip_type="overlay", order=2, duration=5.0),
]
assets = ["a1", "a2", "a3"]
distribute_assets(clips, assets, "pip", random_selection=True)
# All clips should have assets assigned
assert clips[0].asset_id
assert clips[1].asset_id
assert clips[2].asset_id
def test_voice_pip_mode_with_random(self):
"""VOICE_PIP mode with random selection works correctly"""
clips = [
EditPlanClip.create(plan_id="p1", clip_type="background", order=0, duration=5.0),
EditPlanClip.create(plan_id="p1", clip_type="corner_voice", order=1, duration=5.0),
EditPlanClip.create(plan_id="p1", clip_type="b_roll", order=2, duration=5.0),
]
assets = ["a1", "a2", "a3"]
durations = {"a1": 30.0, "a2": 25.0, "a3": 20.0}
distribute_assets(clips, assets, "voice_pip", random_selection=True, asset_durations=durations)
# All clips should have assets and start_times
for clip in clips:
assert clip.asset_id
assert 0.0 <= clip.start_time <= 25.0 # max_start = duration - clip_duration
def test_does_not_modify_original_list(self):
"""random_selection should not modify the original asset_ids list"""
clips = [EditPlanClip.create(plan_id="p1", clip_type="main", order=0, duration=5.0)]
assets = ["a1", "a2", "a3"]
original = list(assets)
distribute_assets(clips, assets, "one_take", random_selection=True)
assert assets == original, "Original asset_ids list should not be modified"
class TestDistributeAssetsBackwardCompatible:
"""Ensure backward compatibility - existing calls without new params still work."""
def test_distribute_assets_default_params(self):
"""distribute_assets works with just required params"""
clips = [
EditPlanClip.create(plan_id="p1", clip_type="main", order=0, duration=5.0),
EditPlanClip.create(plan_id="p1", clip_type="main", order=1, duration=5.0),
]
distribute_assets(clips, ["a1", "a2"], "one_take")
assert clips[0].asset_id == "a1"
assert clips[1].asset_id == "a2"
assert clips[0].start_time == 0.0 # default
def test_all_modes_work_without_new_params(self):
"""All editing modes work without random_selection/asset_durations"""
# one_take / voice_over: use main clips
for mode in ["one_take", "voice_over"]:
clips = [
EditPlanClip.create(plan_id="p1", clip_type="main", order=0, duration=5.0),
]
distribute_assets(clips, ["a1"], mode)
assert clips[0].asset_id == "a1"
# pip: main + overlay
clips = [
EditPlanClip.create(plan_id="p1", clip_type="main", order=0, duration=5.0),
EditPlanClip.create(plan_id="p1", clip_type="overlay", order=1, duration=5.0),
]
distribute_assets(clips, ["a1"], "pip")
assert clips[0].asset_id == "a1" # first asset goes to main
# voice_pip: background + corner_voice + b_roll
clips = [
EditPlanClip.create(plan_id="p1", clip_type="background", order=0, duration=5.0),
EditPlanClip.create(plan_id="p1", clip_type="corner_voice", order=1, duration=5.0),
]
distribute_assets(clips, ["a1"], "voice_pip")
assert clips[0].asset_id == "a1" # first asset goes to background