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(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(Read(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(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(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(() => 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(r.Miner, "lane")); Assert.Null(Read(r.Miner, "dock")); Assert.True(Read(r.Miner, "canceling")); Assert.False(Read(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(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(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(r.Miner, "canceling")); Assert.False(Read(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(r.Miner, "canceling")); Assert.Equal(-1, Read(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(r.Miner, "lane")); } sealed class Transport { readonly P program; public int Registrations, Disabled; public readonly Queue Incoming = new Queue(); public readonly List Sent = new List(); public Transport(P p) { program = p; var listener = new Stub().Method("get_HasPendingMessage", a => Incoming.Count > 0).Method("AcceptMessage", a => Incoming.Dequeue()); TestRig.SetBase(p, "IGC", new Stub().Set("UnicastListener", listener.Value) .Method("RegisterBroadcastListener", a => { Registrations++; return new Stub().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().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(() => 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)); } } }