Files
riojoy/tests/RioJoy.Core.Tests/Feedback/FeedbackServiceTests.cs
T
CydandClaude Fable 5 ad7ac19ab2 feedback: game-to-cockpit endpoint (pipe/UDP lamps+plasma), rumble lamp flash
Phase 9: FeedbackPipeServer (\\.\pipe\riojoy-feedback) + loopback UDP share a
forgiving text line protocol into FeedbackRouter; CoalescingLampScheduler rate-
governs the 9600-baud link; plasma finally wired into activation (greeting,
teardown blank, PlasmaDisplay write lock); ViGEm FeedbackReceived drives
RumbleLampAdapter. Per-profile Feedback config, docs/FEEDBACK.md, 425 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 19:31:03 -05:00

97 lines
3.6 KiB
C#

using System.IO.Pipes;
using System.Text;
using RioJoy.Core.Feedback;
using RioJoy.Core.Mapping;
using RioJoy.Core.Plasma;
using RioJoy.Core.Tests.Mapping;
using RioJoy.Core.Tests.Serial;
using Xunit;
namespace RioJoy.Core.Tests.Feedback;
/// <summary>
/// End-to-end: pipe client → line assembly → parse → router → lamp scheduler /
/// plasma, the exact path a sim export script exercises.
/// </summary>
public class FeedbackServiceTests
{
private static string UniqueName() => $"riojoy-fb-svc-{Guid.NewGuid():N}";
private static NamedPipeClientStream Connect(string name, int timeoutMs = 5000)
{
var deadline = DateTime.UtcNow.AddMilliseconds(timeoutMs);
while (true)
{
var client = new NamedPipeClientStream(".", name, PipeDirection.Out);
try
{
client.Connect(200);
return client;
}
catch (Exception ex) when (ex is IOException or TimeoutException)
{
client.Dispose();
Assert.True(DateTime.UtcNow < deadline, $"could not connect: {ex.Message}");
Thread.Sleep(20);
}
}
}
private static void Send(NamedPipeClientStream client, string text)
{
byte[] data = Encoding.GetEncoding(28591).GetBytes(text);
client.Write(data, 0, data.Length);
client.Flush();
}
[Fact]
public async Task PipeClient_DrivesLampsAndPlasma_MalformedLinesSurvive()
{
string name = UniqueName();
var lamps = new RecordingSink();
var plasmaTransport = new FakeTransport();
using var service = new FeedbackService(new FeedbackEndpointConfig { PipeName = name });
service.Start();
service.Attach(lamps, new RioInputMap(), new PlasmaDisplay(plasmaTransport),
new ProfileFeedbackConfig());
using NamedPipeClientStream client = Connect(name);
Send(client, "# cockpit warmup\nlamp 0x11 fast bright\nbogus nonsense\nplasma clear\n");
await FeedbackWait.For(() => lamps.Snapshot().Length >= 1);
Assert.Equal("Lamp(0x11,0x3F)", Assert.Single(lamps.Snapshot()));
Assert.Equal(PlasmaCommands.Clear(), await plasmaTransport.NextWriteAsync());
Assert.Equal(1, service.MalformedLines); // the bogus line, not the comment
Send(client, "lamp 0x11 off\n"); // the connection survived the bad line
await FeedbackWait.For(() => lamps.Snapshot().Length >= 2);
Assert.Equal("Lamp(0x11,0x00)", lamps.Snapshot()[1]);
}
[Fact]
public async Task Detach_DropsCommands_ReattachAppliesAgain()
{
string name = UniqueName();
var lamps = new RecordingSink();
using var service = new FeedbackService(new FeedbackEndpointConfig { PipeName = name });
service.Start();
using NamedPipeClientStream client = Connect(name);
// Dormant (never attached): commands drop, the client stays connected.
Send(client, "lamp 0x01 bright\n");
await FeedbackWait.For(() => service.DroppedCommands >= 1);
Assert.Empty(lamps.Snapshot());
// Profile activates: the same client now drives lamps.
service.Attach(lamps, new RioInputMap(), null, new ProfileFeedbackConfig());
Send(client, "lamp 0x01 bright\n");
await FeedbackWait.For(() => lamps.Snapshot().Length >= 1);
// Yield to a native game: back to dropping, still connected.
service.Detach();
Send(client, "lamp 0x01 off\n");
await FeedbackWait.For(() => service.DroppedCommands >= 2);
Assert.Single(lamps.Snapshot());
}
}