"""VID_01, VID_07–VID_14 — timeline plugin integration tests.""" from __future__ import annotations import json import pathlib import shutil import pytest from muse.core.schema import DomainSchema from muse.core.types import JsonObject, content_hash from muse.domain import MuseDomainPlugin, SnapshotManifest from muse.plugins.registry import registered_domains, resolve_plugin_by_domain from muse.plugins.timeline._otio_bridge import BridgeError, load_otio_json, parse_otio_to_project from muse.plugins.timeline.entity import ( Clip, Gap, assign_clip_occurrences, assign_identities, clip_identity, project_content_id, sequence_identity, track_identity, ) from muse.plugins.timeline.manifest import build_timeline_manifest from muse.plugins.timeline.plugin import TimelinePlugin, plugin from muse.plugins.timeline.rational_time import RationalRate, RationalTime, TimeRange FIXTURES = pathlib.Path(__file__).parent / "fixtures" / "timeline" GOLDEN = FIXTURES / "golden" @pytest.fixture() def timeline_plugin() -> TimelinePlugin: return TimelinePlugin() @pytest.fixture() def fixture_repo(tmp_path: pathlib.Path) -> pathlib.Path: repo = tmp_path / "repo" repo.mkdir() media = repo / "media" media.mkdir() for name in ("shot_a.mov", "shot_b.mov", "shot_c.mov", "same_shot.mov", "v1.mov", "v2.mov", "a1.wav", "ntsc.mov", "df.mov", "nested_ref.mov"): (media / name).write_bytes(b"stub") shutil.copy(FIXTURES / "single_track.otio", repo / "single_track.otio") return repo class TestVID01Protocol: def test_plugin_satisfies_protocol(self) -> None: assert isinstance(plugin, MuseDomainPlugin) def test_schema_valid(self, timeline_plugin: TimelinePlugin) -> None: schema: DomainSchema = timeline_plugin.schema() assert schema["domain"] == "timeline" assert schema["merge_mode"] == "three_way" assert len(schema["dimensions"]) > 0 def test_registered_active(self) -> None: assert "timeline" in registered_domains() p = resolve_plugin_by_domain("timeline") assert isinstance(p, TimelinePlugin) class TestVID07OtioBridge: def test_single_track_parse(self, fixture_repo: pathlib.Path) -> None: raw = load_otio_json(fixture_repo / "single_track.otio") project = parse_otio_to_project(raw, fixture_repo / "single_track.otio", fixture_repo) assert project.project_key == "single_track" assert len(project.sequences) == 1 track = project.sequences[0].video_tracks[0] clips = [i for i in track.items if isinstance(i, Clip)] gaps = [i for i in track.items if isinstance(i, Gap)] assert len(clips) == 3 assert len(gaps) == 1 assert clips[0].source_range.start_time.rate == RationalRate(24, 1) def test_unknown_schema_version_raises(self) -> None: bad = { "OTIO_SCHEMA": "Timeline.999", "name": "bad", "tracks": {"OTIO_SCHEMA": "Stack.1", "children": []}, } with pytest.raises(BridgeError): parse_otio_to_project(bad, pathlib.Path("bad.otio"), None) class TestVID08Determinism: def test_snapshot_twice_identical(self, fixture_repo: pathlib.Path) -> None: a = plugin.snapshot(fixture_repo) b = plugin.snapshot(fixture_repo) assert content_hash(a) == content_hash(b) def test_reformatted_identical_hash(self, fixture_repo: pathlib.Path) -> None: shutil.copy(FIXTURES / "single_track_reformatted.otio", fixture_repo / "reformatted.otio") base = plugin.snapshot(fixture_repo) only_reformatted = fixture_repo / "only_ref" only_reformatted.mkdir() (only_reformatted / "media").mkdir() for name in ("shot_a.mov", "shot_b.mov", "shot_c.mov"): (only_reformatted / "media" / name).write_bytes(b"stub") shutil.copy(FIXTURES / "single_track.otio", only_reformatted / "single_track.otio") snap_a = plugin.snapshot(only_reformatted) shutil.copy(FIXTURES / "single_track_reformatted.otio", only_reformatted / "single_track.otio") snap_b = plugin.snapshot(only_reformatted) assert snap_a["files"]["single_track.otio"] == snap_b["files"]["single_track.otio"] class TestVID09ClipIdentityExcludesTimelineStart: def test_widen_gap_preserves_clip_ids(self, fixture_repo: pathlib.Path) -> None: raw = load_otio_json(fixture_repo / "single_track.otio") project = parse_otio_to_project(raw, fixture_repo / "single_track.otio", fixture_repo) before = assign_identities(project) clip_ids_before = [ic.clip_id for ic in before.sequences[0].tracks[0].clips] track = project.sequences[0].video_tracks[0] items = list(track.items) gap = items[1] assert isinstance(gap, Gap) wider = Gap( duration=RationalTime(gap.duration.value + 48, gap.duration.rate), metadata=gap.metadata, ) items[1] = wider from dataclasses import replace new_track = replace(track, items=tuple(items)) new_seq = replace( project.sequences[0], video_tracks=(new_track,), ) new_project = replace(project, sequences=(new_seq,)) after = assign_identities(new_project) clip_ids_after = [ic.clip_id for ic in after.sequences[0].tracks[0].clips] assert clip_ids_before == clip_ids_after def test_trim_source_changes_clip_id(self, fixture_repo: pathlib.Path) -> None: raw = load_otio_json(fixture_repo / "single_track.otio") project = parse_otio_to_project(raw, fixture_repo / "single_track.otio", fixture_repo) before = assign_identities(project) cid_before = before.sequences[0].tracks[0].clips[0].clip_id from dataclasses import replace clip = project.sequences[0].video_tracks[0].items[0] assert isinstance(clip, Clip) new_sr = TimeRange( clip.source_range.start_time, RationalTime(clip.source_range.duration.value + 10, clip.source_range.duration.rate), ) new_clip = replace(clip, source_range=new_sr) items = list(project.sequences[0].video_tracks[0].items) items[0] = new_clip new_track = replace(project.sequences[0].video_tracks[0], items=tuple(items)) new_seq = replace(project.sequences[0], video_tracks=(new_track,)) new_project = replace(project, sequences=(new_seq,)) after = assign_identities(new_project) assert after.sequences[0].tracks[0].clips[0].clip_id != cid_before class TestVID10TrackIdentity: def test_reorder_swaps_track_ids(self, fixture_repo: pathlib.Path) -> None: raw = load_otio_json(FIXTURES / "multi_track.otio") project = parse_otio_to_project(raw, FIXTURES / "multi_track.otio", fixture_repo) seq = project.sequences[0] seq_id = sequence_identity(project.project_key, seq.name, seq.ordinal) t0_id = track_identity(seq_id, "video", 0) t1_id = track_identity(seq_id, "video", 1) from dataclasses import replace vtracks = list(seq.video_tracks) vtracks[0], vtracks[1] = vtracks[1], vtracks[0] vtracks[0] = replace(vtracks[0], ordinal=0) vtracks[1] = replace(vtracks[1], ordinal=1) reordered = replace(seq, video_tracks=tuple(vtracks)) assert track_identity(seq_id, "video", 0) == t0_id assert track_identity(seq_id, "video", 1) == t1_id assert vtracks[0].name == "V2" assert vtracks[1].name == "V1" id_v1_after = track_identity(seq_id, "video", vtracks[1].ordinal) id_v2_after = track_identity(seq_id, "video", vtracks[0].ordinal) assert t0_id == id_v2_after assert t1_id == id_v1_after def test_edit_clip_preserves_track_id(self, fixture_repo: pathlib.Path) -> None: raw = load_otio_json(FIXTURES / "multi_track.otio") project = parse_otio_to_project(raw, FIXTURES / "multi_track.otio", fixture_repo) seq = project.sequences[0] tid = track_identity( sequence_identity(project.project_key, seq.name, seq.ordinal), "video", 0, ) from dataclasses import replace clip = seq.video_tracks[0].items[0] assert isinstance(clip, Clip) new_clip = replace(clip, name="renamed") items = list(seq.video_tracks[0].items) items[0] = new_clip new_track = replace(seq.video_tracks[0], items=tuple(items)) new_seq = replace(seq, video_tracks=(new_track, seq.video_tracks[1])) new_project = replace(project, sequences=(new_seq,)) tid_after = track_identity( sequence_identity(new_project.project_key, new_seq.name, new_seq.ordinal), "video", 0, ) assert tid == tid_after class TestVID11PositionalTiebreaker: def test_identical_shots_distinct_ids(self, fixture_repo: pathlib.Path) -> None: shutil.copy(FIXTURES / "identical_shots.otio", fixture_repo / "identical_shots.otio") raw = load_otio_json(fixture_repo / "identical_shots.otio") project = parse_otio_to_project(raw, fixture_repo / "identical_shots.otio", fixture_repo) identified = assign_identities(project) clips = identified.sequences[0].tracks[0].clips assert len(clips) == 2 assert clips[0].clip_id != clips[1].clip_id assert clips[0].occurrence == 0 assert clips[1].occurrence == 1 def test_stable_across_resnapshot(self, fixture_repo: pathlib.Path) -> None: shutil.copy(FIXTURES / "identical_shots.otio", fixture_repo / "identical_shots.otio") a = plugin.snapshot(fixture_repo) b = plugin.snapshot(fixture_repo) assert a == b def test_trim_leaves_identical_set(self, fixture_repo: pathlib.Path) -> None: raw = load_otio_json(FIXTURES / "identical_shots.otio") project = parse_otio_to_project(raw, FIXTURES / "identical_shots.otio", fixture_repo) track = project.sequences[0].video_tracks[0] tid = track_identity( sequence_identity(project.project_key, project.sequences[0].name, 0), "video", 0, ) clip = track.items[1] assert isinstance(clip, Clip) from dataclasses import replace new_sr = TimeRange( RationalTime(10, clip.source_range.start_time.rate), clip.source_range.duration, ) new_clip = replace(clip, source_range=new_sr) items = list(track.items) items[1] = new_clip occ = assign_clip_occurrences(tid, tuple(items)) assert occ[1][1] == 0 def _build_manifest(repo: pathlib.Path, otio_name: str) -> JsonObject: snap = plugin.snapshot(repo) raw = load_otio_json(repo / otio_name) project = parse_otio_to_project(raw, repo / otio_name, repo) identified = assign_identities(project) manifest = build_timeline_manifest( snap["files"], {otio_name: identified}, {otio_name: project_content_id(project)}, snapshot_id="golden", ) return {"files": snap["files"], "manifest": manifest} class TestVID12GoldenSingleTrack: def test_matches_golden(self, fixture_repo: pathlib.Path) -> None: built = _build_manifest(fixture_repo, "single_track.otio") golden_path = GOLDEN / "single_track.manifest.json" if not golden_path.exists(): golden_path.parent.mkdir(parents=True, exist_ok=True) golden_path.write_text(json.dumps(built, indent=2, sort_keys=True) + "\n") golden = json.loads(golden_path.read_text()) assert built["files"] == golden["files"] class TestVID13GoldenMultiTrack: def test_matches_golden(self, fixture_repo: pathlib.Path) -> None: shutil.copy(FIXTURES / "multi_track.otio", fixture_repo / "multi_track.otio") built = _build_manifest(fixture_repo, "multi_track.otio") golden_path = GOLDEN / "multi_track.manifest.json" if not golden_path.exists(): golden_path.parent.mkdir(parents=True, exist_ok=True) golden_path.write_text(json.dumps(built, indent=2, sort_keys=True) + "\n") golden = json.loads(golden_path.read_text()) assert built["files"] == golden["files"] class TestVID14Security: def test_offline_media_deterministic(self, tmp_path: pathlib.Path) -> None: repo = tmp_path / "offline" repo.mkdir() shutil.copy(FIXTURES / "offline_media.otio", repo / "offline_media.otio") a = plugin.snapshot(repo) b = plugin.snapshot(repo) assert a == b raw = load_otio_json(repo / "offline_media.otio") project = parse_otio_to_project(raw, repo / "offline_media.otio", repo) ref = project.media_pool[0].references[0][1] assert ref.is_offline def test_path_traversal_rejected(self) -> None: bad = { "OTIO_SCHEMA": "Timeline.1", "name": "bad", "metadata": {"muse.project_key": "bad"}, "tracks": { "OTIO_SCHEMA": "Stack.1", "children": [ { "OTIO_SCHEMA": "Track.1", "kind": "Video", "children": [ { "OTIO_SCHEMA": "Clip.2", "name": "x", "media_reference": { "OTIO_SCHEMA": "ExternalReference.1", "target_url": "../etc/passwd", }, "source_range": { "OTIO_SCHEMA": "TimeRange.1", "start_time": {"OTIO_SCHEMA": "RationalTime.1", "rate": 24, "value": 0}, "duration": {"OTIO_SCHEMA": "RationalTime.1", "rate": 24, "value": 1}, }, } ], } ], }, } with pytest.raises(BridgeError): parse_otio_to_project(bad, pathlib.Path("bad.otio"), None) def test_no_media_bytes_in_objects(self, fixture_repo: pathlib.Path, tmp_path: pathlib.Path) -> None: muse_repo = tmp_path / "muse_repo" muse_repo.mkdir() shutil.copytree(fixture_repo / "media", muse_repo / "media") shutil.copy(fixture_repo / "single_track.otio", muse_repo / "single_track.otio") from muse.core.paths import objects_dir objects_dir(muse_repo).mkdir(parents=True, exist_ok=True) snap = plugin.snapshot(muse_repo) for digest in snap["files"].values(): blob_path = objects_dir(muse_repo) / digest.split(":", 1)[1] if blob_path.exists(): data = blob_path.read_bytes() assert b"stub" not in data