Files
StandardSence/StandardScene.Magnetic.Tests/Tasking/Fass2TaskStateMachineMustFixTests.cs
T
黄兆尉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

208 lines
8.0 KiB
C#

using SimpleCore;
using StandardScene.Magnetic.Protocol;
using StandardScene.Magnetic.Tasking;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;
namespace StandardScene.Magnetic.Tests.Tasking
{
public class Fass2TaskStateMachineMustFixTests
{
[Fact]
public async Task WaitForCompletion_AfterBootstrapFault_Throws_AndBeginIsAllowed()
{
var machine = CreateMachine();
machine.Begin(10, 12);
var tick = machine.Tick(Report(1, moving: true, startStop: 1, state: 4));
Assert.True(tick.Faulted);
Assert.Equal(Fass2TaskPhase.Fault, machine.Context.Phase);
Assert.True(machine.IsIdle);
await Assert.ThrowsAsync<InvalidOperationException>(
() => machine.WaitForCompletionAsync(1000));
var ex = Record.Exception(() => machine.Begin(10, 12));
Assert.Null(ex);
Assert.Equal(Fass2TaskPhase.Planning, machine.Context.Phase);
}
[Fact]
public void PassingFirstStation_DoesNotRebuildForUnchangedFields()
{
var siteIds = new List<int> { 10, 11, 12 };
var planCalls = 0;
var taskIds = 0UL;
var plan = ThreeStationPlan(siteIds);
var machine = CreateMachine(
() =>
{
planCalls++;
return plan;
},
() => ++taskIds);
machine.Begin(10, 12);
machine.Tick(Report(1, moving: true, startStop: 1));
machine.Tick(Report(1, moving: false, startStop: 1));
machine.Tick(Report(1, moving: false, startStop: 1));
machine.Tick(Report(1, moving: false, startStop: 1));
Assert.Equal(1, machine.Context.CurrentIndex);
Assert.Equal(1UL, machine.Context.TaskId);
Assert.Equal(1, planCalls);
var afterAdvance = machine.Tick(Report(2, moving: true, startStop: 1));
Assert.False(afterAdvance.Rebuilt);
Assert.Equal(1, planCalls);
Assert.Equal(1UL, machine.Context.TaskId);
Assert.Equal(1, machine.Context.CurrentIndex);
Assert.Equal(Fass2TaskPhase.Moving, machine.Context.Phase);
}
[Fact]
public void FirstPrepareTrafficFailure_WaitsInsteadOfFault()
{
var machine = CreateMachine(
prepareTraffic: (_, __) => throw new InvalidOperationException("already locks it"));
machine.Begin(10, 12);
var tick = machine.Tick(Report(1, moving: true, startStop: 1));
Assert.False(tick.Faulted);
Assert.NotEqual(Fass2TaskPhase.Fault, machine.Context.Phase);
Assert.Equal(Fass2TaskPhase.Moving, machine.Context.Phase);
Assert.True(machine.Context.WaitingForTraffic);
Assert.Contains("wait", tick.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void ZeroLockedWindow_DoesNotDispatch()
{
var sent = new List<Fass2NodeMessage[]>();
var machine = CreateMachine(
ensureWindow: (_, __, ___) => 0,
onSendNodes: window => sent.Add(window));
machine.LockCount = 1;
machine.Begin(10, 12);
machine.Tick(Report(1, moving: true, startStop: 1));
Assert.Empty(sent);
Assert.True(machine.Context.WaitingForTraffic);
Assert.NotEqual(Fass2TaskPhase.Fault, machine.Context.Phase);
}
[Fact]
public void AlarmBits_DoNotFaultTask()
{
var machine = CreateMachine();
machine.Begin(10, 12);
var tick = machine.Tick(Report(1, moving: true, startStop: 1, alarm: 1));
Assert.False(tick.Faulted);
Assert.NotEqual(Fass2TaskPhase.Fault, machine.Context.Phase);
}
[Fact]
public void CatchUp_StopsAtUnfinishedStopStation()
{
var plan = new Fass2TaskPlan
{
StartSiteId = 10,
GoalSiteId = 12,
SiteIds = new List<int> { 10, 11, 12 },
Nodes = new List<Fass2NodeMessage>
{
new Fass2NodeMessage { Node = 1, StartStop = Fass2TaskBuilder.StartStopPass, Distance = 100 },
new Fass2NodeMessage { Node = 2, StartStop = Fass2TaskBuilder.StartStopStop },
new Fass2NodeMessage { Node = 3, StartStop = Fass2TaskBuilder.StartStopPass }
},
FieldsSignature = Fass2SiteFieldReader.BuildFieldsSignature(
new List<int> { 10, 11, 12 }, 0, SimpleLib.GetSite)
};
var machine = CreateMachine(() => plan, () => 1UL);
machine.LockCount = 4;
machine.Begin(10, 12);
machine.Tick(Report(1, moving: true, startStop: 1));
machine.Tick(Report(3, moving: true, startStop: 1));
Assert.Equal(1, machine.Context.CurrentIndex);
}
[Fact]
public async Task TrafficWait_TimesOutInsteadOfWaitingForever()
{
var machine = CreateMachine(
prepareTraffic: (_, __) => throw new InvalidOperationException("already locks it"));
machine.TrafficWaitTimeoutMs = 200;
machine.Begin(10, 12);
machine.Tick(Report(1, moving: true, startStop: 1));
await Assert.ThrowsAsync<TimeoutException>(
() => machine.WaitForCompletionAsync(10_000));
Assert.True(machine.IsIdle);
}
private static Fass2TaskStateMachine CreateMachine(
Func<Fass2TaskPlan> buildPlan = null,
Func<ulong> allocateTaskId = null,
Action<IReadOnlyList<int>, int> prepareTraffic = null,
Func<IReadOnlyList<int>, int, int, int> ensureWindow = null,
Action<Fass2NodeMessage[]> onSendNodes = null)
{
var plan = ThreeStationPlan(new List<int> { 10, 11, 12 });
return new Fass2TaskStateMachine(new Fass2TaskCallbacks
{
BuildPlan = (_, __, ___) => buildPlan != null ? buildPlan() : plan,
SendNodes = (window, _) => onSendNodes?.Invoke(window),
SendAction = (_, __) => { },
SendControl = (_, __) => { },
ResolveNodeId = id => (ushort)id,
AllocateTaskId = () => allocateTaskId != null ? allocateTaskId() : 1UL,
AllocateActionId = () => 9UL,
PrepareTraffic = prepareTraffic,
EnsureTrafficWindow = ensureWindow
})
{
StartBeforeMove = false,
LockCount = 4,
ResendIntervalMs = 60_000,
ActionRetryIntervalMs = 0
};
}
private static Fass2TaskPlan ThreeStationPlan(List<int> siteIds)
{
return new Fass2TaskPlan
{
StartSiteId = siteIds[0],
GoalSiteId = siteIds[siteIds.Count - 1],
SiteIds = siteIds,
Nodes = new List<Fass2NodeMessage>
{
new Fass2NodeMessage { Node = 1, StartStop = Fass2TaskBuilder.StartStopPass, Distance = 100 },
new Fass2NodeMessage { Node = 2, StartStop = Fass2TaskBuilder.StartStopPass, Distance = 100 },
new Fass2NodeMessage { Node = 3, StartStop = Fass2TaskBuilder.StartStopStop }
},
FieldsSignature = Fass2SiteFieldReader.BuildFieldsSignature(siteIds, 0, SimpleLib.GetSite)
};
}
private static Fass2StateReport Report(ushort node, bool moving, byte startStop, byte alarm = 0, byte? state = null)
{
return new Fass2StateReport
{
State = state ?? (byte)(moving ? 1 : 2),
Alarm = alarm,
Node = new Fass2NodeMessage
{
Node = node,
StartStop = startStop
}
};
}
}
}