203 lines
7.1 KiB
Python
Executable File
203 lines
7.1 KiB
Python
Executable File
"""Tests for Live mode v1 (v0.9.0) — push_to_fcp / list_fcp_libraries.
|
|
|
|
osascript and pgrep are mocked throughout: unit tests must never launch
|
|
Final Cut Pro. The import-options injection path is real (and
|
|
DTD-checked in test_dtd_validation.py's environment when available).
|
|
"""
|
|
|
|
import subprocess
|
|
import xml.etree.ElementTree as ET
|
|
|
|
import pytest
|
|
|
|
import server
|
|
from fcpxml import live
|
|
from fcpxml.live import inject_import_options, list_fcp_libraries, push_to_fcp
|
|
|
|
SIMPLE_XML = (
|
|
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
'<fcpxml version="1.13"><resources>'
|
|
'<format id="r1" frameDuration="1/24s" width="1920" height="1080"/>'
|
|
'</resources>'
|
|
'<library><event name="Evt"><project name="Live Test">'
|
|
'<sequence format="r1" duration="240/24s"><spine>'
|
|
'<gap name="Gap" offset="0s" duration="240/24s"/>'
|
|
'</spine></sequence></project></event></library></fcpxml>'
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def project_file(tmp_path):
|
|
f = tmp_path / "live.fcpxml"
|
|
f.write_text(SIMPLE_XML)
|
|
return f
|
|
|
|
|
|
def _fake_proc(returncode=0, stdout="", stderr=""):
|
|
return subprocess.CompletedProcess(
|
|
args=["osascript"], returncode=returncode, stdout=stdout, stderr=stderr
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# inject_import_options
|
|
# ============================================================
|
|
|
|
def test_inject_import_options_first_child(project_file, tmp_path):
|
|
out = tmp_path / "live_import.fcpxml"
|
|
inject_import_options(
|
|
str(project_file), str(out),
|
|
library_location="/Users/me/Movies/Test.fcplibrary",
|
|
suppress_warnings=True,
|
|
copy_assets=False,
|
|
)
|
|
root = ET.parse(out).getroot()
|
|
first = list(root)[0]
|
|
assert first.tag == "import-options"
|
|
opts = {o.get("key"): o.get("value") for o in first.findall("option")}
|
|
# location is normalized to .fcpbundle so FCP auto-creates instead of
|
|
# raising the modal "Open Library" picker (live-verified on FCP 12.2)
|
|
assert opts["library location"].startswith("file:///Users/me/Movies/")
|
|
assert opts["library location"].endswith("Test.fcpbundle")
|
|
assert opts["suppress warnings"] == "1"
|
|
assert opts["copy assets"] == "0"
|
|
|
|
|
|
def test_inject_normalizes_to_fcpbundle(project_file, tmp_path):
|
|
out = tmp_path / "n.fcpxml"
|
|
inject_import_options(
|
|
str(project_file), str(out), library_location="/tmp/Bare/MyLib"
|
|
)
|
|
loc = ET.parse(out).getroot().find(
|
|
"import-options/option[@key='library location']"
|
|
).get("value")
|
|
assert loc.endswith("MyLib.fcpbundle")
|
|
|
|
|
|
def test_inject_import_options_replaces_existing(project_file, tmp_path):
|
|
out1 = tmp_path / "a.fcpxml"
|
|
inject_import_options(str(project_file), str(out1), suppress_warnings=False)
|
|
out2 = tmp_path / "b.fcpxml"
|
|
inject_import_options(str(out1), str(out2), suppress_warnings=True)
|
|
root = ET.parse(out2).getroot()
|
|
assert len(root.findall("import-options")) == 1
|
|
|
|
|
|
def test_inject_percent_encodes_spaces(project_file, tmp_path):
|
|
out = tmp_path / "c.fcpxml"
|
|
inject_import_options(
|
|
str(project_file), str(out),
|
|
library_location="/Users/me/My Movies/Lib.fcplibrary",
|
|
)
|
|
root = ET.parse(out).getroot()
|
|
loc = root.find("import-options/option[@key='library location']").get("value")
|
|
assert "%20" in loc and " " not in loc
|
|
|
|
|
|
# ============================================================
|
|
# push_to_fcp (osascript mocked)
|
|
# ============================================================
|
|
|
|
def test_push_sends_options_copy(project_file, tmp_path, monkeypatch):
|
|
calls = []
|
|
|
|
def fake_run(script):
|
|
calls.append(script)
|
|
return _fake_proc()
|
|
|
|
monkeypatch.setattr(live, "_run_osascript", fake_run)
|
|
monkeypatch.setattr(live, "fcp_is_running", lambda: True)
|
|
|
|
copy_path = str(tmp_path / "live_import.fcpxml")
|
|
result = push_to_fcp(
|
|
str(project_file),
|
|
library_location="/tmp/Lib.fcplibrary",
|
|
import_copy_path=copy_path,
|
|
)
|
|
assert result["sent"] == copy_path
|
|
assert result["launched_fcp"] is False
|
|
assert 'tell application "Final Cut Pro"' in calls[0]
|
|
assert "live_import.fcpxml" in calls[0]
|
|
|
|
|
|
def test_push_reports_launch(project_file, tmp_path, monkeypatch):
|
|
monkeypatch.setattr(live, "_run_osascript", lambda s: _fake_proc())
|
|
monkeypatch.setattr(live, "fcp_is_running", lambda: False)
|
|
result = push_to_fcp(
|
|
str(project_file), import_copy_path=str(tmp_path / "x.fcpxml")
|
|
)
|
|
assert result["launched_fcp"] is True
|
|
|
|
|
|
def test_push_permission_error_hint(project_file, tmp_path, monkeypatch):
|
|
monkeypatch.setattr(
|
|
live, "_run_osascript",
|
|
lambda s: _fake_proc(returncode=1, stderr="Not authorized (-1743)"),
|
|
)
|
|
monkeypatch.setattr(live, "fcp_is_running", lambda: True)
|
|
with pytest.raises(RuntimeError, match="Automation permission"):
|
|
push_to_fcp(str(project_file), import_copy_path=str(tmp_path / "x.fcpxml"))
|
|
|
|
|
|
def test_push_escapes_applescript_quotes(tmp_path, monkeypatch):
|
|
evil = tmp_path / 'a"b.fcpxml'
|
|
evil.write_text(SIMPLE_XML)
|
|
captured = {}
|
|
monkeypatch.setattr(
|
|
live, "_run_osascript",
|
|
lambda s: captured.setdefault("script", s) and _fake_proc() or _fake_proc(),
|
|
)
|
|
monkeypatch.setattr(live, "fcp_is_running", lambda: True)
|
|
push_to_fcp(str(evil))
|
|
assert '\\"' in captured["script"]
|
|
|
|
|
|
# ============================================================
|
|
# list_fcp_libraries (osascript mocked)
|
|
# ============================================================
|
|
|
|
def test_list_refuses_to_launch(monkeypatch):
|
|
monkeypatch.setattr(live, "fcp_is_running", lambda: False)
|
|
with pytest.raises(RuntimeError, match="not running"):
|
|
list_fcp_libraries()
|
|
|
|
|
|
def test_list_parses_records(monkeypatch):
|
|
monkeypatch.setattr(live, "fcp_is_running", lambda: True)
|
|
payload = (
|
|
"LibA\x1fEvt1\x1fProj1\x1fProj2\x1f\x1e"
|
|
"LibA\x1fEvt2\x1f\x1e"
|
|
"LibB\x1f\x1e"
|
|
)
|
|
monkeypatch.setattr(live, "_run_osascript", lambda s: _fake_proc(stdout=payload))
|
|
libs = list_fcp_libraries()
|
|
by_name = {lib["name"]: lib for lib in libs}
|
|
assert by_name["LibA"]["events"][0]["projects"] == ["Proj1", "Proj2"]
|
|
assert by_name["LibA"]["events"][1]["name"] == "Evt2"
|
|
assert by_name["LibB"]["events"] == []
|
|
|
|
|
|
# ============================================================
|
|
# MCP handlers
|
|
# ============================================================
|
|
|
|
async def test_handler_push(project_file, monkeypatch):
|
|
monkeypatch.setattr(live, "_run_osascript", lambda s: _fake_proc())
|
|
monkeypatch.setattr(live, "fcp_is_running", lambda: True)
|
|
result = await server.handle_push_to_fcp({"filepath": str(project_file)})
|
|
text = result[0].text
|
|
assert "Sent to Final Cut Pro" in text
|
|
assert "_import" in text # options copy, original untouched
|
|
assert "Export XML" in text # read-back asymmetry stated
|
|
|
|
|
|
async def test_handler_list_not_running(monkeypatch):
|
|
monkeypatch.setattr(live, "fcp_is_running", lambda: False)
|
|
result = await server.handle_list_fcp_libraries({})
|
|
assert "not running" in result[0].text
|
|
|
|
|
|
def test_live_tools_registered():
|
|
assert "push_to_fcp" in server.TOOL_HANDLERS
|
|
assert "list_fcp_libraries" in server.TOOL_HANDLERS
|