PlasmaCommands.GraphicsWrite/GraphicsRow port the display firmware graphics command (ESC P s y x w h, MSB-left); PlasmaDisplay.RowAsync writes a locked whole-row update; the line protocol gains `plasma row <y> <hex32>`. The router plasma slot becomes a bounded FIFO queue: rows stream in order (a frame must not tear), texts still coalesce to the newest, clear flushes, cap 128 with counted drops. Docs updated; 442 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
119 lines
4.5 KiB
C#
119 lines
4.5 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 PipeClient_StreamsBitmapRows()
|
|
{
|
|
string name = UniqueName();
|
|
var plasmaTransport = new FakeTransport();
|
|
using var service = new FeedbackService(new FeedbackEndpointConfig { PipeName = name });
|
|
service.Start();
|
|
service.Attach(new RecordingSink(), new RioInputMap(),
|
|
new PlasmaDisplay(plasmaTransport), new ProfileFeedbackConfig());
|
|
|
|
using NamedPipeClientStream client = Connect(name);
|
|
Send(client, "plasma row 0 FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\n" +
|
|
"plasma row 1 80000000000000000000000000000001\n");
|
|
|
|
var solid = Enumerable.Repeat((byte)0xFF, 16).ToArray();
|
|
var edges = new byte[16];
|
|
edges[0] = 0x80;
|
|
edges[15] = 0x01;
|
|
Assert.Equal(PlasmaCommands.GraphicsRow(0, solid), await plasmaTransport.NextWriteAsync());
|
|
Assert.Equal(PlasmaCommands.GraphicsRow(1, edges), await plasmaTransport.NextWriteAsync());
|
|
}
|
|
|
|
[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());
|
|
}
|
|
}
|