"""Tests for fcpxml/export.py — DaVinci Resolve and XMEML export. Covers: attribute stripping, compound clip flattening, audio track generation, lane mapping, file path handling, no-timeline error, doctype injection, NTSC detection. """ import os import tempfile import xml.etree.ElementTree as ET import pytest from fcpxml.export import DaVinciExporter from fcpxml.safe_xml import serialize_xml BASE_XML = """ """ CONNECTED_XML = """ """ COMPOUND_XML = """ """ def _tmp(xml: str) -> str: fd, path = tempfile.mkstemp(suffix='.fcpxml') with os.fdopen(fd, 'w') as f: f.write(xml) return path def _out(suffix='.fcpxml') -> str: fd, path = tempfile.mkstemp(suffix=suffix) os.close(fd) return path class TestAttributeStripping: """Simplified FCPXML should strip Resolve-incompatible attributes.""" def test_moddate_removed(self): src = _tmp(BASE_XML) out = _out() try: exp = DaVinciExporter(src) exp.export_simplified_fcpxml(out) tree = ET.parse(out) for elem in tree.iter(): assert 'modDate' not in elem.attrib finally: os.unlink(src) os.unlink(out) def test_version_downgraded_to_1_9(self): src = _tmp(BASE_XML) out = _out() try: exp = DaVinciExporter(src) exp.export_simplified_fcpxml(out) tree = ET.parse(out) assert tree.getroot().get('version') == '1.9' finally: os.unlink(src) os.unlink(out) def test_colorprocessing_stripped(self): xml = BASE_XML.replace('', '') src = _tmp(xml) out = _out() try: exp = DaVinciExporter(src) exp.export_simplified_fcpxml(out) tree = ET.parse(out) assert 'colorProcessing' not in tree.getroot().attrib finally: os.unlink(src) os.unlink(out) class TestCompoundClipFlattening: """ref-clip → asset-clip conversion when flatten_compounds=True.""" def test_ref_clip_becomes_asset_clip(self): src = _tmp(COMPOUND_XML) out = _out() try: exp = DaVinciExporter(src) exp.export_simplified_fcpxml(out, flatten_compounds=True) tree = ET.parse(out) ref_clips = tree.findall('.//ref-clip') asset_clips = tree.findall('.//asset-clip') assert len(ref_clips) == 0, "ref-clip should be converted" assert len(asset_clips) >= 1 finally: os.unlink(src) os.unlink(out) def test_flatten_strips_compound_attrs(self): """srcEnable and other compound-specific attrs should be removed.""" src = _tmp(COMPOUND_XML) out = _out() try: exp = DaVinciExporter(src) exp.export_simplified_fcpxml(out, flatten_compounds=True) tree = ET.parse(out) for clip in tree.findall('.//asset-clip'): assert 'srcEnable' not in clip.attrib finally: os.unlink(src) os.unlink(out) def test_no_flatten_preserves_ref_clip(self): src = _tmp(COMPOUND_XML) out = _out() try: exp = DaVinciExporter(src) exp.export_simplified_fcpxml(out, flatten_compounds=False) tree = ET.parse(out) ref_clips = tree.findall('.//ref-clip') assert len(ref_clips) >= 1, "ref-clip should be preserved" finally: os.unlink(src) os.unlink(out) class TestXmemlAudioTracks: """Audio tracks from connected clips with negative lanes.""" def test_audio_track_generated_from_negative_lane(self): src = _tmp(CONNECTED_XML) out = _out('.xml') try: exp = DaVinciExporter(src) exp.export_xmeml(out) tree = ET.parse(out) audio = tree.find('.//audio') assert audio is not None audio_tracks = audio.findall('track') assert len(audio_tracks) >= 1 finally: os.unlink(src) os.unlink(out) def test_primary_storyline_audio_fallback(self): """When no connected audio clips, primary storyline creates audio track.""" src = _tmp(BASE_XML) out = _out('.xml') try: exp = DaVinciExporter(src) exp.export_xmeml(out) tree = ET.parse(out) audio = tree.find('.//audio') tracks = audio.findall('track') assert len(tracks) >= 1 clipitems = tracks[0].findall('clipitem') assert len(clipitems) == 2 # Both primary clips have audio finally: os.unlink(src) os.unlink(out) class TestXmemlFilePathHandling: """Clipitem file/pathurl elements from media_path.""" def test_clipitem_has_file_pathurl(self): src = _tmp(BASE_XML) out = _out('.xml') try: exp = DaVinciExporter(src) exp.export_xmeml(out) tree = ET.parse(out) clipitems = tree.findall('.//clipitem') found_path = False for ci in clipitems: file_elem = ci.find('file') if file_elem is not None: pathurl = file_elem.find('pathurl') if pathurl is not None and pathurl.text: found_path = True break assert found_path, "At least one clipitem should have file/pathurl" finally: os.unlink(src) os.unlink(out) class TestNoTimelineError: """Export should fail cleanly when FCPXML has no timeline.""" NO_TL_XML = """ """ def test_xmeml_raises_on_no_timeline(self): src = _tmp(self.NO_TL_XML) out = _out('.xml') try: exp = DaVinciExporter(src) with pytest.raises(ValueError, match="No timeline"): exp.export_xmeml(out) finally: os.unlink(src) if os.path.exists(out): os.unlink(out) class TestDoctypeInjection: """serialize_xml should inject DOCTYPE correctly.""" def test_doctype_inserted(self): root = ET.Element('fcpxml') root.set('version', '1.9') out = _out() try: serialize_xml(root, out, '') with open(out) as f: content = f.read() assert '' in content assert '' in content finally: os.unlink(out) def test_no_doctype(self): root = ET.Element('test') out = _out() try: serialize_xml(root, out, '') with open(out) as f: content = f.read() assert '