"""Tests for FCPXML validation functions and DTD infrastructure. Covers the 6 _check_*() validators, _dtd_insert() ordering, _enforce_standard_timebases() snapping, and validate_fcpxml() orchestration. These are critical output-correctness guards — bugs here mean FCP import failures. """ import xml.etree.ElementTree as ET from fcpxml.models import ValidationIssueType from fcpxml.writer import ( _check_asset_sources, _check_child_order, _check_effect_refs, _check_frame_alignment, _check_required_attributes, _check_timebases, _dtd_insert, _enforce_standard_timebases, _sanitize_xml_value, validate_fcpxml, ) # ── _dtd_insert: DTD-ordered element insertion ────────────────────── def test_dtd_insert_marker_before_filter(): """Marker must appear before filter-video per DTD ordering.""" clip = ET.Element("asset-clip") ET.SubElement(clip, "filter-video") marker = ET.Element("marker", value="test") _dtd_insert(clip, marker) tags = [c.tag for c in clip] assert tags.index("marker") < tags.index("filter-video") def test_dtd_insert_note_always_first(): """ has lowest priority — always inserted at position 0.""" clip = ET.Element("clip") ET.SubElement(clip, "marker") ET.SubElement(clip, "filter-audio") note = ET.Element("note") _dtd_insert(clip, note) assert clip[0].tag == "note" def test_dtd_insert_unknown_tag_appended(): """Unknown tags get appended at the end.""" clip = ET.Element("asset-clip") ET.SubElement(clip, "note") ET.SubElement(clip, "marker") custom = ET.Element("custom-extension") _dtd_insert(clip, custom) assert clip[-1].tag == "custom-extension" def test_dtd_insert_into_empty_parent(): """Inserting into an empty element works without error.""" clip = ET.Element("clip") marker = ET.Element("marker", value="first") _dtd_insert(clip, marker) assert len(clip) == 1 assert clip[0].tag == "marker" def test_dtd_insert_preserves_between_existing(): """Insert conform-rate between note and marker — correct middle position.""" clip = ET.Element("asset-clip") ET.SubElement(clip, "note") ET.SubElement(clip, "marker") ET.SubElement(clip, "filter-video") conform = ET.Element("conform-rate") _dtd_insert(clip, conform) tags = [c.tag for c in clip] assert tags == ["note", "conform-rate", "marker", "filter-video"] # ── _check_child_order: DTD ordering violations ───────────────────── def test_check_child_order_valid(): root = ET.fromstring( '' '' '' ) assert _check_child_order(root) == [] def test_check_child_order_detects_violation(): """filter-video before marker violates DTD ordering.""" root = ET.fromstring( '' '' '' ) issues = _check_child_order(root) assert len(issues) == 1 assert issues[0].issue_type == ValidationIssueType.ELEMENT_ORDER assert "Bad" in issues[0].message def test_check_child_order_skips_single_child(): """Single-child elements can't be out of order.""" root = ET.fromstring( '' ) assert _check_child_order(root) == [] def test_check_child_order_ignores_non_clip_parents(): """Only clip/asset-clip/video/audio/ref-clip are checked.""" root = ET.fromstring( '' ) assert _check_child_order(root) == [] # ── _check_required_attributes ────────────────────────────────────── def test_required_attrs_all_present(): root = ET.fromstring( '' '' ) assert _check_required_attributes(root) == [] def test_required_attrs_missing_ref_on_asset_clip(): root = ET.fromstring( '' ) issues = _check_required_attributes(root) types = [i.issue_type for i in issues] assert ValidationIssueType.MISSING_ATTRIBUTE in types assert any("ref" in i.message for i in issues) def test_required_attrs_missing_id_on_format(): root = ET.fromstring('') issues = _check_required_attributes(root) assert len(issues) == 1 assert "id" in issues[0].message def test_required_attrs_transition_missing_all(): """Transition missing name, offset, duration → 3 issues.""" root = ET.fromstring('') issues = _check_required_attributes(root) assert len(issues) == 3 # ── _check_timebases ──────────────────────────────────────────────── def test_timebases_standard_ok(): """100/2400s simplifies to 1/24, 2400/2400s to 1/1 — both standard.""" root = ET.fromstring( '' ) assert _check_timebases(root) == [] def test_timebases_flags_nonstandard(): """Denominator 7 is not a standard FCPXML timebase.""" root = ET.fromstring( '' ) issues = _check_timebases(root) assert len(issues) == 1 assert issues[0].issue_type == ValidationIssueType.INVALID_TIMEBASE assert "7" in issues[0].message def test_timebases_deduplicates(): """Same non-standard value on same element+attr only reported once.""" root = ET.fromstring( '' '' '' '' ) issues = _check_timebases(root) # Both have same (clip, offset, 3/7s) key — only 1 issue assert len(issues) == 1 # ── _check_frame_alignment ────────────────────────────────────────── def test_frame_alignment_exact(): """100 frames at 24fps = 100/24s — frame-aligned.""" root = ET.fromstring( '' ) assert _check_frame_alignment(root, fps=24.0) == [] def test_frame_alignment_misaligned(): """7/24s = 7 frames — aligned. But 7/48s = 3.5 frames at 24fps — misaligned.""" root = ET.fromstring( '' ) issues = _check_frame_alignment(root, fps=24.0) assert len(issues) == 1 assert issues[0].issue_type == ValidationIssueType.FRAME_MISALIGNMENT def test_frame_alignment_ignores_non_clip_elements(): root = ET.fromstring( '' ) assert _check_frame_alignment(root, fps=24.0) == [] # ── _check_effect_refs ────────────────────────────────────────────── def test_effect_refs_valid(): root = ET.fromstring( '' '' ) assert _check_effect_refs(root) == [] def test_effect_refs_dangling(): root = ET.fromstring( '' ) issues = _check_effect_refs(root) assert len(issues) == 1 assert issues[0].issue_type == ValidationIssueType.MISSING_EFFECT_REF assert "missing_effect" in issues[0].message # ── _check_asset_sources ──────────────────────────────────────────── def test_asset_with_src(): root = ET.fromstring( '' '' '' ) assert _check_asset_sources(root) == [] def test_asset_with_media_rep(): root = ET.fromstring( '' '' '' ) assert _check_asset_sources(root) == [] def test_asset_missing_both(): root = ET.fromstring( '' '' '' ) issues = _check_asset_sources(root) assert len(issues) == 1 assert issues[0].issue_type == ValidationIssueType.MISSING_MEDIA_REP # ── _enforce_standard_timebases ───────────────────────────────────── def test_enforce_timebases_snaps_nonstandard(): root = ET.fromstring('') _enforce_standard_timebases(root) clip = root.find("clip") val = clip.get("offset") assert "/" in val and val.endswith("s") # Should be snapped to a standard timebase denominator denom = int(val.rstrip("s").split("/")[1]) assert denom in {1, 24, 25, 30, 48, 50, 60, 90, 96, 100, 120, 240, 600, 2400} def test_enforce_timebases_leaves_standard_alone(): root = ET.fromstring('') _enforce_standard_timebases(root) assert root.find("clip").get("offset") == "3600/2400s" def test_enforce_timebases_handles_unparseable(): """Malformed time values are silently skipped, not crashed on.""" root = ET.fromstring('') _enforce_standard_timebases(root) # Should not raise assert root.find("clip").get("offset") == "garbage/0s" # ── _sanitize_xml_value edge cases ────────────────────────────────── def test_sanitize_strips_null_bytes(): assert _sanitize_xml_value("he\x00llo") == "hello" def test_sanitize_preserves_tabs_newlines(): assert _sanitize_xml_value("line1\tline2\n") == "line1\tline2\n" def test_sanitize_enforces_max_length(): result = _sanitize_xml_value("x" * 500, max_length=100) assert len(result) == 100 def test_sanitize_coerces_non_string(): assert _sanitize_xml_value(42) == "42" # ── validate_fcpxml orchestration ─────────────────────────────────── def test_validate_fcpxml_aggregates_all_checks(): """Intentionally broken XML triggers multiple validator categories.""" root = ET.fromstring( '' '' '' '' '' '' ) issues = validate_fcpxml(root, fps=24.0) types = {i.issue_type for i in issues} # Should catch: ordering (filter before marker), missing media rep, # dangling effect ref, frame misalignment assert ValidationIssueType.ELEMENT_ORDER in types assert ValidationIssueType.MISSING_MEDIA_REP in types assert ValidationIssueType.MISSING_EFFECT_REF in types assert ValidationIssueType.FRAME_MISALIGNMENT in types def test_validate_fcpxml_clean_returns_empty(): root = ET.fromstring( '' '' '' '' '' '' '' ) assert validate_fcpxml(root) == []