Files
黄兆尉andCursor e4a245e644 fix: FASS2 状态机与 UDP 枢纽审查项
等锁超时、全路径签名、无锁窗口不下发、Fault 终态可等待、Pass+机构不再短路、Alarm 不杀单、catch-up 停在未完成站;VehicleCode=0/重复车号拒绝注册,Hub 已运行时不抢监听口;FileLogger 空目录可写。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 23:04:58 +08:00

135 lines
4.4 KiB
C#

using StandardScene.Magnetic.Protocol;
using StandardScene.Magnetic.Tests.TestHelpers;
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Xunit;
namespace StandardScene.Magnetic.Tests.Protocol
{
public sealed class Fass2UdpHubMultiCarTests : IDisposable
{
private readonly int _hubPort;
private readonly TestUdpCar _car1;
private readonly TestUdpCar _car2;
public Fass2UdpHubMultiCarTests()
{
_hubPort = AllocateUdpPort();
_car1 = new TestUdpCar(1, AllocateUdpPort());
_car2 = new TestUdpCar(2, AllocateUdpPort());
Fass2UdpHub.Register(_car1);
Fass2UdpHub.Register(_car2);
Fass2UdpHub.EnsureStarted(_hubPort);
Thread.Sleep(100);
}
[Fact]
public void Hub_RoutesStateToRegisteredCar_AndRepliesWith0x10()
{
SendState(_car1.VehicleCode, 2, 10, 100);
var ack1 = _car1.WaitForAck();
Assert.NotNull(ack1);
Assert.Equal(Fass2Protocol.ControlFrameLength, ack1.Length);
Assert.Equal(Fass2Protocol.CmdStateResponse, ack1[1]);
Assert.Equal(0x01, ack1[2]);
Assert.Single(_car1.Reports);
Assert.Equal((ushort)10, _car1.Reports[0].Node.Node);
Assert.Empty(_car2.Reports);
}
[Fact]
public void Hub_DispatchesMultipleCarsIndependently()
{
SendState(1, 1, 11, 201);
SendState(2, 2, 22, 202);
Assert.NotNull(_car1.WaitForAck());
Assert.NotNull(_car2.WaitForAck());
Assert.Single(_car1.Reports);
Assert.Single(_car2.Reports);
Assert.Equal((ushort)11, _car1.Reports[0].Node.Node);
Assert.Equal((ushort)22, _car2.Reports[0].Node.Node);
Assert.Equal((ulong)201, _car1.Reports[0].Task);
Assert.Equal((ulong)202, _car2.Reports[0].Task);
}
[Fact]
public void EnsureStarted_NewPort_ReceivesOnNewPort()
{
var newPort = AllocateUdpPort();
Fass2UdpHub.EnsureStarted(newPort);
Thread.Sleep(150);
var frame = Fass2TestFrameBuilder.BuildStateFrame(1, 1, 88, 301);
using var sender = new UdpClient();
sender.Send(frame, frame.Length, new IPEndPoint(IPAddress.Loopback, newPort));
Assert.NotNull(_car1.WaitForAck());
Assert.Equal((ushort)88, _car1.Reports[_car1.Reports.Count - 1].Node.Node);
}
[Fact]
public void Hub_IgnoresUnregisteredCarCode()
{
SendState(99, 2, 30, 0);
Thread.Sleep(300);
Assert.Empty(_car1.Reports);
Assert.Empty(_car2.Reports);
}
[Fact]
public void Register_VehicleCodeZero_DoesNotReceiveState()
{
var zeroPort = AllocateUdpPort();
using var zero = new TestUdpCar(0, zeroPort);
Assert.False(Fass2UdpHub.Register(zero));
SendState(0, 2, 40, 0);
Thread.Sleep(200);
Assert.Empty(zero.Reports);
}
[Fact]
public void Register_DuplicateVehicleCode_KeepsFirstCar()
{
var dupPort = AllocateUdpPort();
using var dup = new TestUdpCar(1, dupPort);
Assert.False(Fass2UdpHub.Register(dup));
SendState(1, 2, 41, 401);
Assert.NotNull(_car1.WaitForAck());
Assert.Equal((ushort)41, _car1.Reports[_car1.Reports.Count - 1].Node.Node);
Assert.Empty(dup.Reports);
}
public void Dispose()
{
Fass2UdpHub.Unregister(_car1);
Fass2UdpHub.Unregister(_car2);
Fass2UdpHub.Stop();
_car1.Dispose();
_car2.Dispose();
}
private void SendState(ushort carCode, byte state, ushort nodeId, ulong taskId)
{
var frame = Fass2TestFrameBuilder.BuildStateFrame(carCode, state, nodeId, taskId);
using var sender = new UdpClient();
sender.Send(frame, frame.Length, new IPEndPoint(IPAddress.Loopback, _hubPort));
Thread.Sleep(80);
}
private static int AllocateUdpPort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
}
}