//! Import of documents written in code with FilmCraft's writers, and export → re-import. use std::collections::HashMap; use std::sync::Arc; use effectcraft_keyframe::{Keyframe, Value}; use effectcraft_project::{Comp, Footage, FootageKind, ItemId, ItemKind, Layer, LayerSource, Project}; use effectcraft_time::{FrameRate, Tick}; use filmcraft_project as fp; use crate::*; const W: u32 = 640; const H: u32 = 360; fn secs(s: f64) -> filmcraft_time::Tick { filmcraft_time::Tick::from_seconds_f64(s) } fn t(s: f64) -> Tick { Tick::from_seconds_f64(s) } fn info(kind: filmcraft_media::MediaKind, video: bool, audio: bool) -> filmcraft_media::MediaInfo { filmcraft_media::MediaInfo { name: String::new(), kind, duration: secs(20.0), video: video.then(|| filmcraft_media::VideoStreamInfo { width: W, height: H, frame_rate: filmcraft_time::FrameRate { num: 25, den: 1 }, par: (1, 1), codec: "png".into(), pixel_format: String::new(), color: Default::default(), has_alpha: false, bitrate: None, }), audio: audio.then(|| filmcraft_media::AudioStreamInfo { sample_rate: 48_000, channels: 2, codec: "pcm".into(), bits_per_sample: Some(16) }), container: String::new(), start_timecode: None, file_size: None, } } fn media(p: &mut fp::Project, name: &str, path: &str, kind: filmcraft_media::MediaKind, video: bool, audio: bool, bin: Option) -> fp::ItemId { let clip = fp::MediaClip { media: fp::MediaRef::File { path: path.into() }, info: info(kind, video, audio), interpret: Default::default(), mark_in: None, mark_out: None, markers: vec![], offline: false, proxy: None, identity: None, }; p.add_item(name, fp::Label::Iris, fp::ItemKind::Media(clip), bin) } fn rate() -> filmcraft_time::FrameRate { filmcraft_time::FrameRate { num: 25, den: 1 } } fn item(p: &mut fp::Project, it: fp::ItemId, kind: fp::TrackKind, start: f64, src: f64, dur: f64) -> fp::TrackItem { p.make_track_item(it, kind, secs(start), filmcraft_time::TimeRange::new(secs(src), secs(dur)), rate()).unwrap() } /// A Premiere-like edit: two clips with a cross dissolve on V1, a speed-changed, scaled and /// faded clip on V2, a nested sequence on V3, and an audio clip at -6 dB on A1. fn edit() -> (fp::Project, fp::ItemId) { let mut p = fp::Project::new("Edit"); let bin = p.add_bin("Footage", None); let a = media(&mut p, "a.png", "/media/a.mov", filmcraft_media::MediaKind::Movie, true, false, Some(bin)); let b = media(&mut p, "b.png", "/media/b.mov", filmcraft_media::MediaKind::Movie, true, false, Some(bin)); let s = media(&mut p, "tone.wav", "/media/tone.wav", filmcraft_media::MediaKind::AudioOnly, false, true, Some(bin)); let settings = fp::SequenceSettings { width: W, height: H, frame_rate: rate(), ..Default::default() }; let nested = p.new_sequence("Nested", settings.clone(), 1, 0, None); let n_clip = item(&mut p, a, fp::TrackKind::Video, 0.0, 0.0, 2.0); p.sequence_mut(nested).unwrap().video_tracks[0].items.push(n_clip); let main = p.new_sequence("Main", settings, 3, 1, None); let ca = item(&mut p, a, fp::TrackKind::Video, 0.0, 0.0, 2.0); let cb = item(&mut p, b, fp::TrackKind::Video, 2.0, 1.0, 2.0); let tr = fp::Transition { id: fp::TransitionId(p.alloc_id()), effect: fp::find_effect("cross_dissolve").unwrap().instance(), start: secs(1.6), duration: secs(0.8), from: Some(ca.id), to: Some(cb.id), align: fp::TransitionAlign::CenterAtCut, reverse: false, }; let mut cc = item(&mut p, b, fp::TrackKind::Video, 1.0, 2.0, 2.0); cc.speed = 2.0; let m = cc.effect_mut("motion").unwrap(); m.params.insert("scale".into(), fp::Param::new(fp::ParamValue::Float(50.0))); m.params.insert("rotation".into(), fp::Param::new(fp::ParamValue::Float(30.0))); let mut pos = fp::Param::new(fp::ParamValue::Vec2(filmcraft_geom::Vec2 { x: 100.0, y: 100.0 })); pos.keyframes = vec![ fp::Keyframe::new(secs(0.0), fp::ParamValue::Vec2(filmcraft_geom::Vec2 { x: 100.0, y: 100.0 })), fp::Keyframe::new(secs(1.0), fp::ParamValue::Vec2(filmcraft_geom::Vec2 { x: 300.0, y: 200.0 })), ]; m.params.insert("position".into(), pos); cc.effect_mut("opacity").unwrap().params.insert("opacity".into(), fp::Param::new(fp::ParamValue::Float(50.0))); let cn = item(&mut p, nested, fp::TrackKind::Video, 4.0, 0.0, 2.0); let mut cs = item(&mut p, s, fp::TrackKind::Audio, 0.0, 0.0, 3.0); cs.effect_mut("volume").unwrap().params.insert("level".into(), fp::Param::new(fp::ParamValue::Float(-6.0))); let seq = p.sequence_mut(main).unwrap(); seq.video_tracks[0].items = vec![ca, cb]; seq.video_tracks[0].transitions = vec![tr]; seq.video_tracks[1].items = vec![cc]; seq.video_tracks[2].items = vec![cn]; seq.audio_tracks[0].items = vec![cs]; (p, main) } fn probe(path: &str) -> Option { let audio = path.ends_with(".wav"); Some(Footage { path: path.into(), kind: if audio { FootageKind::Audio } else { FootageKind::Video }, width: if audio { 0 } else { W }, height: if audio { 0 } else { H }, frame_rate: FrameRate::new(25, 1), duration: t(20.0), has_video: !audio, has_audio: audio, ..Default::default() }) } fn import_doc(bytes: &[u8], name: &str, probe_fn: &mut dyn FnMut(&str) -> Option) -> (Project, ImportResult) { let mut p = Project::default(); let r = import(&mut p, bytes, None, &ImportOptions { name: name.into(), ..Default::default() }, probe_fn).unwrap(); (p, r) } fn layer<'a>(c: &'a Comp, name: &str) -> &'a Layer { c.layers.iter().find(|l| l.name == name).unwrap_or_else(|| panic!("no layer {name}: {:?}", c.layers.iter().map(|l| &l.name).collect::>())) } fn near(a: Tick, b: Tick) -> bool { (a.0 - b.0).abs() <= Tick::from_seconds_f64(0.001).0 } fn check_structure(p: &Project, r: &ImportResult, motion: bool) { let main = r.comps.iter().copied().find(|c| p.item(*c).unwrap().name == "Main").expect("Main comp"); let c = p.comp(main).unwrap(); assert_eq!((c.width, c.height), (W, H)); assert_eq!(c.frame_rate, FrameRate::new(25, 1)); assert!(near(c.duration, t(6.0)), "{:?}", c.duration.seconds()); // V3 (nested) on top, then V2, then V1's later clip, then the earlier one; audio last. let names: Vec<&str> = c.layers.iter().map(|l| l.name.as_str()).collect(); let pos = |n: &str| names.iter().position(|x| x.starts_with(n)).unwrap_or_else(|| panic!("{n} in {names:?}")); assert!(pos("Nested") < pos("b.png"), "{names:?}"); let nested = layer(c, "Nested"); assert!(matches!(nested.source, LayerSource::Comp { .. })); assert!(near(nested.in_point, t(4.0)) && near(nested.out_point, t(6.0))); // The faster clip: 200 % speed → 50 % stretch, source 2 s at its in point. let fast = c.layers.iter().find(|l| (l.stretch - 50.0).abs() < 1e-6).expect("stretched layer"); assert!(near(fast.in_point, t(1.0)) && near(fast.out_point, t(3.0))); assert!(near(fast.layer_time(t(1.0)), t(2.0)), "{}", fast.layer_time(t(1.0)).seconds()); // V1: b above a, overlapping over the dissolve; b fades in. let v1: Vec<&Layer> = c.layers.iter().filter(|l| (l.stretch - 100.0).abs() < 1e-6 && matches!(l.source, LayerSource::Footage { .. }) && l.switches.video).collect(); assert_eq!(v1.len(), 2, "{names:?}"); let (lb, la) = (v1[0], v1[1]); assert!(near(la.in_point, t(0.0)) && near(la.out_point, t(2.4)), "{:?}", la.out_point.seconds()); assert!(near(lb.in_point, t(1.6)) && near(lb.out_point, t(4.0)), "{:?}", lb.in_point.seconds()); assert!(near(lb.layer_time(t(2.0)), t(1.0))); let op = lb.transform().unwrap().get("opacity").unwrap(); assert_eq!(op.keys.len(), 2); assert!(op.value_at(lb.layer_time(t(1.6))).as_f64() < 1e-6); assert!((op.value_at(lb.layer_time(t(2.0))).as_f64() - 50.0).abs() < 1.0); assert!((op.value_at(lb.layer_time(t(3.0))).as_f64() - 100.0).abs() < 1e-6); // Audio: an audio-only layer at -6 dB. let au = c.layers.iter().find(|l| !l.switches.video && l.switches.audio).expect("audio layer"); assert!(near(au.out_point, t(3.0))); if motion { let lv = au.props.prop("audio/levels").unwrap().value.as_vec2(); assert!((lv[0] + 6.0).abs() < 0.01, "{lv:?}"); let tr = fast.transform().unwrap(); assert_eq!(tr.get("scale").unwrap().value.as_vec3()[..2], [50.0, 50.0]); assert!((tr.get("rotation").unwrap().value.as_f64() - 30.0).abs() < 1e-6); assert!((tr.get("opacity").unwrap().value.as_f64() - 50.0).abs() < 1e-6); let pk = &tr.get("position").unwrap().keys; assert_eq!(pk.len(), 2); // Clip time 1 s = comp 2 s = layer time 4 s at 200 % speed. assert!(near(pk[1].time, fast.layer_time(t(2.0)))); assert_eq!(pk[1].value.as_vec3()[..2], [300.0, 200.0]); } // Everything lands in the document's folder. assert!(p.item(r.folder).unwrap().is_folder()); assert!(p.items.values().filter(|i| i.id != r.folder).all(|i| i.parent.is_some())); } /// A Premiere-style xmeml export with a bin, a master clip, Basic Motion and Opacity keyframes. const PREMIERE_XML: &str = r#" Promo Shots Day 1 wide.mov 250 25FALSE Promo Cut 50 25FALSE "#; #[test] fn premiere_xml_bins_motion_and_opacity() { let mut paths = vec![]; let (p, r) = import_doc(PREMIERE_XML.as_bytes(), "Promo.xml", &mut |path| { paths.push(path.to_string()); probe(path) }); assert_eq!(paths, vec!["/media/wide shot.mov".to_string()]); let shots = p.items.values().find(|i| i.name == "Shots").unwrap(); assert_eq!(shots.parent, Some(r.folder)); let day = p.items.values().find(|i| i.name == "Day 1").unwrap(); assert_eq!(day.parent, Some(shots.id)); assert!(p.items.values().any(|i| i.name == "wide.mov" && i.parent == Some(day.id))); let c = p.comp(r.comps[0]).unwrap(); assert_eq!(p.item(r.comps[0]).unwrap().name, "Promo Cut"); assert!(near(c.duration, t(2.0))); let l = &c.layers[0]; assert!(near(l.layer_time(Tick::ZERO), t(1.0)), "source in 1 s"); let tr = l.transform().unwrap(); assert_eq!(tr.get("scale").unwrap().value.as_vec3()[..2], [150.0, 150.0]); // Center 0.25 of the width right of the frame centre. assert_eq!(tr.get("position").unwrap().value.as_vec3()[..2], [320.0 + 160.0, 180.0]); let op = tr.get("opacity").unwrap(); assert_eq!(op.keys.len(), 2); assert!(near(op.keys[1].time, l.layer_time(t(1.0)))); assert!((op.value_at(l.layer_time(t(0.5))).as_f64() - 50.0).abs() < 1e-6); } #[test] fn imports_fcp7_xml_fcpxml_and_otio() { let (doc, main) = edit(); for (fmt, motion) in [(fc::Format::Fcp7Xml, true), (fc::Format::Fcpxml, false), (fc::Format::Otio, false)] { let (bytes, _) = fc::export(&doc, main, fmt, &Default::default()).unwrap(); let name = format!("Main.{}", fmt.extension()); assert_eq!(TimelineFormat::detect(&bytes, Some(&name)).map(|f| f.fc()), Some(fmt)); let (p, r) = import_doc(&bytes, &name, &mut probe); assert!(r.missing.is_empty(), "{fmt:?}: {:?}", r.missing); check_structure(&p, &r, motion); } } #[test] fn imports_aaf_and_omf() { let (doc, main) = edit(); for fmt in [fc::Format::Aaf, fc::Format::Omf] { let (bytes, _) = fc::export(&doc, main, fmt, &Default::default()).unwrap(); let name = format!("Main.{}", fmt.extension()); assert_eq!(TimelineFormat::detect(&bytes, Some(&name)).map(|f| f.fc()), Some(fmt)); let (p, r) = import_doc(&bytes, &name, &mut probe); let c = p.comp(r.comps[0]).unwrap(); if fmt == fc::Format::Aaf { assert_eq!((c.width, c.height), (W, H)); } // Audio: an audio-only layer for the A1 clip (OMF carries the audio tracks only). let au = c.layers.iter().find(|l| !l.switches.video && l.switches.audio).expect("audio layer"); assert!(near(au.in_point, Tick::ZERO) && near(au.out_point, t(3.0))); if fmt == fc::Format::Aaf { // The dissolve's clips overlap over the transition, the incoming one on top. let a = layer(c, "a.png"); assert!(near(a.out_point, t(2.4)), "{}", a.out_point.seconds()); let b = c.layers.iter().find(|l| near(l.in_point, t(1.6))).expect("incoming clip"); assert!(c.index_of(b.id) < c.index_of(a.id)); assert_eq!(b.transform().unwrap().get("opacity").unwrap().keys.len(), 2); } } } #[test] fn missing_media_become_placeholders() { let (doc, main) = edit(); let (bytes, _) = fc::export(&doc, main, fc::Format::Fcp7Xml, &Default::default()).unwrap(); let (p, r) = import_doc(&bytes, "Main.xml", &mut |_| None); assert_eq!(r.missing.len(), 3, "{:?}", r.missing); let a = p.items.values().find(|i| i.name == "a.png").unwrap(); match &a.kind { ItemKind::Footage(f) => assert!(f.missing && f.width == W && f.height == H && f.has_video), _ => panic!(), } check_structure(&p, &r, true); } #[test] fn edl_imports_one_track() { let (doc, main) = edit(); let (bytes, _) = fc::export(&doc, main, fc::Format::Edl, &Default::default()).unwrap(); let mut p = Project::default(); let r = import(&mut p, &bytes, None, &ImportOptions { name: "Main.edl".into(), edl_frame_rate: Some(25.0), ..Default::default() }, &mut probe).unwrap(); assert_eq!(r.format, Some(TimelineFormat::Edl)); let c = p.comp(r.comps[0]).unwrap(); assert!(c.layers.len() >= 2); } #[test] fn rejects_unknown_documents() { let mut p = Project::default(); assert!(matches!(import(&mut p, b"hello", None, &ImportOptions::default(), &mut probe), Err(Error::UnknownFormat))); } // ---------------------------------------------------------------- export fn footage_item(p: &mut Project, path: &str) -> ItemId { let f = probe(path).unwrap(); p.add_item(path.rsplit('/').next().unwrap(), effectcraft_color::Label::Aqua, None, ItemKind::Footage(f)) } fn comp_item(p: &mut Project, name: &str, c: Comp) -> ItemId { p.add_item(name, effectcraft_color::Label::Sandstone, None, ItemKind::Comp(Arc::new(c))) } /// A comp with a stretched, keyframed footage layer, a text layer, a precomp and an audio layer. fn ae_project() -> (Project, ItemId) { let mut p = Project::default(); let a = footage_item(&mut p, "/media/a.mov"); let s = footage_item(&mut p, "/media/tone.wav"); let mut inner = Comp::new(W, H, FrameRate::new(25, 1), t(4.0)); let li = effectcraft_project::build::layer(&mut p, &inner, "inner a", LayerSource::Footage { item: a }, (W, H), None); inner.layers.push(li); let inner_id = comp_item(&mut p, "Inner", inner); let mut c = Comp::new(W, H, FrameRate::new(25, 1), t(5.0)); let mut la = effectcraft_project::build::layer(&mut p, &c, "clip a", LayerSource::Footage { item: a }, (W, H), None); la.stretch = 200.0; // half speed la.start_time = t(-1.0); // layer time 1 s at comp 3 s… source 1 s at comp 1 s la.in_point = t(1.0); la.out_point = t(4.0); { let tr = la.transform_mut().unwrap(); let pos = tr.get_mut("position").unwrap(); pos.keys = vec![Keyframe::new(t(1.0), Value::Vec3([100.0, 120.0, 0.0])), Keyframe::new(t(2.0), Value::Vec3([500.0, 240.0, 0.0])).hold()]; tr.get_mut("scale").unwrap().value = Value::Vec3([80.0, 80.0, 100.0]); tr.get_mut("rotation").unwrap().value = Value::Scalar(-15.0); tr.get_mut("opacity").unwrap().value = Value::Scalar(75.0); } let mut lt = effectcraft_project::build::layer(&mut p, &c, "Title", LayerSource::Text, (W, H), None); lt.in_point = t(0.6); lt.out_point = t(2.6); let mut lp = effectcraft_project::build::layer(&mut p, &c, "Inner", LayerSource::Comp { item: inner_id }, (W, H), None); lp.start_time = t(2.0); lp.in_point = t(2.0); lp.out_point = t(5.0); let mut ls = effectcraft_project::build::layer(&mut p, &c, "tone", LayerSource::Footage { item: s }, (0, 0), None); ls.out_point = t(3.0); ls.props.prop_mut("audio/levels").unwrap().value = Value::Vec2([-3.0, -3.0]); c.layers = vec![lt, lp, la, ls]; let cid = comp_item(&mut p, "Main", c); (p, cid) } #[test] fn plan_prerenders_rendered_only_layers() { let (p, cid) = ae_project(); let opts = ExportOptions::default(); let plan = plan_prerender(&p, cid, &opts); let c = p.comp(cid).unwrap(); assert_eq!(plan, vec![(cid, layer(c, "Title").id)]); assert_eq!(prerender_reason(&p, cid, layer(c, "Title").id, &opts).as_deref(), Some("text layer")); let all = plan_prerender(&p, cid, &ExportOptions { prerender: PrerenderMode::All, ..Default::default() }); assert_eq!(all.len(), 3, "audio-only layers are never rendered"); assert!(plan_prerender(&p, cid, &ExportOptions { prerender: PrerenderMode::None, ..Default::default() }).is_empty()); } #[test] fn export_then_reimport_keeps_the_timeline() { let (p, cid) = ae_project(); let c = p.comp(cid).unwrap(); let title = layer(c, "Title").id; let mut pre = HashMap::new(); pre.insert((cid, title), Prerendered { path: "/render/Title.mov".into(), width: W, height: H, frame_rate: FrameRate::new(25, 1), duration: t(2.0) }); for format in [TimelineFormat::Fcp7Xml, TimelineFormat::Fcpxml, TimelineFormat::Otio] { let out = export(&p, cid, &ExportOptions { format, ..Default::default() }, &pre).unwrap(); assert_eq!(out.prerendered, 1); assert_eq!(out.sequences, 2); assert_eq!(out.video_tracks, 4, "3 in Main + 1 in Inner"); assert_eq!(out.audio_tracks, 1); let text = String::from_utf8(out.bytes.clone()).unwrap(); assert!(text.contains("Title.mov"), "{format:?}"); let (q, r) = import_doc(&out.bytes, &format!("Main.{}", format.extension()), &mut probe); let main = r.comps.iter().copied().find(|c| q.item(*c).unwrap().name == "Main").expect("Main"); let m = q.comp(main).unwrap(); assert_eq!(m.layers.len(), 4, "{format:?}: {:?}", m.layers.iter().map(|l| &l.name).collect::>()); // Stacking order kept: Title (pre-rendered) on top, then the precomp, then clip a. assert_eq!(m.layers[0].name, "Title"); assert!(near(m.layers[0].in_point, t(0.6)) && near(m.layers[0].out_point, t(2.6))); assert!(matches!(m.layers[1].source, LayerSource::Comp { .. }), "{format:?}"); assert!(near(m.layers[1].in_point, t(2.0)) && near(m.layers[1].layer_time(t(2.0)), Tick::ZERO)); let la = &m.layers[2]; assert!((la.stretch - 200.0).abs() < 1e-6, "{format:?} stretch {}", la.stretch); assert!(near(la.in_point, t(1.0)) && near(la.out_point, t(4.0))); let orig = layer(c, "clip a"); assert!(near(la.layer_time(t(1.0)), orig.layer_time(t(1.0))), "{format:?} {}", la.layer_time(t(1.0)).seconds()); assert!(!m.layers[3].switches.video && m.layers[3].switches.audio); if format == TimelineFormat::Fcp7Xml { let tr = la.transform().unwrap(); assert_eq!(tr.get("scale").unwrap().value.as_vec3()[..2], [80.0, 80.0]); assert!((tr.get("rotation").unwrap().value.as_f64() + 15.0).abs() < 1e-6); assert!((tr.get("opacity").unwrap().value.as_f64() - 75.0).abs() < 1e-6); let pk = &tr.get("position").unwrap().keys; assert_eq!(pk.len(), 2); for (k, o) in pk.iter().zip(&tr_orig(orig).keys) { assert!(near(la.comp_time(k.time), orig.comp_time(o.time)), "{} vs {}", k.time.seconds(), o.time.seconds()); assert_eq!(k.value.as_vec3()[..2], o.value.as_vec3()[..2]); } let lv = m.layers[3].props.prop("audio/levels").unwrap().value.as_vec2(); assert!((lv[0] + 3.0).abs() < 0.01, "{lv:?}"); } } } fn tr_orig(l: &Layer) -> &effectcraft_project::Property { l.transform().unwrap().get("position").unwrap() } #[test] fn export_without_prerender_leaves_text_out() { let (p, cid) = ae_project(); let out = export(&p, cid, &ExportOptions { prerender: PrerenderMode::None, ..Default::default() }, &HashMap::new()).unwrap(); assert!(out.warnings.iter().any(|w| w.contains("Title")), "{:?}", out.warnings); assert_eq!(out.prerendered, 0); assert_eq!(out.video_tracks, 3); } #[test] fn formats_parse() { assert_eq!(TimelineFormat::parse("premiere"), Some(TimelineFormat::Fcp7Xml)); assert_eq!(TimelineFormat::parse(".fcpxml"), Some(TimelineFormat::Fcpxml)); assert_eq!(TimelineFormat::from_path("/a/b.otio"), Some(TimelineFormat::Otio)); assert_eq!(TimelineFormat::Fcp7Xml.extension(), "xml"); assert_eq!(par_fraction(0.9), (9, 10)); assert_eq!(par_fraction(1.0), (1, 1)); } #[test] fn export_survives_a_precomp_layer_pointing_at_a_non_comp() { // A damaged project: the precomp layer's source is a footage item (or a deleted item). let (mut p, cid) = ae_project(); let footage = p.items.iter().find(|(_, i)| matches!(i.kind, ItemKind::Footage(_))).map(|(id, _)| *id).unwrap(); for bad in [footage, ItemId(9_999)] { let mut c = (*p.comp_arc(cid).unwrap()).clone(); for l in &mut c.layers { if matches!(l.source, LayerSource::Comp { .. }) { l.source = LayerSource::Comp { item: bad }; } } p.item_mut(cid).unwrap().kind = ItemKind::Comp(Arc::new(c)); for format in [TimelineFormat::Fcp7Xml, TimelineFormat::Fcpxml, TimelineFormat::Otio] { let out = export(&p, cid, &ExportOptions { format, prerender: PrerenderMode::None, ..Default::default() }, &HashMap::new()).unwrap(); assert_eq!(out.sequences, 1); } } assert!(matches!(export(&p, footage, &ExportOptions::default(), &HashMap::new()), Err(Error::NoComp(_)))); } #[test] fn deeply_nested_documents_error_instead_of_overflowing_the_stack() { // A few hundred kilobytes nested 100 000 levels deep overflowed the parsers' stack. let n = 100_000; let docs = [ (TimelineFormat::Fcp7Xml, format!("{}{}", "".repeat(n), "".repeat(n))), (TimelineFormat::Fcpxml, format!("{}{}", "".repeat(n), "".repeat(n))), (TimelineFormat::Otio, format!("{{\"OTIO_SCHEMA\":\"Timeline.1\",\"tracks\":{}{}}}", "[".repeat(n), "]".repeat(n))), ]; for (format, doc) in docs { let mut p = Project::default(); let r = import(&mut p, doc.as_bytes(), Some(format), &ImportOptions::default(), &mut |_| None); assert!(matches!(r, Err(Error::TooDeep(_))), "{format:?}"); } assert_eq!(super::import::xml_depth(b"]]>"), 2); assert_eq!(super::import::json_depth(br#"{"a": "[[[{{", "b": [[1], {"c": "\\\"["}]}"#), 3); }