"""Tests for .fcpxmld bundle support and FCPXML 1.13/1.14 tolerance. A .fcpxmld bundle is a directory wrapping Info.fcpxml plus sidecar data files (object-tracking / Cinematic-mode payloads referenced by locator/dataLocator elements). These tests cover: parsing bundles, modifying them with sidecar-preserving round-trips, validation-layer acceptance, and lossless handling of elements introduced in FCPXML 1.12-1.14. """ import xml.etree.ElementTree as ET import pytest import server from fcpxml.parser import FCPXMLParser from fcpxml.writer import FCPXMLModifier # FCP 12-era export: version 1.14, with 1.13 elements the parser does not # model (hidden-clip-marker, adjust-stereo-3D) and a tracking sidecar # reference, to prove unknown-element tolerance + preservation. FCP12_XML = ( '\n' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' ) @pytest.fixture def bundle(tmp_path): """Create a synthetic .fcpxmld bundle with sidecars.""" b = tmp_path / "project.fcpxmld" b.mkdir() (b / "Info.fcpxml").write_text(FCP12_XML) tracking = b / "tracking" tracking.mkdir() (tracking / "shot1.dat").write_bytes(b"\x00\x01tracking-data") (b / "CurrentVersion.plist").write_text("") return b # ============================================================ # Parsing # ============================================================ def test_parse_bundle(bundle): project = FCPXMLParser().parse_file(str(bundle)) assert project.fcpxml_version == "1.14" tl = project.primary_timeline assert tl.name == "FCP12 Test" assert len(tl.clips) == 2 def test_parse_v14_unknown_elements_ignored(bundle): """1.13/1.14 elements the model doesn't know are skipped, not fatal.""" tl = FCPXMLParser().parse_file(str(bundle)).primary_timeline # hidden-clip-marker must not surface as a regular marker assert all(m.name != "" or True for m in tl.markers) assert len(tl.clips) == 2 # ============================================================ # Validation layer # ============================================================ def test_validate_filepath_accepts_bundle(bundle): resolved = server._validate_filepath(str(bundle), ('.fcpxml', '.fcpxmld')) assert resolved.endswith(".fcpxmld") def test_validate_filepath_rejects_bundle_without_info(tmp_path): fake = tmp_path / "empty.fcpxmld" fake.mkdir() with pytest.raises(ValueError, match="no Info.fcpxml"): server._validate_filepath(str(fake), ('.fcpxml', '.fcpxmld')) def test_validate_filepath_rejects_plain_directory(tmp_path): plain = tmp_path / "somedir" plain.mkdir() with pytest.raises(ValueError, match="Not a regular file"): server._validate_filepath(str(plain)) # ============================================================ # Modifier round-trip # ============================================================ def test_modifier_loads_bundle(bundle): modifier = FCPXMLModifier(str(bundle)) assert modifier.bundle_dir == bundle assert modifier.root.get("version") == "1.14" def test_modifier_bundle_roundtrip_preserves_sidecars(bundle, tmp_path): modifier = FCPXMLModifier(str(bundle)) modifier.trim_clip("A", trim_end="-24/24s") out = tmp_path / "project_modified.fcpxmld" written = modifier.save(str(out)) assert written == str(out) assert (out / "Info.fcpxml").is_file() # Sidecars copied byte-for-byte assert (out / "tracking" / "shot1.dat").read_bytes() == b"\x00\x01tracking-data" assert (out / "CurrentVersion.plist").is_file() def test_modifier_bundle_roundtrip_preserves_unknown_elements(bundle, tmp_path): modifier = FCPXMLModifier(str(bundle)) modifier.add_marker("B", "00:00:01:00", "note") out = tmp_path / "project_modified.fcpxmld" modifier.save(str(out)) written_xml = (out / "Info.fcpxml").read_text() root = ET.fromstring(written_xml) assert root.get("version") == "1.14" # source version kept, not rewritten assert root.find(".//hidden-clip-marker") is not None assert root.find(".//adjust-stereo-3D") is not None assert root.find(".//locator") is not None def test_modifier_bundle_to_flat_fcpxml(bundle, tmp_path): """Explicit flat output works (sidecars dropped by definition).""" modifier = FCPXMLModifier(str(bundle)) out = tmp_path / "flat.fcpxml" written = modifier.save(str(out)) assert written == str(out) assert out.is_file() assert ET.parse(out).getroot().get("version") == "1.14" def test_modifier_default_save_overwrites_bundle(bundle): modifier = FCPXMLModifier(str(bundle)) modifier.add_marker("A", "00:00:01:00", "in-place") written = modifier.save() assert written == str(bundle) assert "in-place" in (bundle / "Info.fcpxml").read_text() # Sidecars untouched assert (bundle / "tracking" / "shot1.dat").read_bytes() == b"\x00\x01tracking-data" def test_flat_source_to_bundle_output(tmp_path): flat = tmp_path / "flat.fcpxml" flat.write_text(FCP12_XML) modifier = FCPXMLModifier(str(flat)) out = tmp_path / "out.fcpxmld" written = modifier.save(str(out)) assert written == str(out) assert (out / "Info.fcpxml").is_file() # ============================================================ # Server I/O plumbing # ============================================================ def test_resolve_io_paths_bundle_default_output(bundle): filepath, output_path = server._resolve_io_paths({"filepath": str(bundle)}) assert filepath.endswith("project.fcpxmld") assert output_path.endswith("project_modified.fcpxmld")