using System.Threading.Channels; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using TeamsISO.Engine.Domain; using TeamsISO.Engine.Pipeline; namespace TeamsISO.Engine.Tests.Pipeline; /// /// Targets the IsoPipeline stats wiring (FPS ring buffer + drops/dups surfaced /// from FrameProcessor). The production-ctor's runner pumps the receiver in a /// background thread, so we drive the FrameProcessor directly here — that's /// where FramesDropped and FramesDuplicated are computed. /// public class FrameProcessorStatsTests { [Fact] public async Task FrameProcessor_DropsBackloggedFrames_WhenInputHasMultipleQueued() { // Arrange: a raw channel pre-filled with three frames before ProcessOnce runs. // The processor should keep only the newest (closest-frame strategy) and report // FramesDropped == 2 (the two it threw away). var raw = Channel.CreateUnbounded(); var processed = Channel.CreateUnbounded(); var clock = new FakeClock(); var settings = FrameProcessingSettings.Default; var processor = new FrameProcessor( settings, new ManagedNearestNeighborFrameScaler(), new SolidFrameRenderer(), clock, raw.Reader, processed.Writer, slateThreshold: TimeSpan.FromSeconds(2.5), NullLogger.Instance); for (var i = 0; i < 3; i++) raw.Writer.TryWrite(MakeFrame(width: 320, height: 180, ticks: i)); // Act await processor.ProcessOnceAsync(CancellationToken.None); // Assert var stats = processor.Stats; stats.FramesIn.Should().Be(3, because: "the processor counts every frame it pulled off the channel"); stats.FramesOut.Should().Be(1, because: "closest-frame strategy emits one frame per tick"); stats.FramesDropped.Should().Be(2, because: "two queued frames were superseded by the newest"); stats.IncomingWidth.Should().Be(320); stats.IncomingHeight.Should().Be(180); } [Fact] public async Task FrameProcessor_DuplicatesLastFrame_WhenNoNewArrival() { // First tick: a single frame. Second tick: nothing new — should re-emit // the last frame (within slate threshold) and increment FramesDuplicated. var raw = Channel.CreateUnbounded(); var processed = Channel.CreateUnbounded(); var clock = new FakeClock { NowTicks = 0 }; var processor = new FrameProcessor( FrameProcessingSettings.Default, new ManagedNearestNeighborFrameScaler(), new SolidFrameRenderer(), clock, raw.Reader, processed.Writer, slateThreshold: TimeSpan.FromSeconds(2.5), NullLogger.Instance); raw.Writer.TryWrite(MakeFrame(width: 320, height: 180, ticks: 0)); await processor.ProcessOnceAsync(CancellationToken.None); // Advance clock 100ms; no new frame. clock.NowTicks = TimeSpan.FromMilliseconds(100).Ticks; await processor.ProcessOnceAsync(CancellationToken.None); var stats = processor.Stats; stats.FramesIn.Should().Be(1); stats.FramesOut.Should().Be(2); stats.FramesDuplicated.Should().Be(1, because: "the second tick re-emitted the last frame"); } private static RawFrame MakeFrame(int width, int height, long ticks) { var bytes = new byte[width * height * 4]; return new RawFrame(width, height, ticks, bytes, PixelFormat.Bgra); } /// Simple deterministic clock for processor tests. private sealed class FakeClock : IFrameClock { public long NowTicks { get; set; } public ValueTask WaitForNextTickAsync(CancellationToken cancellationToken) => ValueTask.FromResult(true); } }