Files
riojoy/tests/RioJoy.Core.Tests/Protocol/PacketParserTests.cs
T
CydandClaude Opus 4.8 b3cb764f4d Phase 2: serial + RIO protocol core (RioJoy.Core) with unit tests
Port the RIO wire protocol from legacy/riovjoy2.cpp into testable C#:

- Protocol/: command + length table, 7-bit checksum, packet builder, and a
  streaming receive-side framing state machine (PacketParser) that mirrors the
  legacy ReadCommBlock framing/resync (high-bit-mid-packet abort). Typed RIO->PC
  decodes: AnalogReport (14-bit sign-extend), VersionInfo, CheckStatus; lamp-state
  composition.
- Serial/: RioSerialLink drives an async receive loop with ACK/NAK reply policy
  (legacy force-accept vs. opt-in VerifyInboundChecksum), the analog poll timer,
  and the >5s reset-recovery watchdog. IRioTransport abstracts the COM port; the
  SerialPort-backed transport does 9600 8N1 + DTR reset pulse, and acquire/release
  is just create/dispose (foundation for native-game serial yield).
- tests/RioJoy.Core.Tests: 54 xUnit tests covering checksum, framing/resync,
  builder round-trips, analog sign-extension + sentinel rejection, lamp combos,
  and the read loop driven against an in-memory fake transport.

Hardware verification (version/check/analog against a cabinet) remains; it can't
be done off-device.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 13:04:03 -05:00

121 lines
4.1 KiB
C#

using RioJoy.Core.Protocol;
using Xunit;
namespace RioJoy.Core.Tests.Protocol;
public class PacketParserTests
{
private static List<RioRxEvent> FeedAll(PacketParser parser, ReadOnlySpan<byte> data)
{
var events = new List<RioRxEvent>();
foreach (byte b in data)
{
if (parser.Feed(b, out RioRxEvent ev))
events.Add(ev);
}
return events;
}
[Fact]
public void FramesValidPacket_WithChecksumValid()
{
var parser = new PacketParser();
byte[] frame = PacketBuilder.Build(RioCommand.ButtonPressed, new byte[] { 0x05 });
List<RioRxEvent> events = FeedAll(parser, frame);
RioRxEvent ev = Assert.Single(events);
Assert.Equal(RioRxEventKind.Packet, ev.Kind);
Assert.True(ev.ChecksumValid);
Assert.Equal(RioCommand.ButtonPressed, ev.Packet.Command);
Assert.Equal(new byte[] { 0x05 }, ev.Packet.Payload.ToArray());
Assert.False(parser.InPacket);
}
[Fact]
public void ReportsChecksumInvalid_ButStillFramesPacket()
{
var parser = new PacketParser();
byte[] frame = PacketBuilder.Build(RioCommand.ButtonPressed, new byte[] { 0x05 });
frame[^1] ^= 0x01; // corrupt checksum (stays high-bit clear)
RioRxEvent ev = Assert.Single(FeedAll(parser, frame));
Assert.Equal(RioRxEventKind.Packet, ev.Kind);
Assert.False(ev.ChecksumValid);
}
[Fact]
public void ControlByte_OutsideFraming_IsReported()
{
var parser = new PacketParser();
RioRxEvent ev = Assert.Single(FeedAll(parser, new[] { (byte)RioControl.Ack }));
Assert.Equal(RioRxEventKind.ControlByte, ev.Kind);
Assert.True(ev.IsControl(RioControl.Ack));
}
[Fact]
public void HighBitMidPacket_AbortsAndResyncs()
{
var parser = new PacketParser();
// Start a ButtonPressed (expects 1 payload + 1 checksum), feed one payload
// byte, then a high-bit byte mid-packet → framing error + resync.
var events = new List<RioRxEvent>();
foreach (byte b in new byte[] { 0x88, 0x05, 0x82 })
{
if (parser.Feed(b, out RioRxEvent ev))
events.Add(ev);
}
RioRxEvent framing = Assert.Single(events);
Assert.Equal(RioRxEventKind.FramingError, framing.Kind);
Assert.False(parser.InPacket);
// A subsequent clean frame parses normally (the parser resynced).
byte[] next = PacketBuilder.Build(RioCommand.ButtonReleased, new byte[] { 0x07 });
RioRxEvent ev2 = Assert.Single(FeedAll(parser, next));
Assert.Equal(RioCommand.ButtonReleased, ev2.Packet.Command);
}
[Fact]
public void ParsesMultiplePackets_InOneStream()
{
var parser = new PacketParser();
byte[] a = PacketBuilder.Build(RioCommand.ButtonPressed, new byte[] { 0x01 });
byte[] b = PacketBuilder.Build(RioCommand.ButtonReleased, new byte[] { 0x02 });
byte[] stream = new byte[a.Length + b.Length];
a.CopyTo(stream, 0);
b.CopyTo(stream, a.Length);
List<RioRxEvent> events = FeedAll(parser, stream);
Assert.Equal(2, events.Count);
Assert.Equal(RioCommand.ButtonPressed, events[0].Packet.Command);
Assert.Equal(RioCommand.ButtonReleased, events[1].Packet.Command);
}
[Fact]
public void HandlesPacket_SplitAcrossFeeds()
{
var parser = new PacketParser();
byte[] frame = PacketBuilder.Build(RioCommand.AnalogReply, new byte[] { 1, 0, 2, 0, 3, 0, 4, 0, 5, 0 });
// Feed byte-by-byte: only the final byte should yield the packet event.
var events = new List<RioRxEvent>();
for (int i = 0; i < frame.Length; i++)
{
bool produced = parser.Feed(frame[i], out RioRxEvent ev);
if (produced)
events.Add(ev);
Assert.Equal(i == frame.Length - 1, produced);
}
RioRxEvent done = Assert.Single(events);
Assert.Equal(RioCommand.AnalogReply, done.Packet.Command);
Assert.True(done.ChecksumValid);
}
}