using System; using System.Diagnostics; using Xunit; using ytLive.Helpers; namespace ytLive.Tests; /// /// UNIT 0 (2026-10-05): the camera and the pump could both drop frames invisibly — /// a 60fps rawvideo container stamps frames by ARRIVAL, so a missing camera frame /// and a padded repeat look identical in the recorded file, and the old stats line /// counted SUBMITTED frames (always the full padded target) over an unknown number /// of renders. These tests pin the one shared counter that makes the two measurable: /// gaps, hitches, and the longest stall in a window of a timestamped stream. /// public sealed class FrameCadenceTests { private static long Ms(double ms) => (long)(ms * Stopwatch.Frequency / 1000d); private static long AtMs(double ms) => Ms(ms); [Fact] public void SteadyStream_ReportsNominalRateAndNoHitches() { var cadence = new FrameCadence(); // 30 frames at a true 33.3ms period: 29 gaps, none over the 50ms threshold. for (var i = 0; i < 30; i++) cadence.Add(AtMs(i * (1000d / 30d))); var s = cadence.Summarize(); Assert.Equal(30, s.Frames); Assert.Equal(29, s.Gaps); Assert.Equal(0, s.Hitches); Assert.Equal(30d, Math.Round(s.RatePerSecond, 0)); Assert.Equal(33.3, Math.Round(s.AverageGapMs, 1)); Assert.InRange(s.LongestGapMs, 33.0, 33.5); } [Fact] public void DroppedFrames_CountAsHitchesAndLongestGap() { var cadence = new FrameCadence(); // 30fps with two dropped frames (66.6ms gaps) and one double-frame (0.1ms): // the pattern the 2026-10-05 recording showed in the webcam corner. foreach (var ms in new[] { 0.0, 33.3, 33.4, 66.7, 100.0, 133.3, 199.9, 233.2, 266.5, 333.1, 366.4, }) cadence.Add(AtMs(ms)); var s = cadence.Summarize(); Assert.Equal(11, s.Frames); Assert.Equal(10, s.Gaps); // the bunched pair still counts as an interval Assert.Equal(2, s.Hitches); // the two dropped-frame gaps at 30fps Assert.Equal(66.6, Math.Round(s.LongestGapMs, 1)); } [Fact] public void OutOfOrderStamp_CountsAsFrameWithoutPoisoningGaps() { var cadence = new FrameCadence(); cadence.Add(AtMs(0)); cadence.Add(AtMs(33.3)); cadence.Add(AtMs(10.0)); // out of order — a stamp from another clock domain cadence.Add(AtMs(66.6)); var s = cadence.Summarize(); Assert.Equal(4, s.Frames); Assert.Equal(2, s.Gaps); // the two valid intervals only Assert.Equal(0, s.Hitches); Assert.Equal(33.3, Math.Round(s.LongestGapMs, 1)); } [Fact] public void Reset_StartsFreshWindowAndKeepsThreshold() { var cadence = new FrameCadence(hitchThresholdMs: 20); cadence.Add(AtMs(0)); cadence.Add(AtMs(100)); Assert.Equal(1, cadence.Hitches); cadence.Reset(); Assert.Equal(0, cadence.Frames); Assert.Equal(0, cadence.Gaps); Assert.Equal(20, cadence.Summarize().HitchThresholdMs); Assert.Contains("no frames", cadence.Summarize().Describe("cam")); } [Fact] public void Describe_NamesTheWindowRateHitchesAndLongestGap() { var cadence = new FrameCadence(); foreach (var ms in new[] { 0.0, 16.7, 33.4, 50.1, 120.0 }) cadence.Add(AtMs(ms)); Assert.Equal( "webcam 5f/0.1s 33.3/s, 4 gaps, 1 >= 50ms, avg 30.0ms, longest 69.9ms", cadence.Summarize().Describe("webcam")); } [Fact] public void ThresholdsMustBePositive() { Assert.Throws(() => new FrameCadence(0)); Assert.Throws(() => new FrameCadence(-1)); } }