96 lines
3.9 KiB
C#
96 lines
3.9 KiB
C#
|
|
using System.Threading.Channels;
|
||
|
|
using FluentAssertions;
|
||
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
||
|
|
using TeamsISO.Engine.Domain;
|
||
|
|
using TeamsISO.Engine.Pipeline;
|
||
|
|
|
||
|
|
namespace TeamsISO.Engine.Tests.Pipeline;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 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.
|
||
|
|
/// </summary>
|
||
|
|
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<RawFrame>();
|
||
|
|
var processed = Channel.CreateUnbounded<ProcessedFrame>();
|
||
|
|
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<FrameProcessor>.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<RawFrame>();
|
||
|
|
var processed = Channel.CreateUnbounded<ProcessedFrame>();
|
||
|
|
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<FrameProcessor>.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);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Simple deterministic clock for processor tests.</summary>
|
||
|
|
private sealed class FakeClock : IFrameClock
|
||
|
|
{
|
||
|
|
public long NowTicks { get; set; }
|
||
|
|
public ValueTask<bool> WaitForNextTickAsync(CancellationToken cancellationToken) => ValueTask.FromResult(true);
|
||
|
|
}
|
||
|
|
}
|