f1621ace9f
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
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 / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 5s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m17s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m26s
CI/CD Pipeline / Integration Tests (push) Successful in 3m48s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m13s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m44s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m19s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / Validate - Style (push) Successful in 7m51s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m54s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m44s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m58s
CI/CD Pipeline / Unit Tests (push) Successful in 10m42s
CI/CD Pipeline / Validate - Security (push) Successful in 12m25s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
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 / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
111 lines
3.9 KiB
Python
111 lines
3.9 KiB
Python
"""#1970 原子片段 resolver 单元测试:DB 加载 + 内存兜底."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from packages.domain.asset_atom_clip import AssetAtomClip
|
|
from packages.domain.atom_clip_resolver import (
|
|
flatten_candidates,
|
|
load_atom_clips_for_assets,
|
|
)
|
|
|
|
|
|
def _atom(asset_id: str, idx: int, start: float, end: float) -> AssetAtomClip:
|
|
return AssetAtomClip(
|
|
id=f"{asset_id}-clip-{idx}",
|
|
asset_id=asset_id,
|
|
start_time=start,
|
|
end_time=end,
|
|
duration=round(end - start, 3),
|
|
clip_index=idx,
|
|
)
|
|
|
|
|
|
class FakeAtomRepo:
|
|
def __init__(self, by_asset):
|
|
self._by_asset = by_asset
|
|
|
|
def find_candidates_for_selection(self, asset_ids, *, limit=0):
|
|
out = []
|
|
for aid in asset_ids:
|
|
out.extend(self._by_asset.get(aid, []))
|
|
return out
|
|
|
|
def find_by_asset(self, asset_id):
|
|
return list(self._by_asset.get(asset_id, []))
|
|
|
|
|
|
class _Asset:
|
|
def __init__(self, duration):
|
|
self.duration = duration
|
|
|
|
|
|
class FakeAssetRepo:
|
|
def __init__(self, durations):
|
|
self._durations = durations
|
|
|
|
def get(self, asset_id):
|
|
d = self._durations.get(asset_id)
|
|
return _Asset(d) if d is not None else None
|
|
|
|
|
|
class TestLoadAtomClips:
|
|
def test_persisted_clips_loaded_sorted(self):
|
|
clips = [_atom("a", 1, 4.5, 9.0), _atom("a", 0, 0.0, 4.5)]
|
|
repo = FakeAtomRepo({"a": clips})
|
|
result = load_atom_clips_for_assets(["a"], atom_clip_repo=repo)
|
|
assert [c.clip_index for c in result["a"]] == [0, 1]
|
|
|
|
def test_dedup_asset_ids_preserves_order(self):
|
|
repo = FakeAtomRepo({"a": [_atom("a", 0, 0, 4)], "b": [_atom("b", 0, 0, 4)]})
|
|
result = load_atom_clips_for_assets(["a", "b", "a"], atom_clip_repo=repo)
|
|
assert list(result.keys()) == ["a", "b"]
|
|
|
|
def test_fallback_when_no_persisted_clips(self):
|
|
"""老素材没有 atom_clips 时,内存按 3-6 秒均匀切片,标记 is_fallback。"""
|
|
atom_repo = FakeAtomRepo({})
|
|
asset_repo = FakeAssetRepo({"old": 20.0})
|
|
result = load_atom_clips_for_assets(["old"], atom_clip_repo=atom_repo, asset_repo=asset_repo)
|
|
assert "old" in result
|
|
clips = result["old"]
|
|
assert clips
|
|
assert all(c.is_fallback for c in clips)
|
|
assert abs(clips[-1].end_time - 20.0) < 0.01
|
|
|
|
def test_missing_duration_skipped(self):
|
|
atom_repo = FakeAtomRepo({})
|
|
asset_repo = FakeAssetRepo({})
|
|
result = load_atom_clips_for_assets(["ghost"], atom_clip_repo=atom_repo, asset_repo=asset_repo)
|
|
assert result == {}
|
|
|
|
def test_no_asset_repo_skips_empty_assets(self):
|
|
atom_repo = FakeAtomRepo({})
|
|
result = load_atom_clips_for_assets(["a"], atom_clip_repo=atom_repo, asset_repo=None)
|
|
assert result == {}
|
|
|
|
def test_mixed_persisted_and_fallback(self):
|
|
atom_repo = FakeAtomRepo({"new": [_atom("new", 0, 0, 5)]})
|
|
asset_repo = FakeAssetRepo({"new": 5.0, "old": 10.0})
|
|
result = load_atom_clips_for_assets(["new", "old"], atom_clip_repo=atom_repo, asset_repo=asset_repo)
|
|
assert not result["new"][0].is_fallback
|
|
assert all(c.is_fallback for c in result["old"])
|
|
|
|
def test_repo_exception_falls_back(self):
|
|
class BrokenRepo(FakeAtomRepo):
|
|
def find_candidates_for_selection(self, asset_ids, *, limit=0):
|
|
raise RuntimeError("db down")
|
|
|
|
asset_repo = FakeAssetRepo({"a": 9.0})
|
|
result = load_atom_clips_for_assets(["a"], atom_clip_repo=BrokenRepo({}), asset_repo=asset_repo)
|
|
assert result["a"]
|
|
assert all(c.is_fallback for c in result["a"])
|
|
|
|
def test_empty_input(self):
|
|
assert load_atom_clips_for_assets([], atom_clip_repo=FakeAtomRepo({})) == {}
|
|
|
|
|
|
class TestFlatten:
|
|
def test_flatten_order(self):
|
|
clips = flatten_candidates({"a": [_atom("a", 0, 0, 4)], "b": [_atom("b", 0, 0, 4), _atom("b", 1, 4, 8)]})
|
|
assert len(clips) == 3
|
|
assert clips[0].asset_id == "a"
|