motion-core 0.2.0

Backend-independent motion engine primitives
Documentation
use std::num::NonZeroU32;

use super::*;
use crate::fps;

fn nonzero(value: u32) -> NonZeroU32 {
    NonZeroU32::new(value).unwrap()
}

#[test]
fn unsigned_literals_and_integer_multipliers_preserve_large_tick_precision() {
    assert_eq!(2_u32.seconds(), 2_i64.seconds());
    assert_eq!(3_usize.milliseconds(), 3_i64.milliseconds());
    assert_eq!(4_u64.seconds(), 4_i64.seconds());
    let large = Duration::from_ticks((1 << 53) + 1);
    assert_eq!((large * 3_i64).ticks(), ((1 << 53) + 1) * 3);
    assert_eq!((3_u32 * large).ticks(), (large * 3_i64).ticks());
    assert_eq!((Duration::from_ticks(2) * 4_u64).ticks(), 8);
    assert_eq!(Duration::from_ticks(u64::MAX).checked_mul(2), None);
    assert!(std::panic::catch_unwind(|| Duration::from_ticks(u64::MAX) * 2_u32).is_err());
    assert!(std::panic::catch_unwind(|| u64::MAX.seconds()).is_err());
}

#[test]
fn standard_video_and_audio_intervals_are_exact() {
    for numerator in [24, 25, 30, 48, 50, 60, 90, 100, 120] {
        let rate = FrameRate::per_second(nonzero(numerator));
        assert_eq!(
            rate.frame_duration().ticks() * u64::from(numerator),
            TICKS_PER_SECOND as u64
        );
    }
    for rate in [
        FrameRate::NTSC_FILM,
        FrameRate::NTSC,
        FrameRate::NTSC_DOUBLE,
    ] {
        assert_eq!(
            u128::from(rate.frame_duration().ticks()) * u128::from(rate.numerator()),
            u128::from(TICKS_PER_SECOND as u64) * u128::from(rate.denominator())
        );
    }
    for sample_rate in [44_100, 48_000, 96_000] {
        assert_eq!(TICKS_PER_SECOND % sample_rate, 0);
    }
    assert_eq!(FrameRate::try_new(60_000, 2_002).unwrap(), FrameRate::NTSC);
    assert_eq!(
        FrameRate::new(nonzero(120), nonzero(2)),
        FrameRate::from(nonzero(60))
    );
    assert_eq!(fps!(60), FrameRate::from(nonzero(60)));
}

#[test]
fn frame_rate_construction_rejects_zero_components() {
    for (numerator, denominator) in [(0, 1), (1, 0), (0, 0)] {
        assert!(FrameRate::try_new(numerator, denominator).is_err());
    }
}

#[test]
fn signed_frame_boundaries_round_trip_without_drift() {
    for rate in [
        fps!(24),
        fps!(60),
        FrameRate::NTSC,
        FrameRate::NTSC_FILM,
        FrameRate::try_new(11, 1).unwrap(),
        FrameRate::try_new(123_457, 1_001).unwrap(),
    ] {
        for frame in [-1_000_000, -101, -1, 0, 1, 101, 1_000_000] {
            let time = Time::from_frame(frame, rate);
            assert_eq!(time.to_frame(rate), frame);
            assert_eq!((time - Duration::from_ticks(1)).to_frame(rate), frame - 1);
        }
        for frame in [0_u64, 1, 101, 1_000_000] {
            let span = Duration::from_frame(frame, rate);
            assert_eq!(span.to_frame(rate), frame);
            assert_eq!(span.after_origin(), Time::from_frame(frame as i64, rate));
        }
    }
    let rate = fps!(11);
    assert_eq!(Time::from_frame(11, rate), Time::ZERO + 1.seconds());
    assert_ne!(rate.frame_duration() * 11.0, 1.seconds());
    assert_eq!(Time::from_ticks(-1).to_frame(fps!(30)), -1);
    let tiny_time = Time::from_frame(i64::MAX, FrameRate::try_new(u32::MAX, 1).unwrap());
    assert!(tiny_time.ticks() > 0);
}

#[test]
fn checked_frame_conversions_preserve_rounding_and_reject_overflow() {
    let huge = FrameRate::try_new(u32::MAX, 1).unwrap();
    for rate in [
        FrameRate::NTSC,
        FrameRate::try_new(11, 1).unwrap(),
        FrameRate::try_new(123_457, 1_001).unwrap(),
    ] {
        for frame in [-1_000_000, -1, 0, 1, 1_000_000] {
            let time = Time::checked_from_frame(frame, rate).unwrap();
            assert_eq!(time, Time::from_frame(frame, rate));
            assert_eq!(time.checked_to_frame(rate), Some(frame));
            assert_eq!(
                (time - TimeOffset::from_ticks(1)).checked_to_frame(rate),
                Some(frame - 1)
            );
        }
    }
    assert_eq!(Time::checked_from_frame(i64::MAX, fps!(1)), None);
    assert_eq!(Time::checked_from_frame(i64::MIN, fps!(1)), None);
    assert_eq!(Duration::checked_from_frame(u64::MAX, fps!(1)), None);
    assert_eq!(Time::from_ticks(i64::MAX).checked_to_frame(huge), None);
    assert_eq!(Time::from_ticks(i64::MIN).checked_to_frame(huge), None);
    assert_eq!(Duration::from_ticks(u64::MAX).checked_to_frame(huge), None);
}

#[test]
fn literals_and_arithmetic_share_tick_time() {
    let duration: Duration = 1.5.seconds();
    assert_eq!(duration, 1.seconds() + 500.milliseconds());
    assert_eq!(Duration::from_seconds(1.0 / 1024.0).ticks(), 689_063);
    assert_eq!(TimeOffset::from_seconds(-1.0 / 1024.0).ticks(), -689_063);
    assert_eq!((1.0 / 128.0).milliseconds().ticks(), 5_513);
    assert_eq!(2.0 * duration, 3.seconds());
    assert_eq!(duration / 2.0, 750.milliseconds());
    assert_eq!(duration / 500.milliseconds(), 3.0);
    assert_eq!(duration.as_seconds(), 1.5);
    let mut time = Time::ZERO;
    time += duration;
    time -= 2.seconds();
    assert_eq!(time, Time::ZERO - 500.milliseconds());
    assert_eq!(time - Time::ZERO, TimeOffset::from_seconds(-0.5));
    assert_eq!(time.since_origin().magnitude(), 500.milliseconds());
    assert_eq!(Time::ZERO + time.since_origin(), time);
    assert_eq!(
        (Time::ZERO + 2.seconds()).duration_since(Time::ZERO + 500.milliseconds()),
        Some(1500.milliseconds())
    );
    assert_eq!(time.duration_since(Time::ZERO), None);
    assert_eq!(
        Time::from_ticks(i64::MAX).duration_since(Time::from_ticks(i64::MIN)),
        Some(Duration::from_ticks(u64::MAX))
    );
    for ticks in [0, 1, u64::MAX] {
        assert_eq!(
            Duration::from_ticks(ticks) * 1.0,
            Duration::from_ticks(ticks)
        );
        assert_eq!(
            Duration::from_ticks(ticks) / 1.0,
            Duration::from_ticks(ticks)
        );
    }
}

#[test]
fn durations_cannot_be_negative() {
    assert!(Duration::try_from_seconds(-1.0).is_err());
    assert!(Duration::try_from_seconds(-0.0).is_ok());
    assert_eq!(1.seconds().checked_sub(2.seconds()), None);
    assert_eq!(1.seconds().saturating_sub(2.seconds()), Duration::ZERO);
    assert_eq!(2.seconds().checked_sub(1.seconds()), Some(1.seconds()));
    assert!(1.seconds().try_mul(-1.0).is_err());
    assert!(1.seconds().try_div(-1.0).is_err());
    assert!(TimeOffset::from_ticks(-1).to_duration().is_none());
    assert_eq!(
        TimeOffset::from_ticks(i64::MIN).magnitude().ticks(),
        1 << 63
    );
    assert!(std::panic::catch_unwind(|| 1.seconds() - 2.seconds()).is_err());
    assert!(std::panic::catch_unwind(|| (-1).seconds()).is_err());
    assert!(std::panic::catch_unwind(|| (-1.0).seconds()).is_err());
    assert!(std::panic::catch_unwind(|| 1.seconds() * -1).is_err());
    assert!(std::panic::catch_unwind(|| 1.seconds() * -1.0).is_err());
}

#[test]
fn invalid_inputs_and_overflow_do_not_saturate_or_wrap() {
    for seconds in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, f64::MAX] {
        assert!(std::panic::catch_unwind(|| Duration::from_seconds(seconds)).is_err());
        assert!(std::panic::catch_unwind(|| Time::from_seconds(seconds)).is_err());
    }
    let maximum = Duration::from_ticks(u64::MAX);
    assert!(std::panic::catch_unwind(|| maximum + Duration::from_ticks(1)).is_err());
    assert!(
        std::panic::catch_unwind(|| Time::from_ticks(i64::MAX) + Duration::from_ticks(1)).is_err()
    );
    assert!(std::panic::catch_unwind(|| Time::from_ticks(0) + maximum).is_err());
    assert!(std::panic::catch_unwind(|| maximum.after_origin()).is_err());
    assert!(std::panic::catch_unwind(|| Time::from_ticks(i64::MIN) - Time::from_ticks(1)).is_err());
    assert!(std::panic::catch_unwind(|| -TimeOffset::from_ticks(i64::MIN)).is_err());
    assert!(std::panic::catch_unwind(|| maximum * 2.0).is_err());
    assert!(std::panic::catch_unwind(|| 1.seconds() * f64::NAN).is_err());
    assert!(std::panic::catch_unwind(|| 1.seconds() / 0.0).is_err());
    assert!(std::panic::catch_unwind(|| Duration::ZERO / Duration::ZERO).is_err());
    assert!(std::panic::catch_unwind(|| i64::MAX.seconds()).is_err());
    assert!(std::panic::catch_unwind(|| i64::MAX.milliseconds()).is_err());
    assert!(std::panic::catch_unwind(|| Time::from_frame(i64::MAX, fps!(1))).is_err());
    assert!(
        std::panic::catch_unwind(|| maximum.to_frame(FrameRate::try_new(u32::MAX, 1).unwrap()))
            .is_err()
    );
}