dragon-iso/src/tests/TeamsISO.Engine.Tests/Pipeline/IsoPipelineStatsTests.cs
Zac Gaetano 0c82ac71f0
Some checks failed
CI / build-and-test (push) Failing after 27s
feat: bundle Inter font, emergency stop button, window persistence + tests
Four polish items + a test pass.

1. Inter Variable (rsms/inter v3.19, OFL) is bundled at Assets/Fonts/Inter.ttf (~800 KB) and registered as a WPF Resource. WildDragonTheme.xaml's Wd.Font.Sans now points at pack://application:,,,/Assets/Fonts/#Inter so the typography matches wilddragon.net regardless of whether the user has Inter installed system-wide. Falls back to Segoe UI Variable Display if the resource is missing.

2. 'Stop all ISOs' button at the right of the participants header. Bound to a new MainViewModel.StopAllIsosCommand that snapshots the enabled list, awaits DisableIsoAsync sequentially, and silently swallows per-pipeline failures (best-effort emergency stop). CanExecute gates on whether any ISO is currently enabled.

3. WindowStateStore service persists the main window's Left/Top/Width/Height/State to %LOCALAPPDATA%\\TeamsISO\\window.json on close and restores it on SourceInitialized. Multi-monitor friendly: a saved position with no corner inside any virtual screen is rejected so a disconnected monitor doesn't strand the window off-screen.

4. Two new unit tests cover FrameProcessor's drops + duplicates accounting. 76/76 unit tests pass (was 74).
2026-05-08 13:59:14 -04:00

95 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);
}
}