等锁超时、全路径签名、无锁窗口不下发、Fault 终态可等待、Pass+机构不再短路、Alarm 不杀单、catch-up 停在未完成站;VehicleCode=0/重复车号拒绝注册,Hub 已运行时不抢监听口;FileLogger 空目录可写。 Co-authored-by: Cursor <cursoragent@cursor.com>
208 lines
8.0 KiB
C#
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
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|