Files
riojoy/tests/RioJoy.Core.Tests/RioRuntimeTests.cs
T
CydandClaude Fable 5 e60d551f39 Phase 7: live encoder gauges in the profile editor
RioRuntime raises AxesUpdated with the calibrated AxisOutputs after every
analog reply (independent of the joystick sink, so the gauges work while
editor input routing is suppressed). The editor strip now renders the
values live: Z and the L/R pedals (Rx/Ry) fill bottom-up, Rz deflects
from a center tick, and the X/Y box tracks the stick as a dot over a
crosshair. Fraction/deflection math is pure in Core.Editing.AxisGauges
(clamped, unit-tested); the canvas repaints only the strip and only on
change. Verified by offline form screenshots against injected values.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 09:37:29 -05:00

187 lines
6.6 KiB
C#

using System.Diagnostics;
using AxisOutputs = RioJoy.Core.Calibration.AxisOutputs;
using RioJoy.Core;
using RioJoy.Core.Mapping;
using RioJoy.Core.Protocol;
using RioJoy.Core.Serial;
using RioJoy.Core.Tests.Mapping;
using RioJoy.Core.Tests.Serial;
using Xunit;
namespace RioJoy.Core.Tests;
public class RioRuntimeTests
{
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(5);
private static async Task WaitUntilAsync(Func<bool> condition)
{
var sw = Stopwatch.StartNew();
while (!condition())
{
if (sw.Elapsed > Timeout)
throw new TimeoutException("Condition not met in time.");
await Task.Delay(10);
}
}
[Fact]
public async Task ButtonPressed_RoutesToJoystick()
{
var fake = new FakeTransport();
var link = new RioSerialLink(fake, new RioSerialLinkOptions { AutoPollAnalog = false });
var recorder = new RecordingSink();
var map = new RioInputMap { [0x05] = new RioMapEntry(0x1009) }; // joystick button 9
using var runtime = new RioRuntime(link, map, recorder, recorder);
runtime.Start();
using var cts = new CancellationTokenSource();
Task run = link.RunAsync(cts.Token);
fake.Enqueue(PacketBuilder.Build(RioCommand.ButtonPressed, new byte[] { 0x05 }));
await WaitUntilAsync(() => recorder.Snapshot().Contains("Joy(9,True)"));
cts.Cancel();
await run;
}
[Fact]
public async Task AnalogReply_DrivesAllSixAxes()
{
var fake = new FakeTransport();
var link = new RioSerialLink(fake, new RioSerialLinkOptions { AutoPollAnalog = false });
var recorder = new RecordingSink();
using var runtime = new RioRuntime(link, new RioInputMap(), recorder, recorder);
runtime.Start();
using var cts = new CancellationTokenSource();
Task run = link.RunAsync(cts.Token);
// All axes at zero → joystick X/Y centered.
fake.Enqueue(PacketBuilder.Build(RioCommand.AnalogReply, new byte[10]));
await WaitUntilAsync(() => recorder.Snapshot().Count(e => e.StartsWith("Axis(")) == 6);
string[] log = recorder.Snapshot();
Assert.Contains("Axis(X,16383)", log);
Assert.Contains("Axis(Y,16383)", log);
cts.Cancel();
await run;
}
[Fact]
public async Task AnalogReply_RaisesAxesUpdated()
{
var fake = new FakeTransport();
var link = new RioSerialLink(fake, new RioSerialLinkOptions { AutoPollAnalog = false });
var recorder = new RecordingSink();
using var runtime = new RioRuntime(link, new RioInputMap(), recorder, recorder);
AxisOutputs? seen = null;
runtime.AxesUpdated += axes => seen = axes;
runtime.Start();
using var cts = new CancellationTokenSource();
Task run = link.RunAsync(cts.Token);
// All raw axes at zero → joystick X/Y centered, same values the sink receives.
fake.Enqueue(PacketBuilder.Build(RioCommand.AnalogReply, new byte[10]));
await WaitUntilAsync(() => seen is not null);
Assert.Equal(AxisOutputs.Center, seen!.Value.X);
Assert.Equal(AxisOutputs.Center, seen.Value.Y);
cts.Cancel();
await run;
}
[Fact]
public async Task LitLamps_Dimmed_OnFirstBoardReply_NotOnStart()
{
var fake = new FakeTransport();
var link = new RioSerialLink(fake, new RioSerialLinkOptions { AutoPollAnalog = false });
var recorder = new RecordingSink();
var map = new RioInputMap { [0x05] = new RioMapEntry(0x9001) }; // lit joystick button
using var runtime = new RioRuntime(link, map, recorder, recorder);
runtime.Start();
using var cts = new CancellationTokenSource();
Task run = link.RunAsync(cts.Token);
// Nothing is sent on Start: lamp commands would be dropped during the board's
// post-reset boot, so no write happens until the board first replies.
await Assert.ThrowsAnyAsync<OperationCanceledException>(
() => fake.NextWriteAsync(TimeSpan.FromMilliseconds(150)));
// A reply proves the board is alive → the lit lamp is dimmed.
fake.Enqueue(PacketBuilder.Build(RioCommand.AnalogReply, new byte[10]));
bool dimmed = false;
for (int i = 0; i < 8 && !dimmed; i++)
{
byte[] w = await fake.NextWriteAsync(TimeSpan.FromSeconds(2));
dimmed = w.Length >= 3 && w[0] == (byte)RioCommand.LampRequest && w[1] == 0x05;
}
Assert.True(dimmed, "expected a LampRequest dimming address 0x05 after the first reply");
cts.Cancel();
await run;
}
[Fact]
public async Task ButtonActivity_FiresForPressAndRelease_EvenWhenUnmapped()
{
var fake = new FakeTransport();
var link = new RioSerialLink(fake, new RioSerialLinkOptions { AutoPollAnalog = false });
var recorder = new RecordingSink();
using var runtime = new RioRuntime(link, new RioInputMap(), recorder, recorder); // empty map
var activity = new List<(int Address, bool Pressed)>();
runtime.ButtonActivity += (addr, pressed) => activity.Add((addr, pressed));
runtime.Start();
using var cts = new CancellationTokenSource();
Task run = link.RunAsync(cts.Token);
fake.Enqueue(PacketBuilder.Build(RioCommand.ButtonPressed, new byte[] { 0x05 }));
fake.Enqueue(PacketBuilder.Build(RioCommand.ButtonReleased, new byte[] { 0x05 }));
await WaitUntilAsync(() => activity.Count == 2);
Assert.Equal((0x05, true), activity[0]);
Assert.Equal((0x05, false), activity[1]);
Assert.Empty(recorder.Snapshot()); // unmapped → no output routed, but activity still fired
cts.Cancel();
await run;
}
[Fact]
public async Task KeypadKey_RoutesThroughOffsetAddress()
{
var fake = new FakeTransport();
var link = new RioSerialLink(fake, new RioSerialLinkOptions { AutoPollAnalog = false });
var recorder = new RecordingSink();
// Keypad 1, index 2 → address 0x62; map a keyboard key there (VK 0x41 = 'A').
var map = new RioInputMap { [0x62] = new RioMapEntry(0x0041) };
using var runtime = new RioRuntime(link, map, recorder, recorder);
runtime.Start();
using var cts = new CancellationTokenSource();
Task run = link.RunAsync(cts.Token);
fake.Enqueue(PacketBuilder.Build(RioCommand.KeyPressed, new byte[] { 1, 2 }));
await WaitUntilAsync(() => recorder.Snapshot().Contains("KeyDown(0x41,ext=False)"));
cts.Cancel();
await run;
}
}