fix: FASS2 状态机与 UDP 枢纽审查项
等锁超时、全路径签名、无锁窗口不下发、Fault 终态可等待、Pass+机构不再短路、Alarm 不杀单、catch-up 停在未完成站;VehicleCode=0/重复车号拒绝注册,Hub 已运行时不抢监听口;FileLogger 空目录可写。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user