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( () => 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 { 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(); 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 { 10, 11, 12 }, Nodes = new List { 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 { 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( () => machine.WaitForCompletionAsync(10_000)); Assert.True(machine.IsIdle); } private static Fass2TaskStateMachine CreateMachine( Func buildPlan = null, Func allocateTaskId = null, Action, int> prepareTraffic = null, Func, int, int, int> ensureWindow = null, Action onSendNodes = null) { var plan = ThreeStationPlan(new List { 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 siteIds) { return new Fass2TaskPlan { StartSiteId = siteIds[0], GoalSiteId = siteIds[siteIds.Count - 1], SiteIds = siteIds, Nodes = new List { 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 } }; } } }