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; /// /// End-to-end: pipe client → line assembly → parse → router → lamp scheduler / /// plasma, the exact path a sim export script exercises. /// 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()); } }