e83a0b86f8
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 38s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 58s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 55s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m5s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m18s
CI/CD Pipeline / Validate - Style (push) Successful in 2m31s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m30s
CI/CD Pipeline / Integration Tests (push) Successful in 2m35s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m7s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m15s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m30s
CI/CD Pipeline / Validate - Security (push) Successful in 4m34s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m37s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m54s
AI Code Review / AI Code Review (pull_request) Successful in 6m19s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m57s
CI/CD Pipeline / Unit Tests (push) Successful in 8m33s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m48s
CI/CD Pipeline / Canary Release to Production (push) Failing after 221h53m0s
CI/CD Pipeline / CI Gate (push) Failing after 221h53m4s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 221h53m4s
CI/CD Pipeline / Build Production API Image (push) Failing after 221h53m6s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 221h58m26s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 221h59m0s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 221h59m0s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 222h0m54s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 222h0m57s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 221h59m0s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 222h0m57s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 222h0m55s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 222h1m13s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 222h1m17s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 222h0m55s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 222h1m38s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 222h1m38s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 222h1m23s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 222h1m38s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 222h1m28s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 222h1m39s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 222h1m33s
CI/CD Pipeline / Frontend Lint (push) Failing after 222h1m41s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 222h1m37s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 222h1m42s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 222h1m43s
CI/CD Pipeline / PR Build API Image (push) Failing after 222h1m43s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 222h1m42s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 222h1m44s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 222h1m42s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 222h1m45s
CI/CD Pipeline / Deploy Production (push) Failing after 222h27m37s
CI/CD Pipeline / Build Production Web Image (push) Failing after 222h27m42s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 222h35m33s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 222h35m50s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 222h36m14s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 222h36m15s
CI/CD Pipeline / PR Build Web Image (push) Failing after 222h36m20s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
194 lines
7.3 KiB
Python
194 lines
7.3 KiB
Python
"""#1749 EditPlanService 新增方法单元测试。
|
||
|
||
覆盖 get_asset_durations / apply_voice_duration_to_plan / ensure_variant_plans。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
import pytest
|
||
|
||
# ── helpers ───────────────────────────────────────────────────────────────────
|
||
|
||
|
||
def _make_svc():
|
||
"""构造 EditPlanService,mock 掉 DB/repos。"""
|
||
from app.services.edit_plan_service import EditPlanService
|
||
|
||
db = MagicMock()
|
||
svc = EditPlanService.__new__(EditPlanService)
|
||
svc._plan_repo = MagicMock()
|
||
svc._clip_repo = MagicMock()
|
||
svc._clip_repo.session = db
|
||
svc._generation_task_repo = MagicMock()
|
||
return svc, db
|
||
|
||
|
||
def _make_clip(
|
||
order=0,
|
||
asset_id="a1",
|
||
start_time=0.0,
|
||
duration=5.0,
|
||
clip_type="main",
|
||
transition_effect="cut",
|
||
transition_duration=0.0,
|
||
playback_speed=1.0,
|
||
text_content="",
|
||
config=None,
|
||
):
|
||
c = MagicMock()
|
||
c.order = order
|
||
c.asset_id = asset_id
|
||
c.start_time = start_time
|
||
c.duration = duration
|
||
c.clip_type = clip_type
|
||
c.transition_effect = transition_effect
|
||
c.transition_duration = transition_duration
|
||
c.playback_speed = playback_speed
|
||
c.text_content = text_content
|
||
c.config = config or {}
|
||
return c
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# get_asset_durations
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
class TestGetAssetDurations:
|
||
def test_empty_ids_returns_empty(self):
|
||
svc, _db = _make_svc()
|
||
assert svc.get_asset_durations([]) == {}
|
||
assert svc.get_asset_durations(None) == {}
|
||
|
||
def test_dedup_and_query(self):
|
||
svc, db = _make_svc()
|
||
m1 = MagicMock()
|
||
m1.id = "a1"
|
||
m1.duration = 10.5
|
||
m2 = MagicMock()
|
||
m2.id = "a2"
|
||
m2.duration = None # -> 0.0
|
||
|
||
db.query.return_value.filter.return_value.all.return_value = [m1, m2]
|
||
|
||
result = svc.get_asset_durations(["a1", "a2", "a1"])
|
||
assert result == {"a1": 10.5, "a2": 0.0}
|
||
|
||
def test_bad_duration_type_returns_zero(self):
|
||
svc, db = _make_svc()
|
||
m = MagicMock()
|
||
m.id = "a1"
|
||
m.duration = "not-a-number"
|
||
db.query.return_value.filter.return_value.all.return_value = [m]
|
||
result = svc.get_asset_durations(["a1"])
|
||
assert result == {"a1": 0.0}
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# apply_voice_duration_to_plan
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
class TestApplyVoiceDurationToPlan:
|
||
def test_voice_le_zero_returns_none(self):
|
||
svc, _ = _make_svc()
|
||
assert svc.apply_voice_duration_to_plan("p1", 0) is None
|
||
assert svc.apply_voice_duration_to_plan("p1", -5) is None
|
||
|
||
def test_plan_not_found_returns_none(self):
|
||
svc, _ = _make_svc()
|
||
svc.get_plan = MagicMock(return_value=None)
|
||
assert svc.apply_voice_duration_to_plan("p1", 30.0) is None
|
||
|
||
def test_no_clips_returns_none(self):
|
||
svc, _ = _make_svc()
|
||
plan = MagicMock()
|
||
svc.get_plan = MagicMock(return_value=plan)
|
||
svc._clip_repo.list_by_plan.return_value = []
|
||
assert svc.apply_voice_duration_to_plan("p1", 30.0) is None
|
||
|
||
def test_happy_path_two_clips(self):
|
||
svc, db = _make_svc()
|
||
plan = MagicMock()
|
||
plan.total_duration = 0.0
|
||
svc.get_plan = MagicMock(return_value=plan)
|
||
|
||
clips = [
|
||
_make_clip(order=0, asset_id="a1", start_time=0.0),
|
||
_make_clip(order=1, asset_id="a2", start_time=0.0),
|
||
]
|
||
svc._clip_repo.list_by_plan.return_value = clips
|
||
|
||
# get_asset_durations: a1=100s, a2=50s
|
||
m1 = MagicMock(id="a1", duration=100.0)
|
||
m2 = MagicMock(id="a2", duration=50.0)
|
||
db.query.return_value.filter.return_value.all.return_value = [m1, m2]
|
||
|
||
svc.replace_all_clips_transactional = MagicMock(return_value=2)
|
||
|
||
result = svc.apply_voice_duration_to_plan("p1", 30.0)
|
||
assert result is plan
|
||
svc.replace_all_clips_transactional.assert_called_once()
|
||
|
||
def test_invalid_voice_returns_none(self):
|
||
svc, _ = _make_svc()
|
||
assert svc.apply_voice_duration_to_plan("p1", "bad") is None
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# ensure_variant_plans
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
class TestEnsureVariantPlans:
|
||
def _setup_svc(self):
|
||
svc, _ = _make_svc()
|
||
plan0 = MagicMock()
|
||
plan0.id = "plan_v0"
|
||
svc.clone_plan_for_variant = MagicMock(return_value=plan0)
|
||
|
||
plan1 = MagicMock()
|
||
plan1.id = "plan_v1"
|
||
svc.reselect_plan_for_variant = MagicMock(return_value=plan1)
|
||
|
||
svc.apply_voice_duration_to_plan = MagicMock(return_value=plan0)
|
||
return svc
|
||
|
||
def test_count_1_no_voice(self):
|
||
svc = self._setup_svc()
|
||
result = svc.ensure_variant_plans("src", 1, ["a1", "a2"])
|
||
assert result == ["plan_v0"]
|
||
svc.clone_plan_for_variant.assert_called_once()
|
||
svc.apply_voice_duration_to_plan.assert_not_called()
|
||
|
||
def test_count_1_with_voice(self):
|
||
svc = self._setup_svc()
|
||
result = svc.ensure_variant_plans("src", 1, ["a1"], voice_durations=[30.0])
|
||
assert result == ["plan_v0"]
|
||
svc.apply_voice_duration_to_plan.assert_called_once_with("plan_v0", 30.0)
|
||
|
||
def test_count_3_with_voice(self):
|
||
svc = self._setup_svc()
|
||
svc.reselect_plan_for_variant = MagicMock(
|
||
side_effect=[
|
||
MagicMock(id="plan_v1"),
|
||
MagicMock(id="plan_v2"),
|
||
]
|
||
)
|
||
|
||
result = svc.ensure_variant_plans("src", 3, ["a1", "a2"], voice_durations=[30.0, 20.0, 15.0])
|
||
assert len(result) == 3
|
||
assert result[0] == "plan_v0"
|
||
# v0 配音
|
||
svc.apply_voice_duration_to_plan.assert_called_once_with("plan_v0", 30.0)
|
||
# reselect 被调 2 次
|
||
assert svc.reselect_plan_for_variant.call_count == 2
|
||
|
||
def test_voice_apply_failure_not_blocking(self):
|
||
svc = self._setup_svc()
|
||
svc.apply_voice_duration_to_plan.side_effect = RuntimeError("boom")
|
||
result = svc.ensure_variant_plans("src", 1, ["a1"], voice_durations=[30.0])
|
||
assert result == ["plan_v0"]
|