using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Sandbox.ModAPI.Ingame;
using VRage.Game.ModAPI.Ingame.Utilities;
using VRageMath;
using Xunit;
using P = AutoMiningScript.Program;
namespace AutoMiningScript.Tests
{
public class FleetResetTests
{
const BindingFlags Private = BindingFlags.Instance | BindingFlags.NonPublic;
const string Configuration = "[System]\nId=base\nFleetId=mining\nBaseId=base\n[Fleet]\nDockGroup=AMS Docks\n; keep my configuration\n";
static T Read<T>(object o, string name) { return (T)o.GetType().GetField(name, Private).GetValue(o); }
static void Write(object o, string name, object value) { o.GetType().GetField(name, Private).SetValue(o, value); }
static MyIni Save(P.RoleLogic role) { var data = new MyIni(); role.Save(data); return data; }
static P.FleetController Current(P program) { return Assert.IsType<P.FleetController>(Read<P.RoleLogic>(program, "role")); }
static FleetStateTests.Rig ActiveFleet()
{
var r = new FleetStateTests.Rig(2, false);
r.Program.Me.CustomData = Configuration; Write(r.Program, "role", r.Fleet);
r.RegisterAll(); r.Tick(30); r.Receive("DOCK_REQUEST", r.Miners[0]); r.Tick(12);
r.Fleet.Command("mark 24 6 20 GPS:entry:0:0:-200: GPS:inside:0:0:-201:"); r.Tick(10);
r.Fleet.Command("start"); r.Checkpoint();
Assert.NotEmpty(r.Fleet.GetJobs()); Assert.NotEmpty(r.Fleet.GetTelemetry());
Assert.True(P.Data.ReadLong(r.Save(), "fleet", "LaneCount") > 0);
return r;
}
static void AssertEmpty(P.FleetController fleet)
{
Assert.False(fleet.Restoring); Assert.Empty(fleet.GetJobs()); Assert.Empty(fleet.GetTelemetry());
var data = Save(fleet); Assert.False(P.Data.Flag(data, "fleet", "Running"));
foreach (var key in new[] { "MinerCount", "LaneCount", "CancellationCount", "ClearanceCount", "ProtectedOwnerCount", "MarkerCount", "OperatorStopCount", "NextWaitingSlot", "TokenSerial" })
Assert.Equal(0, P.Data.ReadLong(data, "fleet", key));
Assert.Equal(0, Read<P.TaskPlanner>(fleet, "planner").TaskSerial);
}
[Theory]
[InlineData("init")]
[InlineData("INIT")]
[InlineData(" init ")]
[InlineData("初始化")]
public void MainInitDropsAllSavedAndInMemoryWorkWithoutChangingConfiguration(string command)
{
if (command == "初始化" && P.L.Language != "zh-CN") return;
var r = ActiveFleet(); var previousStorage = r.Program.Storage; var previousSession = r.Program.Bus.Session;
r.Program.Events.Enqueue("old event"); r.Sent.Clear();
r.Program.Main(command, UpdateType.Trigger);
var reset = Current(r.Program); Assert.NotSame(r.Fleet, reset); AssertEmpty(reset);
Assert.Equal(Configuration, r.Program.Me.CustomData); Assert.Equal("base", r.Program.Config.Id);
Assert.Equal("fleet", r.Program.Config.Role); Assert.NotEqual(previousStorage, r.Program.Storage);
Assert.Equal(P.FleetController.ResetStorage(r.Program.Bus.Epoch), r.Program.Storage);
Assert.True(r.Program.Bus.Epoch > 0); Assert.NotEqual(previousSession, r.Program.Bus.Session);
Assert.Equal(0, r.Program.Bus.PendingCount); Assert.DoesNotContain("old event", r.Program.Events);
var recall = Assert.Single(r.Sent); Assert.Equal("COMMAND", recall.Kind);
Assert.Equal("", recall.Target); Assert.Equal("return", P.Data.Text(recall.Body, "command", "Text"));
Assert.Equal(r.Program.Bus.Epoch, recall.Epoch);
}
[Theory]
[InlineData("legacy")]
[InlineData("huge legacy")]
[InlineData("corrupt current")]
[InlineData("half restored")]
[InlineData("initialization failed")]
public void InitPreemptsUnreadableOrPartiallyRestoredStorage(string scenario)
{
var r = ActiveFleet();
var old = scenario == "huge legacy" ? "[planner]\nJobs=" + new string('x', 1600000) :
scenario == "legacy" ? "[storage]\nVersion=2\nRole=fleet\nId=base\n" : "XFEAMS-FLEET-3\n999999:broken";
if (scenario == "half restored")
{
old = r.Program.Storage; var restoring = new P.FleetController(r.Program);
restoring.BeginRestoreStorage(old); Write(r.Program, "role", restoring); Assert.True(restoring.Restoring);
}
if (scenario == "initialization failed") { Write(r.Program, "role", null); Write(r.Program, "storageFailure", true); }
TestRig.SetBase(r.Program, "Storage", old); r.Program.Main("init", UpdateType.Trigger);
AssertEmpty(Current(r.Program)); Assert.DoesNotContain(old, r.Program.Storage);
Assert.Equal(P.FleetController.ResetStorage(r.Program.Bus.Epoch), r.Program.Storage);
}
[Fact]
public void ResetMarkerIsDurableEvenWhenUserConfigurationIsInvalid()
{
var r = ActiveFleet(); r.Program.Me.CustomData = "[broken";
r.Program.Main("init", UpdateType.Trigger);
Assert.Null(Read<P.RoleLogic>(r.Program, "role")); Assert.Equal("[broken", r.Program.Me.CustomData);
Assert.Equal(0, r.Program.Bus.PendingCount); var marker = r.Program.Storage;
Assert.StartsWith("XFEAMS-RESET-1\n", marker);
var restart = new FleetStateTests.Rig(2, false); restart.Restore(marker); AssertEmpty(restart.Fleet);
r.Program.Me.CustomData = Configuration; r.Program.Main("init", UpdateType.Trigger); AssertEmpty(Current(r.Program));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void LegacyIniIsNeverMigratedOrReplacedDuringOrdinaryRestore(bool large)
{
var r = new FleetStateTests.Rig(0, false);
var text = "[storage]\nVersion=2\nRole=fleet\nId=base\n[planner]\nJobs=0\n" + (large ? ";" + new string('x', 1000000) : "");
TestRig.SetBase(r.Program, "Storage", text);
Assert.Equal(P.L.FleetLegacyResetRequired, Assert.Throws<ArgumentException>(() => r.Fleet.BeginRestoreStorage(text)).Message);
Assert.Equal(text, r.Program.Storage); Assert.False(r.Fleet.Restoring);
}
[Fact]
public void EmptyResetSurvivesImmediateRestartAndCurrentCheckpointRestore()
{
var r = ActiveFleet(); r.Program.Main("init", UpdateType.Trigger); var epoch = r.Program.Bus.Epoch;
var immediate = new FleetStateTests.Rig(2, false); immediate.Restore(r.Program.Storage);
AssertEmpty(immediate.Fleet); Assert.Equal(epoch, immediate.Program.Bus.Epoch);
var checkpoint = immediate.Checkpoint(); Assert.StartsWith("XFEAMS-FLEET-3\n", checkpoint);
var restarted = new FleetStateTests.Rig(2, false); restarted.Restore(checkpoint);
AssertEmpty(restarted.Fleet); Assert.Equal(epoch, restarted.Program.Bus.Epoch);
immediate.Program.Me.CustomData = Configuration; Write(immediate.Program, "role", immediate.Fleet);
immediate.Program.Main("init", UpdateType.Trigger); Assert.True(immediate.Program.Bus.Epoch > epoch);
}
[Fact]
public void NormalCurrentFormatRestoreStillPreservesJobsAndOccupancy()
{
var r = ActiveFleet(); r.Program.Bus.Epoch = 42; var checkpoint = r.Checkpoint();
var restored = new FleetStateTests.Rig(2, false); restored.Restore(checkpoint);
Assert.Equal(42, restored.Program.Bus.Epoch); Assert.Equal(r.Fleet.GetJobs().Select(j => j.Id), restored.Fleet.GetJobs().Select(j => j.Id));
Assert.Equal(2, restored.Fleet.GetTelemetry().Count); Assert.Equal(1, P.Data.ReadLong(restored.Save(), "fleet", "LaneCount"));
Assert.All(restored.Fleet.GetTelemetry(), t => Assert.True(t.ReceivedAt < 0));
}
[Theory]
[InlineData("init miner-0")]
[InlineData("rescan")]
public void TargetedInitializationAndRescanKeepFleetStorage(string command)
{
var r = ActiveFleet(); var storage = r.Program.Storage; var count = r.Fleet.GetJobs().Count;
r.Sent.Clear(); r.Fleet.Command(command);
Assert.Same(r.Fleet, Current(r.Program)); Assert.Equal(storage, r.Program.Storage); Assert.Equal(count, r.Fleet.GetJobs().Count);
Assert.Contains(r.Sent, p => p.Kind == "COMMAND" && P.Data.Text(p.Body, "command", "Text") == "rescan");
}
[Fact]
public void DirectFleetCommandUsesTheSameResetEntryPoint()
{
var r = ActiveFleet(); r.Fleet.Command("init"); AssertEmpty(Current(r.Program));
}
[Theory]
[InlineData("MARK")]
[InlineData("SURVEY_REQUEST")]
[InlineData("RESULT")]
[InlineData("DEPART_REQUEST")]
[InlineData("HELLO")]
[InlineData("TELEMETRY")]
public void OldMessagesCannotRecreateWorkAfterReset(string kind)
{
var r = ActiveFleet(); r.Program.Main("init", UpdateType.Trigger); var reset = Current(r.Program); r.Sent.Clear();
var body = kind == "MARK" ? P.Marking.Parse(r.Program, "mark 6 6 20 GPS:entry:0:0:-200: GPS:inside:0:0:-201:") : new MyIni();
reset.Receive(new P.Packet { Epoch = 0, Kind = kind, From = "miner-0", Source = 5000, Body = body });
AssertEmpty(reset); Assert.Contains(r.Sent, p => p.Kind == "COMMAND" && p.Epoch == r.Program.Bus.Epoch);
}
[Fact]
public void ReturningMinerMustAdoptCurrentGenerationBeforeNewAssignments()
{
var r = ActiveFleet(); r.Program.Main("init", UpdateType.Trigger); var reset = Current(r.Program);
r.SetConnected(0, true);
var old = new P.Packet { Kind = "HELLO", From = "miner-0", Source = 5000, Session = "old", Body = r.Miners[0].ToIni() };
reset.Receive(old); Assert.Empty(reset.GetTelemetry());
reset.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inside:0:0:-201:"); reset.Command("start");
for (int i = 0; i < 20; i++) { r.Program.Now += .1; reset.Tick(.1); }
Assert.All(reset.GetJobs(), j => Assert.Equal("", j.Owner));
var current = new P.Packet { Kind = "HELLO", Epoch = r.Program.Bus.Epoch, From = "miner-0", Source = 5000, Session = "fresh", Body = r.Miners[0].ToIni() };
reset.Receive(current); reset.Receive(old);
Assert.Equal(r.Program.Bus.Epoch, Assert.Single(reset.GetTelemetry()).Epoch);
for (int i = 0; i < 20; i++) { r.Program.Now += .1; reset.Tick(.1); }
Assert.Contains(reset.GetJobs(), j => j.Owner == "miner-0");
}
static P.Packet ResetPacket(P program, long epoch = 100)
{
var body = new MyIni(); body.Set("command", "Text", "return"); body.Set("command", "Reset", true);
return new P.Packet { From = program.Config.BaseId, Epoch = epoch, Kind = "COMMAND", Body = body };
}
[Theory]
[InlineData(P.FlightState.Transit)]
[InlineData(P.FlightState.Survey)]
[InlineData(P.FlightState.DockAlign)]
[InlineData(P.FlightState.DockApproach)]
[InlineData(P.FlightState.Holding)]
public void AirborneResetCancelsOldWorkAndReacquiresItsReturnRoute(P.FlightState state)
{
var r = new MinerStateTests.MinerRig(); r.Task(); r.State(state);
Write(r.Miner, "lane", "old-permit"); Write(r.Miner, "dock", new P.DockFrame());
r.Program.Bus.Send("RESULT", r.Program.Config.BaseId, new MyIni(), true); r.Drill.Set("Enabled", true);
r.Miner.Receive(ResetPacket(r.Program));
Assert.Equal(P.FlightState.Returning, r.StateNow); Assert.Equal("", Read<string>(r.Miner, "lane")); Assert.Null(Read<P.DockFrame>(r.Miner, "dock"));
Assert.True(Read<bool>(r.Miner, "canceling")); Assert.False(Read<bool>(r.Miner, "leaseConfirmed")); Assert.False(r.Drill.Value.Enabled);
Assert.Equal(0, r.Program.Bus.PendingCount); Assert.Equal(100, r.Program.Bus.Epoch);
Assert.Equal(100, P.Data.ReadLong(Save(r.Miner), "miner", "FleetEpoch"));
}
[Fact]
public void BoreResetRetainsTheOriginalExitAxisUntilRetreatIsComplete()
{
var r = new MinerStateTests.MinerRig(); r.Task(12); r.State(P.FlightState.Drilling);
var job = Assert.Single(r.Miner.GetJobs()); var direction = job.Direction;
r.Drill.Set("Enabled", true); r.Miner.Receive(ResetPacket(r.Program));
Assert.Equal(P.FlightState.Retreat, r.StateNow); Assert.Same(job, Assert.Single(r.Miner.GetJobs()));
Assert.Equal(direction, job.Direction); Assert.Equal(12, job.Progress); Assert.True(Read<bool>(r.Miner, "insideBore")); Assert.False(r.Drill.Value.Enabled);
var saved = Save(r.Miner); var restarted = new MinerStateTests.MinerRig(); restarted.Miner.Load(saved);
Assert.Equal(100, restarted.Program.Bus.Epoch); Assert.True(Read<bool>(restarted.Miner, "insideBore"));
Assert.Equal(direction, Assert.Single(restarted.Miner.GetJobs()).Direction);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void ResetDoesNotTakeOverManualControlOrBrokenHardware(bool manual)
{
var r = new MinerStateTests.MinerRig(); r.Task();
r.State(manual ? P.FlightState.Manual : P.FlightState.Fault);
if (manual) r.Controller.Set("IsUnderControl", true); else Write(r.Miner, "healthy", false);
r.Miner.Receive(ResetPacket(r.Program));
r.Miner.Receive(ResetPacket(r.Program));
Assert.Equal(manual ? P.FlightState.Manual : P.FlightState.Fault, r.StateNow);
Assert.True(Read<bool>(r.Miner, "canceling")); Assert.False(Read<bool>(r.Miner, "running"));
}
[Fact]
public void DockedMinerDiscardsCancelledSerialWatermarkButRejectsEveryOldGenerationPacket()
{
var r = new MinerStateTests.MinerRig(); r.PrepareDeparture("old"); Write(r.Miner, "cancelledThrough", 500L);
r.Miner.Receive(ResetPacket(r.Program)); Assert.Empty(r.Miner.GetJobs()); Assert.False(Read<bool>(r.Miner, "canceling"));
Assert.Equal(-1, Read<long>(r.Miner, "cancelledThrough")); Assert.True(r.Connector.Value.Status == MyShipConnectorStatus.Connected);
var job = new P.Job { Id = r.Program.Config.BaseId + "-job-1", Owner = r.Program.Config.Id, Depth = 10, Direction = Vector3D.Forward, Up = Vector3D.Up };
var packet = new P.Packet { Kind = "JOB", From = r.Program.Config.BaseId, Body = job.ToIni() };
r.Miner.Receive(packet); Assert.Empty(r.Miner.GetJobs());
packet.Epoch = 100; r.Miner.Receive(packet); Assert.Equal(job.Id, Assert.Single(r.Miner.GetJobs()).Id);
var staleCancel = ResetPacket(r.Program, 99); staleCancel.Body.Set("command", "Text", "cancel-through 999");
staleCancel.Body.Set("command", "Reset", false);
r.Miner.Receive(staleCancel); Assert.Equal(job.Id, Assert.Single(r.Miner.GetJobs()).Id);
r.Miner.Receive(new P.Packet { From = r.Program.Config.BaseId, Epoch = 99, Kind = "DOCK_GRANT", Body = new P.DockFrame { Token = "old", BaseId = r.Program.Config.BaseId }.ToIni() });
Assert.Equal("", Read<string>(r.Miner, "lane"));
}
sealed class Transport
{
readonly P program;
public int Registrations, Disabled;
public readonly Queue<MyIGCMessage> Incoming = new Queue<MyIGCMessage>();
public readonly List<P.Packet> Sent = new List<P.Packet>();
public Transport(P p)
{
program = p;
var listener = new Stub<IMyUnicastListener>().Method("get_HasPendingMessage", a => Incoming.Count > 0).Method("AcceptMessage", a => Incoming.Dequeue());
TestRig.SetBase(p, "IGC", new Stub<IMyIntergridCommunicationSystem>().Set("UnicastListener", listener.Value)
.Method("RegisterBroadcastListener", a => { Registrations++; return new Stub<IMyBroadcastListener>().Value; })
.Method("DisableBroadcastListener", a => { Disabled++; return null; })
.Method("SendBroadcastMessage", a => Capture((string)a[1]))
.Method("SendUnicastMessage", a => { Capture((string)a[2]); return true; }).Value);
p.Bus = new P.Wire(p);
}
object Capture(string text) { P.Packet packet; Assert.True(P.Packet.TryDecode(text, program.Config.FleetId, out packet)); Sent.Add(packet); return null; }
public void Add(P.Packet packet, long source = 5000)
{ Incoming.Enqueue(new MyIGCMessage(packet.Encode(program.Config.FleetId), "AMS2/" + program.Config.FleetId, source)); }
}
[Fact]
public void RepeatedInitClearsRetriesWithoutRegisteringDuplicateListeners()
{
var r = ActiveFleet(); var transport = new Transport(r.Program);
for (int n = 0; n < 4; n++)
{
r.Program.Bus.Send("JOB", "miner-0", new MyIni(), true); r.Program.Main("init", UpdateType.Trigger);
Assert.Equal(0, r.Program.Bus.PendingCount); AssertEmpty(Current(r.Program));
}
Assert.Equal(1, transport.Registrations); Assert.Equal(0, transport.Disabled);
r.Program.Me.CustomData = Configuration.Replace("FleetId=mining", "FleetId=another"); r.Program.Main("init", UpdateType.Trigger);
Assert.Equal(2, transport.Registrations); Assert.Equal(1, transport.Disabled);
}
[Fact]
public void OldGenerationCannotPoisonTheWireSessionOfAnAlreadyReconnectedMiner()
{
var r = ActiveFleet(); var transport = new Transport(r.Program); r.Program.Main("init", UpdateType.Trigger); var fleet = Current(r.Program);
var current = new P.Packet { Kind = "HELLO", From = "miner-0", Epoch = r.Program.Bus.Epoch, Seq = 1, Session = "fresh", Id = "fresh1", Body = r.Miners[0].ToIni() };
transport.Add(current); r.Program.Bus.Drain(fleet.Receive);
var old = new P.Packet { Kind = "HELLO", From = "miner-0", Epoch = 0, Seq = 999, Session = "old", Id = "old999", Body = r.Miners[0].ToIni() };
transport.Add(old); r.Program.Bus.Drain(fleet.Receive);
r.Miners[0].Battery = .7; current.Body = r.Miners[0].ToIni(); current.Seq++; current.Id = "fresh2";
transport.Add(current); r.Program.Bus.Drain(fleet.Receive);
var actual = Assert.Single(fleet.GetTelemetry()); Assert.Equal(.7, actual.Battery, 8); Assert.Equal(current.Epoch, actual.Epoch);
}
[Fact]
public void MarkerDropsPendingOldMarksWhenBaseGenerationChangesAndPersistsIt()
{
var p = TestRig.Program(); TestRig.SetBase(p, "GridTerminalSystem", new Stub<IMyGridTerminalSystem>().Value);
var transport = new Transport(p); var marker = new P.MarkerController(p);
marker.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inside:0:0:-201:"); Assert.Equal(1, p.Bus.PendingCount);
marker.Receive(ResetPacket(p)); Assert.Equal(0, p.Bus.PendingCount); Assert.Equal(100, p.Bus.Epoch);
var saved = Save(marker); p.Bus.Epoch = 0; marker.Load(saved); Assert.Equal(100, p.Bus.Epoch);
marker.Receive(ResetPacket(p, 99)); Assert.Equal(100, p.Bus.Epoch);
marker.Command("survey 300"); Assert.Equal(100, transport.Sent.Last().Epoch);
}
[Theory]
[InlineData("-1")]
[InlineData("3155378976000000000")]
[InlineData("NaN")]
public void InvalidResetGenerationIsRejectedByStorageAndProtocol(string value)
{
var r = new FleetStateTests.Rig(0, false);
Assert.Throws<ArgumentException>(() => r.Fleet.BeginRestoreStorage("XFEAMS-RESET-1\n" + value));
var packet = new P.Packet { Kind = "BASE", From = "base", Session = "s", Seq = 1 };
var ini = new MyIni(); ini.TryParse(packet.Encode("mining")); ini.Set("wire", "Epoch", value);
P.Packet decoded; Assert.False(P.Packet.TryDecode(ini.ToString(), "mining", out decoded));
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Sandbox.ModAPI.Ingame;
using VRage.Game.ModAPI.Ingame.Utilities;
using VRageMath;
using Xunit;
using P = AutoMiningScript.Program;
namespace AutoMiningScript.Tests
{
public class FleetResetTests
{
const BindingFlags Private = BindingFlags.Instance | BindingFlags.NonPublic;
const string Configuration = "[System]\nId=base\nFleetId=mining\nBaseId=base\n[Fleet]\nDockGroup=AMS Docks\n; keep my configuration\n";
static T Read<T>(object o, string name) { return (T)o.GetType().GetField(name, Private).GetValue(o); }
static void Write(object o, string name, object value) { o.GetType().GetField(name, Private).SetValue(o, value); }
static MyIni Save(P.RoleLogic role) { var data = new MyIni(); role.Save(data); return data; }
static P.FleetController Current(P program) { return Assert.IsType<P.FleetController>(Read<P.RoleLogic>(program, "role")); }
static FleetStateTests.Rig ActiveFleet()
{
var r = new FleetStateTests.Rig(2, false);
r.Program.Me.CustomData = Configuration; Write(r.Program, "role", r.Fleet);
r.RegisterAll(); r.Tick(30); r.Receive("DOCK_REQUEST", r.Miners[0]); r.Tick(12);
r.Fleet.Command("mark 24 6 20 GPS:entry:0:0:-200: GPS:inside:0:0:-201:"); r.Tick(10);
r.Fleet.Command("start"); r.Checkpoint();
Assert.NotEmpty(r.Fleet.GetJobs()); Assert.NotEmpty(r.Fleet.GetTelemetry());
Assert.True(P.Data.ReadLong(r.Save(), "fleet", "LaneCount") > 0);
return r;
}
static void AssertEmpty(P.FleetController fleet)
{
Assert.False(fleet.Restoring); Assert.Empty(fleet.GetJobs()); Assert.Empty(fleet.GetTelemetry());
var data = Save(fleet); Assert.False(P.Data.Flag(data, "fleet", "Running"));
foreach (var key in new[] { "MinerCount", "LaneCount", "CancellationCount", "ClearanceCount", "ProtectedOwnerCount", "MarkerCount", "OperatorStopCount", "NextWaitingSlot", "TokenSerial" })
Assert.Equal(0, P.Data.ReadLong(data, "fleet", key));
Assert.Equal(0, Read<P.TaskPlanner>(fleet, "planner").TaskSerial);
}
[Theory]
[InlineData("init")]
[InlineData("INIT")]
[InlineData(" init ")]
[InlineData("初始化")]
public void MainInitDropsAllSavedAndInMemoryWorkWithoutChangingConfiguration(string command)
{
if (command == "初始化" && P.L.Language != "zh-CN") return;
var r = ActiveFleet(); var previousStorage = r.Program.Storage; var previousSession = r.Program.Bus.Session;
r.Program.Events.Enqueue("old event"); r.Sent.Clear();
r.Program.Main(command, UpdateType.Trigger);
var reset = Current(r.Program); Assert.NotSame(r.Fleet, reset); AssertEmpty(reset);
Assert.Equal(Configuration, r.Program.Me.CustomData); Assert.Equal("base", r.Program.Config.Id);
Assert.Equal("fleet", r.Program.Config.Role); Assert.NotEqual(previousStorage, r.Program.Storage);
Assert.Equal(P.FleetController.ResetStorage(r.Program.Bus.Epoch), r.Program.Storage);
Assert.True(r.Program.Bus.Epoch > 0); Assert.NotEqual(previousSession, r.Program.Bus.Session);
Assert.Equal(0, r.Program.Bus.PendingCount); Assert.DoesNotContain("old event", r.Program.Events);
var recall = Assert.Single(r.Sent); Assert.Equal("COMMAND", recall.Kind);
Assert.Equal("", recall.Target); Assert.Equal("return", P.Data.Text(recall.Body, "command", "Text"));
Assert.Equal(r.Program.Bus.Epoch, recall.Epoch);
}
[Theory]
[InlineData("legacy")]
[InlineData("huge legacy")]
[InlineData("corrupt current")]
[InlineData("half restored")]
[InlineData("initialization failed")]
public void InitPreemptsUnreadableOrPartiallyRestoredStorage(string scenario)
{
var r = ActiveFleet();
var old = scenario == "huge legacy" ? "[planner]\nJobs=" + new string('x', 1600000) :
scenario == "legacy" ? "[storage]\nVersion=2\nRole=fleet\nId=base\n" : "XFEAMS-FLEET-3\n999999:broken";
if (scenario == "half restored")
{
old = r.Program.Storage; var restoring = new P.FleetController(r.Program);
restoring.BeginRestoreStorage(old); Write(r.Program, "role", restoring); Assert.True(restoring.Restoring);
}
if (scenario == "initialization failed") { Write(r.Program, "role", null); Write(r.Program, "storageFailure", true); }
TestRig.SetBase(r.Program, "Storage", old); r.Program.Main("init", UpdateType.Trigger);
AssertEmpty(Current(r.Program)); Assert.DoesNotContain(old, r.Program.Storage);
Assert.Equal(P.FleetController.ResetStorage(r.Program.Bus.Epoch), r.Program.Storage);
}
[Fact]
public void ResetMarkerIsDurableEvenWhenUserConfigurationIsInvalid()
{
var r = ActiveFleet(); r.Program.Me.CustomData = "[broken";
r.Program.Main("init", UpdateType.Trigger);
Assert.Null(Read<P.RoleLogic>(r.Program, "role")); Assert.Equal("[broken", r.Program.Me.CustomData);
Assert.Equal(0, r.Program.Bus.PendingCount); var marker = r.Program.Storage;
Assert.StartsWith("XFEAMS-RESET-1\n", marker);
var restart = new FleetStateTests.Rig(2, false); restart.Restore(marker); AssertEmpty(restart.Fleet);
r.Program.Me.CustomData = Configuration; r.Program.Main("init", UpdateType.Trigger); AssertEmpty(Current(r.Program));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void LegacyIniIsNeverMigratedOrReplacedDuringOrdinaryRestore(bool large)
{
var r = new FleetStateTests.Rig(0, false);
var text = "[storage]\nVersion=2\nRole=fleet\nId=base\n[planner]\nJobs=0\n" + (large ? ";" + new string('x', 1000000) : "");
TestRig.SetBase(r.Program, "Storage", text);
Assert.Equal(P.L.FleetLegacyResetRequired, Assert.Throws<ArgumentException>(() => r.Fleet.BeginRestoreStorage(text)).Message);
Assert.Equal(text, r.Program.Storage); Assert.False(r.Fleet.Restoring);
}
[Fact]
public void EmptyResetSurvivesImmediateRestartAndCurrentCheckpointRestore()
{
var r = ActiveFleet(); r.Program.Main("init", UpdateType.Trigger); var epoch = r.Program.Bus.Epoch;
var immediate = new FleetStateTests.Rig(2, false); immediate.Restore(r.Program.Storage);
AssertEmpty(immediate.Fleet); Assert.Equal(epoch, immediate.Program.Bus.Epoch);
var checkpoint = immediate.Checkpoint(); Assert.StartsWith("XFEAMS-FLEET-3\n", checkpoint);
var restarted = new FleetStateTests.Rig(2, false); restarted.Restore(checkpoint);
AssertEmpty(restarted.Fleet); Assert.Equal(epoch, restarted.Program.Bus.Epoch);
immediate.Program.Me.CustomData = Configuration; Write(immediate.Program, "role", immediate.Fleet);
immediate.Program.Main("init", UpdateType.Trigger); Assert.True(immediate.Program.Bus.Epoch > epoch);
}
[Fact]
public void NormalCurrentFormatRestoreStillPreservesJobsAndOccupancy()
{
var r = ActiveFleet(); r.Program.Bus.Epoch = 42; var checkpoint = r.Checkpoint();
var restored = new FleetStateTests.Rig(2, false); restored.Restore(checkpoint);
Assert.Equal(42, restored.Program.Bus.Epoch); Assert.Equal(r.Fleet.GetJobs().Select(j => j.Id), restored.Fleet.GetJobs().Select(j => j.Id));
Assert.Equal(2, restored.Fleet.GetTelemetry().Count); Assert.Equal(1, P.Data.ReadLong(restored.Save(), "fleet", "LaneCount"));
Assert.All(restored.Fleet.GetTelemetry(), t => Assert.True(t.ReceivedAt < 0));
}
[Theory]
[InlineData("init miner-0")]
[InlineData("rescan")]
public void TargetedInitializationAndRescanKeepFleetStorage(string command)
{
var r = ActiveFleet(); var storage = r.Program.Storage; var count = r.Fleet.GetJobs().Count;
r.Sent.Clear(); r.Fleet.Command(command);
Assert.Same(r.Fleet, Current(r.Program)); Assert.Equal(storage, r.Program.Storage); Assert.Equal(count, r.Fleet.GetJobs().Count);
Assert.Contains(r.Sent, p => p.Kind == "COMMAND" && P.Data.Text(p.Body, "command", "Text") == "rescan");
}
[Fact]
public void DirectFleetCommandUsesTheSameResetEntryPoint()
{
var r = ActiveFleet(); r.Fleet.Command("init"); AssertEmpty(Current(r.Program));
}
[Theory]
[InlineData("MARK")]
[InlineData("SURVEY_REQUEST")]
[InlineData("RESULT")]
[InlineData("DEPART_REQUEST")]
[InlineData("HELLO")]
[InlineData("TELEMETRY")]
public void OldMessagesCannotRecreateWorkAfterReset(string kind)
{
var r = ActiveFleet(); r.Program.Main("init", UpdateType.Trigger); var reset = Current(r.Program); r.Sent.Clear();
var body = kind == "MARK" ? P.Marking.Parse(r.Program, "mark 6 6 20 GPS:entry:0:0:-200: GPS:inside:0:0:-201:") : new MyIni();
reset.Receive(new P.Packet { Epoch = 0, Kind = kind, From = "miner-0", Source = 5000, Body = body });
AssertEmpty(reset); Assert.Contains(r.Sent, p => p.Kind == "COMMAND" && p.Epoch == r.Program.Bus.Epoch);
}
[Fact]
public void ReturningMinerMustAdoptCurrentGenerationBeforeNewAssignments()
{
var r = ActiveFleet(); r.Program.Main("init", UpdateType.Trigger); var reset = Current(r.Program);
r.SetConnected(0, true);
var old = new P.Packet { Kind = "HELLO", From = "miner-0", Source = 5000, Session = "old", Body = r.Miners[0].ToIni() };
reset.Receive(old); Assert.Empty(reset.GetTelemetry());
reset.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inside:0:0:-201:"); reset.Command("start");
for (int i = 0; i < 20; i++) { r.Program.Now += .1; reset.Tick(.1); }
Assert.All(reset.GetJobs(), j => Assert.Equal("", j.Owner));
var current = new P.Packet { Kind = "HELLO", Epoch = r.Program.Bus.Epoch, From = "miner-0", Source = 5000, Session = "fresh", Body = r.Miners[0].ToIni() };
reset.Receive(current); reset.Receive(old);
Assert.Equal(r.Program.Bus.Epoch, Assert.Single(reset.GetTelemetry()).Epoch);
for (int i = 0; i < 20; i++) { r.Program.Now += .1; reset.Tick(.1); }
Assert.Contains(reset.GetJobs(), j => j.Owner == "miner-0");
}
static P.Packet ResetPacket(P program, long epoch = 100)
{
var body = new MyIni(); body.Set("command", "Text", "return"); body.Set("command", "Reset", true);
return new P.Packet { From = program.Config.BaseId, Epoch = epoch, Kind = "COMMAND", Body = body };
}
[Theory]
[InlineData(P.FlightState.Transit)]
[InlineData(P.FlightState.Survey)]
[InlineData(P.FlightState.DockAlign)]
[InlineData(P.FlightState.DockApproach)]
[InlineData(P.FlightState.Holding)]
public void AirborneResetCancelsOldWorkAndReacquiresItsReturnRoute(P.FlightState state)
{
var r = new MinerStateTests.MinerRig(); r.Task(); r.State(state);
Write(r.Miner, "lane", "old-permit"); Write(r.Miner, "dock", new P.DockFrame());
r.Program.Bus.Send("RESULT", r.Program.Config.BaseId, new MyIni(), true); r.Drill.Set("Enabled", true);
r.Miner.Receive(ResetPacket(r.Program));
Assert.Equal(P.FlightState.Returning, r.StateNow); Assert.Equal("", Read<string>(r.Miner, "lane")); Assert.Null(Read<P.DockFrame>(r.Miner, "dock"));
Assert.True(Read<bool>(r.Miner, "canceling")); Assert.False(Read<bool>(r.Miner, "leaseConfirmed")); Assert.False(r.Drill.Value.Enabled);
Assert.Equal(0, r.Program.Bus.PendingCount); Assert.Equal(100, r.Program.Bus.Epoch);
Assert.Equal(100, P.Data.ReadLong(Save(r.Miner), "miner", "FleetEpoch"));
}
[Fact]
public void BoreResetRetainsTheOriginalExitAxisUntilRetreatIsComplete()
{
var r = new MinerStateTests.MinerRig(); r.Task(12); r.State(P.FlightState.Drilling);
var job = Assert.Single(r.Miner.GetJobs()); var direction = job.Direction;
r.Drill.Set("Enabled", true); r.Miner.Receive(ResetPacket(r.Program));
Assert.Equal(P.FlightState.Retreat, r.StateNow); Assert.Same(job, Assert.Single(r.Miner.GetJobs()));
Assert.Equal(direction, job.Direction); Assert.Equal(12, job.Progress); Assert.True(Read<bool>(r.Miner, "insideBore")); Assert.False(r.Drill.Value.Enabled);
var saved = Save(r.Miner); var restarted = new MinerStateTests.MinerRig(); restarted.Miner.Load(saved);
Assert.Equal(100, restarted.Program.Bus.Epoch); Assert.True(Read<bool>(restarted.Miner, "insideBore"));
Assert.Equal(direction, Assert.Single(restarted.Miner.GetJobs()).Direction);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void ResetDoesNotTakeOverManualControlOrBrokenHardware(bool manual)
{
var r = new MinerStateTests.MinerRig(); r.Task();
r.State(manual ? P.FlightState.Manual : P.FlightState.Fault);
if (manual) r.Controller.Set("IsUnderControl", true); else Write(r.Miner, "healthy", false);
r.Miner.Receive(ResetPacket(r.Program));
r.Miner.Receive(ResetPacket(r.Program));
Assert.Equal(manual ? P.FlightState.Manual : P.FlightState.Fault, r.StateNow);
Assert.True(Read<bool>(r.Miner, "canceling")); Assert.False(Read<bool>(r.Miner, "running"));
}
[Fact]
public void DockedMinerDiscardsCancelledSerialWatermarkButRejectsEveryOldGenerationPacket()
{
var r = new MinerStateTests.MinerRig(); r.PrepareDeparture("old"); Write(r.Miner, "cancelledThrough", 500L);
r.Miner.Receive(ResetPacket(r.Program)); Assert.Empty(r.Miner.GetJobs()); Assert.False(Read<bool>(r.Miner, "canceling"));
Assert.Equal(-1, Read<long>(r.Miner, "cancelledThrough")); Assert.True(r.Connector.Value.Status == MyShipConnectorStatus.Connected);
var job = new P.Job { Id = r.Program.Config.BaseId + "-job-1", Owner = r.Program.Config.Id, Depth = 10, Direction = Vector3D.Forward, Up = Vector3D.Up };
var packet = new P.Packet { Kind = "JOB", From = r.Program.Config.BaseId, Body = job.ToIni() };
r.Miner.Receive(packet); Assert.Empty(r.Miner.GetJobs());
packet.Epoch = 100; r.Miner.Receive(packet); Assert.Equal(job.Id, Assert.Single(r.Miner.GetJobs()).Id);
var staleCancel = ResetPacket(r.Program, 99); staleCancel.Body.Set("command", "Text", "cancel-through 999");
staleCancel.Body.Set("command", "Reset", false);
r.Miner.Receive(staleCancel); Assert.Equal(job.Id, Assert.Single(r.Miner.GetJobs()).Id);
r.Miner.Receive(new P.Packet { From = r.Program.Config.BaseId, Epoch = 99, Kind = "DOCK_GRANT", Body = new P.DockFrame { Token = "old", BaseId = r.Program.Config.BaseId }.ToIni() });
Assert.Equal("", Read<string>(r.Miner, "lane"));
}
sealed class Transport
{
readonly P program;
public int Registrations, Disabled;
public readonly Queue<MyIGCMessage> Incoming = new Queue<MyIGCMessage>();
public readonly List<P.Packet> Sent = new List<P.Packet>();
public Transport(P p)
{
program = p;
var listener = new Stub<IMyUnicastListener>().Method("get_HasPendingMessage", a => Incoming.Count > 0).Method("AcceptMessage", a => Incoming.Dequeue());
TestRig.SetBase(p, "IGC", new Stub<IMyIntergridCommunicationSystem>().Set("UnicastListener", listener.Value)
.Method("RegisterBroadcastListener", a => { Registrations++; return new Stub<IMyBroadcastListener>().Value; })
.Method("DisableBroadcastListener", a => { Disabled++; return null; })
.Method("SendBroadcastMessage", a => Capture((string)a[1]))
.Method("SendUnicastMessage", a => { Capture((string)a[2]); return true; }).Value);
p.Bus = new P.Wire(p);
}
object Capture(string text) { P.Packet packet; Assert.True(P.Packet.TryDecode(text, program.Config.FleetId, out packet)); Sent.Add(packet); return null; }
public void Add(P.Packet packet, long source = 5000)
{ Incoming.Enqueue(new MyIGCMessage(packet.Encode(program.Config.FleetId), "AMS2/" + program.Config.FleetId, source)); }
}
[Fact]
public void RepeatedInitClearsRetriesWithoutRegisteringDuplicateListeners()
{
var r = ActiveFleet(); var transport = new Transport(r.Program);
for (int n = 0; n < 4; n++)
{
r.Program.Bus.Send("JOB", "miner-0", new MyIni(), true); r.Program.Main("init", UpdateType.Trigger);
Assert.Equal(0, r.Program.Bus.PendingCount); AssertEmpty(Current(r.Program));
}
Assert.Equal(1, transport.Registrations); Assert.Equal(0, transport.Disabled);
r.Program.Me.CustomData = Configuration.Replace("FleetId=mining", "FleetId=another"); r.Program.Main("init", UpdateType.Trigger);
Assert.Equal(2, transport.Registrations); Assert.Equal(1, transport.Disabled);
}
[Fact]
public void OldGenerationCannotPoisonTheWireSessionOfAnAlreadyReconnectedMiner()
{
var r = ActiveFleet(); var transport = new Transport(r.Program); r.Program.Main("init", UpdateType.Trigger); var fleet = Current(r.Program);
var current = new P.Packet { Kind = "HELLO", From = "miner-0", Epoch = r.Program.Bus.Epoch, Seq = 1, Session = "fresh", Id = "fresh1", Body = r.Miners[0].ToIni() };
transport.Add(current); r.Program.Bus.Drain(fleet.Receive);
var old = new P.Packet { Kind = "HELLO", From = "miner-0", Epoch = 0, Seq = 999, Session = "old", Id = "old999", Body = r.Miners[0].ToIni() };
transport.Add(old); r.Program.Bus.Drain(fleet.Receive);
r.Miners[0].Battery = .7; current.Body = r.Miners[0].ToIni(); current.Seq++; current.Id = "fresh2";
transport.Add(current); r.Program.Bus.Drain(fleet.Receive);
var actual = Assert.Single(fleet.GetTelemetry()); Assert.Equal(.7, actual.Battery, 8); Assert.Equal(current.Epoch, actual.Epoch);
}
[Fact]
public void MarkerDropsPendingOldMarksWhenBaseGenerationChangesAndPersistsIt()
{
var p = TestRig.Program(); TestRig.SetBase(p, "GridTerminalSystem", new Stub<IMyGridTerminalSystem>().Value);
var transport = new Transport(p); var marker = new P.MarkerController(p);
marker.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inside:0:0:-201:"); Assert.Equal(1, p.Bus.PendingCount);
marker.Receive(ResetPacket(p)); Assert.Equal(0, p.Bus.PendingCount); Assert.Equal(100, p.Bus.Epoch);
var saved = Save(marker); p.Bus.Epoch = 0; marker.Load(saved); Assert.Equal(100, p.Bus.Epoch);
marker.Receive(ResetPacket(p, 99)); Assert.Equal(100, p.Bus.Epoch);
marker.Command("survey 300"); Assert.Equal(100, transport.Sent.Last().Epoch);
}
[Theory]
[InlineData("-1")]
[InlineData("3155378976000000000")]
[InlineData("NaN")]
public void InvalidResetGenerationIsRejectedByStorageAndProtocol(string value)
{
var r = new FleetStateTests.Rig(0, false);
Assert.Throws<ArgumentException>(() => r.Fleet.BeginRestoreStorage("XFEAMS-RESET-1\n" + value));
var packet = new P.Packet { Kind = "BASE", From = "base", Session = "s", Seq = 1 };
var ini = new MyIni(); ini.TryParse(packet.Encode("mining")); ini.Set("wire", "Epoch", value);
P.Packet decoded; Assert.False(P.Packet.TryDecode(ini.ToString(), "mining", out decoded));
}
}
}