use super::*;
use crate::{PixelSize, SceneCache, TimeLiteral, ValidationLimits};
fn size() -> Size {
Size::new(8.0, 4.0)
}
fn scene(duration: Duration) -> impl Scene {
(|_: &SceneContext<'_>| Node::empty()).with_duration(duration)
}
#[test]
fn fluent_builders_retain_all_errors_and_valid_chains_need_no_question_marks() -> crate::Result {
let error = Timeline::new(size(), crate::fps!(30))
.track(|track| {
track
.push((|_: &SceneContext<'_>| Node::empty()).with_duration(1.seconds()))
.place(
Time::ZERO - 1.seconds(),
(|_: &SceneContext<'_>| Node::empty()).with_duration(1.seconds()),
)
})
.build()
.unwrap_err();
assert_eq!(error.errors().len(), 1);
assert!(error.to_string().contains("track[0]"));
let valid = Timeline::new(size(), crate::fps!(30))
.track(|track| {
track.push((|_: &SceneContext<'_>| Node::empty()).with_duration(1.seconds()))
})
.build()?;
assert_eq!(valid.frame_count(), 30);
assert!(
Timeline::new(Size::new(f64::NAN, 4.0), crate::fps!(30))
.build()
.is_err()
);
Ok(())
}
#[test]
fn track_boundaries_are_half_open_and_sorted() -> crate::Result {
let timeline = Timeline::try_new(size(), crate::fps!(10))?
.try_track(|track| {
track
.try_place(2.seconds().after_origin(), scene(1.seconds()))?
.try_place(Time::ZERO, scene(1.seconds()))?
.try_push(scene(1.seconds()))
})?
.build()?;
assert_eq!(timeline.frame_count(), 40);
assert_eq!(
timeline.tracks()[0]
.clips()
.iter()
.map(Clip::id)
.collect::<Vec<_>>(),
[1, 0, 2]
);
let track = &timeline.tracks()[0];
assert_eq!(track.active_clip(Time::ZERO).unwrap().id(), 1);
assert!(track.active_clip(1.seconds().after_origin()).is_none());
assert_eq!(
track.active_clip(3.seconds().after_origin()).unwrap().id(),
2
);
assert!(track.active_clip(4.seconds().after_origin()).is_none());
let cache = SceneCache::new(0);
let context = SceneContext::new(Time::ZERO, crate::fps!(10), size(), &cache);
timeline
.render(&context)
.compile(size(), PixelSize::new(8, 4), ValidationLimits::default())?;
Ok(())
}
#[test]
fn nested_push_and_embed_preserve_global_ticks_and_child_rate_without_retiming() -> crate::Result {
let observed = Arc::new(std::sync::Mutex::new(Vec::new()));
let calls = observed.clone();
let child = Timeline::try_new(Size::new(8.0, 4.0), crate::fps!(24))?
.try_track(|track| {
track.try_push(
(move |context: &SceneContext<'_>| {
calls.lock().unwrap().push((
context.time(),
context.global_time(),
context.frame_rate(),
context.size(),
context.clip_duration(),
));
Node::from(context.bounds().fill(Color::WHITE))
})
.with_duration(2.seconds()),
)
})?
.build()?;
let parent = Timeline::try_new(Size::new(12.0, 12.0), FrameRate::NTSC)?
.try_track(|track| track.try_gap(1.seconds())?.try_nest(child.clone()))?
.build()?;
let cache = SceneCache::new(0);
let context = SceneContext::new(
9.seconds().after_origin(),
FrameRate::NTSC,
parent.size(),
&cache,
)
.shifted(7.seconds().offset());
let start = parent.tracks()[0].clips()[0].start();
let pushed = parent.render(&context).compile(
parent.size(),
PixelSize::new(12, 12),
ValidationLimits::default(),
)?;
let embedded = context.embed(&child.visual(), start).compile(
parent.size(),
PixelSize::new(12, 12),
ValidationLimits::default(),
)?;
assert_eq!(pushed.commands, embedded.commands);
assert_eq!(
observed.lock().unwrap().as_slice(),
&[(
Time::ZERO + 2.seconds() - start.since_origin(),
9.seconds().after_origin(),
crate::fps!(24),
child.size(),
Some(2.seconds())
); 2]
);
let at_end = SceneContext::new(
start + child.duration(),
FrameRate::NTSC,
parent.size(),
&cache,
);
assert!(
at_end
.embed(&child.visual(), start)
.compile(
parent.size(),
PixelSize::new(12, 12),
ValidationLimits::default()
)?
.commands
.is_empty()
);
Ok(())
}
#[test]
fn timeline_rejects_overlap_missing_duration_and_collapsed_intervals() -> crate::Result {
let builder = Timeline::try_new(size(), crate::fps!(10))?
.try_track(|track| track.try_push(scene(1.seconds())))?;
let error = builder
.clone()
.try_track(|track| {
track
.try_push(scene(1.seconds()))?
.try_place(500.milliseconds().after_origin(), scene(1.seconds()))
})
.unwrap_err();
assert_eq!(error.kind(), crate::ErrorKind::Timeline);
assert!(error.to_string().contains("track[1]") && error.to_string().contains("overlaps"));
assert_eq!(builder.build()?.tracks.len(), 1);
assert!(
Timeline::try_new(size(), crate::fps!(10))?
.try_track(|track| track.try_push(|_: &SceneContext<'_>| Node::empty()))
.unwrap_err()
.to_string()
.contains("with_duration")
);
assert!(
Timeline::try_new(size(), crate::fps!(10))?
.try_track(|track| track.try_push(scene(1.milliseconds())))
.is_err()
);
assert!(Timeline::try_new(size(), FrameRate::try_new(u32::MAX, 1).unwrap()).is_err());
Ok(())
}
#[test]
fn timeline_snaps_independent_boundaries_and_retains_warnings() -> crate::Result {
let timeline = Timeline::try_new(size(), crate::fps!(10))?
.try_track(|track| {
track.try_place(50.milliseconds().after_origin(), scene(100.milliseconds()))
})?
.build()?;
let clip = &timeline.tracks()[0].clips()[0];
assert_eq!(clip.start(), 100.milliseconds().after_origin());
assert_eq!(clip.end(), 200.milliseconds().after_origin());
assert!(
timeline
.diagnostics()
.boundary_adjustments()
.any(|adjustment| adjustment.edge == BoundaryEdge::Start
&& adjustment.requested == 50.milliseconds().after_origin())
);
assert_eq!(timeline.frame_count(), 2);
Ok(())
}
#[test]
fn alignment_uses_tick_distances_ntsc_and_raw_cursors_without_drift() -> crate::Result {
for rate in [
crate::fps!(60),
FrameRate::NTSC_FILM,
FrameRate::NTSC,
FrameRate::NTSC_DOUBLE,
FrameRate::try_new(61, 1).unwrap(),
FrameRate::try_new(1000, 3).unwrap(),
FrameRate::try_new(TICKS_PER_SECOND as u32, 1).unwrap(),
] {
for index in [0, 1, 2, 59, 1000, 100_000] {
let first = Time::from_frame(index, rate);
let second = Time::from_frame(index + 1, rate);
assert_eq!(align(first, rate)?, (first, index));
let middle = Time::from_ticks(first.ticks() + (second.ticks() - first.ticks()) / 2);
let expected = if middle.ticks() - first.ticks() < second.ticks() - middle.ticks() {
(first, index)
} else {
(second, index + 1)
};
assert_eq!(align(middle, rate)?, expected);
}
}
let timeline = Timeline::try_new(size(), crate::fps!(60))?
.try_track(|mut track| {
for _ in 0..1000 {
track = track.try_push(scene(167.milliseconds()))?;
}
assert_eq!(track.cursor(), 167.seconds().after_origin());
Ok(track)
})?
.build()?;
assert_eq!(timeline.frame_count(), 10_020);
for clips in timeline.tracks[0].clips.windows(2) {
assert_eq!(clips[0].end(), clips[1].start());
}
let ntsc = Timeline::try_new(size(), FrameRate::NTSC)?
.try_track(|track| track.try_push(scene(1.seconds())))?;
assert_eq!(
ntsc.clone().build()?.duration(),
Duration::from_frame(30, FrameRate::NTSC)
);
let adjusted = ntsc.try_duration(1500.milliseconds())?;
assert_eq!(adjusted.clone().build()?.frame_count(), 45);
let durations = |timeline: &Timeline| {
timeline
.diagnostics()
.boundary_adjustments()
.filter(|adjustment| adjustment.edge == BoundaryEdge::Duration)
.count()
};
assert_eq!(durations(&adjusted.clone().build()?), 1);
assert_eq!(durations(&adjusted.try_duration(2.seconds())?.build()?), 1);
Ok(())
}
#[test]
fn construction_errors_limits_and_cow_snapshots_are_bounded() -> crate::Result {
for bad in [Time::from_ticks(-1), Time::from_ticks(i64::MIN)] {
assert!(
Timeline::try_new(size(), crate::fps!(30))?
.try_track(|track| track.try_place(bad, scene(1.seconds())))
.is_err()
);
}
assert!(
Timeline::try_new(size(), crate::fps!(30))?
.try_track(|track| track.try_push(scene(Duration::ZERO)))
.is_err()
);
assert!(
Timeline::try_new(size(), crate::fps!(30))?
.try_track(|track| track.try_place(Time::from_ticks(i64::MAX), scene(1.seconds())))
.is_err()
);
assert!(
Timeline::try_new(size(), crate::fps!(30))?
.try_track(|track| track
.try_gap(Duration::from_ticks(i64::MAX as u64))?
.try_gap(1.seconds()))
.is_err()
);
assert!(Timeline::try_new(Size::new(f64::NAN, 4.0), crate::fps!(30)).is_err());
let mut original = Timeline::try_new(size(), crate::fps!(30))?;
for _ in 0..MAX_TRACKS {
original = original.try_track(Ok)?;
}
let snapshot = original.clone();
assert!(original.try_track(Ok).is_err());
assert_eq!(snapshot.build()?.tracks.len(), MAX_TRACKS);
let full = Timeline::try_new(size(), crate::fps!(10))?.try_track(|mut track| {
for _ in 0..MAX_CLIPS {
track = track.try_push(scene(100.milliseconds()))?;
}
Ok(track)
})?;
assert!(
full.clone()
.try_track(|track| track.try_push(scene(1.seconds())))
.is_err()
);
assert_eq!(full.build()?.tracks()[0].clips().len(), MAX_CLIPS);
let mut foreign = None;
let captured = Timeline::try_new(size(), crate::fps!(24))?.try_track(|track| {
foreign = Some(track);
Err(invalid("capture only"))
});
assert!(captured.is_err());
assert!(
Timeline::try_new(size(), crate::fps!(30))?
.try_track(|_| Ok(foreign.unwrap()))
.is_err()
);
Ok(())
}
#[test]
fn soundtrack_exposes_declared_audio_and_master() -> crate::Result {
let master = crate::Master {
gain_db: -3.0,
..Default::default()
};
let timeline = Timeline::new(size(), crate::fps!(30))
.audio(crate::AudioClip::file("a.wav").duration(1.seconds()))
.audio_bus(crate::AudioBus::new("music"))
.master(master)
.build()?;
let soundtrack = timeline.soundtrack();
assert_eq!(soundtrack.clips().len(), 1);
assert_eq!(soundtrack.buses().len(), 1);
assert_eq!(soundtrack.master(), master);
Ok(())
}
#[test]
fn explicit_duration_shorter_than_content_warns_and_audio_extends_inferred_length() -> crate::Result
{
let timeline = Timeline::new(size(), crate::fps!(10))
.track(|track| track.push(scene(2.seconds())))
.duration(1.seconds())
.build()?;
assert_eq!(timeline.duration(), 1.seconds());
assert_eq!(
timeline.diagnostics().warnings(),
[TimelineWarning::Truncated {
duration: 1.seconds(),
content: 2.seconds(),
}]
);
let with_audio = Timeline::new(size(), crate::fps!(10))
.track(|track| track.push(scene(1.seconds())))
.audio(
crate::AudioClip::file("a.wav")
.start(500.milliseconds().after_origin())
.duration(1.seconds()),
)
.build()?;
assert_eq!(with_audio.duration(), 1500.milliseconds());
assert!(with_audio.diagnostics().is_empty());
Ok(())
}
#[test]
fn clips_take_their_duration_at_placement_and_can_start_inside_the_scene() -> crate::Result {
let observed = Arc::new(std::sync::Mutex::new(Vec::new()));
let calls = observed.clone();
let probe = move |context: &SceneContext<'_>| {
calls.lock().unwrap().push(context.time());
Node::empty()
};
let timeline = Timeline::try_new(size(), crate::fps!(10))?
.try_track(|track| {
track
.try_push_for(1.seconds(), probe.clone())?
.try_push_trimmed(500.milliseconds(), 1.seconds(), probe.clone())
})?
.build()?;
assert_eq!(timeline.duration(), 2.seconds());
let clips = timeline.tracks()[0].clips();
assert_eq!(clips[0].offset(), Duration::ZERO);
assert_eq!(clips[1].offset(), 500.milliseconds());
let cache = SceneCache::new(0);
for time in [200.milliseconds(), 1200.milliseconds()] {
let context = SceneContext::new(time.after_origin(), crate::fps!(10), size(), &cache);
timeline.render(&context);
}
assert_eq!(
observed.lock().unwrap().as_slice(),
[
200.milliseconds().after_origin(),
700.milliseconds().after_origin()
]
);
Ok(())
}