Files
gart/code/tests/test_validation.py
T

351 lines
12 KiB
Python
Executable File

"""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():
"""<note> 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(
'<fcpxml><asset-clip name="A">'
'<note/><marker/><filter-video/>'
'</asset-clip></fcpxml>'
)
assert _check_child_order(root) == []
def test_check_child_order_detects_violation():
"""filter-video before marker violates DTD ordering."""
root = ET.fromstring(
'<fcpxml><asset-clip name="Bad">'
'<filter-video ref="r1"/><marker value="x"/>'
'</asset-clip></fcpxml>'
)
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(
'<fcpxml><clip name="Solo"><marker/></clip></fcpxml>'
)
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(
'<fcpxml><spine><filter-video/><marker/></spine></fcpxml>'
)
assert _check_child_order(root) == []
# ── _check_required_attributes ──────────────────────────────────────
def test_required_attrs_all_present():
root = ET.fromstring(
'<fcpxml><asset-clip ref="r1" duration="100/2400s" name="A"/>'
'<format id="r1"/></fcpxml>'
)
assert _check_required_attributes(root) == []
def test_required_attrs_missing_ref_on_asset_clip():
root = ET.fromstring(
'<fcpxml><asset-clip duration="100/2400s" name="No Ref"/></fcpxml>'
)
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('<fcpxml><format name="HD"/></fcpxml>')
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('<fcpxml><transition/></fcpxml>')
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(
'<fcpxml><clip offset="100/2400s" duration="2400/2400s"/></fcpxml>'
)
assert _check_timebases(root) == []
def test_timebases_flags_nonstandard():
"""Denominator 7 is not a standard FCPXML timebase."""
root = ET.fromstring(
'<fcpxml><clip offset="3/7s" name="Weird"/></fcpxml>'
)
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(
'<fcpxml>'
'<clip offset="3/7s" name="A"/>'
'<clip offset="3/7s" name="B"/>'
'</fcpxml>'
)
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(
'<fcpxml><clip duration="100/24s" name="Aligned"/></fcpxml>'
)
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(
'<fcpxml><clip duration="7/48s" name="Half Frame"/></fcpxml>'
)
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(
'<fcpxml><format duration="7/48s"/></fcpxml>'
)
assert _check_frame_alignment(root, fps=24.0) == []
# ── _check_effect_refs ──────────────────────────────────────────────
def test_effect_refs_valid():
root = ET.fromstring(
'<fcpxml><resources><effect id="e1"/></resources>'
'<clip><filter-video ref="e1"/></clip></fcpxml>'
)
assert _check_effect_refs(root) == []
def test_effect_refs_dangling():
root = ET.fromstring(
'<fcpxml><clip><filter-video ref="missing_effect"/></clip></fcpxml>'
)
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(
'<fcpxml><resources>'
'<asset id="r1" src="file:///video.mov" name="V"/>'
'</resources></fcpxml>'
)
assert _check_asset_sources(root) == []
def test_asset_with_media_rep():
root = ET.fromstring(
'<fcpxml><resources>'
'<asset id="r1" name="V"><media-rep/></asset>'
'</resources></fcpxml>'
)
assert _check_asset_sources(root) == []
def test_asset_missing_both():
root = ET.fromstring(
'<fcpxml><resources>'
'<asset id="r1" name="Orphan"/>'
'</resources></fcpxml>'
)
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('<fcpxml><clip offset="3/7s"/></fcpxml>')
_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('<fcpxml><clip offset="3600/2400s"/></fcpxml>')
_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('<fcpxml><clip offset="garbage/0s"/></fcpxml>')
_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(
'<fcpxml>'
'<resources><asset id="r1" name="No Source"/></resources>'
'<asset-clip ref="r1" duration="7/48s" name="Clip">'
'<filter-video ref="ghost"/><marker value="x"/>'
'</asset-clip>'
'</fcpxml>'
)
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(
'<fcpxml>'
'<resources><effect id="e1"/>'
'<asset id="r1" src="file:///v.mov"/></resources>'
'<asset-clip ref="r1" duration="100/2400s" name="OK">'
'<marker value="ch1"/><filter-video ref="e1"/>'
'</asset-clip>'
'</fcpxml>'
)
assert validate_fcpxml(root) == []