using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Sandbox.ModAPI.Ingame; using VRage.Game.ModAPI.Ingame; using VRage.Game.ModAPI.Ingame.Utilities; using VRageMath; using Xunit; using P=AutoMiningScript.Program; namespace AutoMiningScript.Tests { public class FleetStateTests { internal sealed class Rig { public readonly P Program; public readonly P.FleetController Fleet; public readonly List Sent=new List(); public readonly List Miners=new List(); readonly List> portStubs=new List>(),remoteStubs=new List>(); long sequence; public Rig(int count,bool connected,string config="") { Program=TestRig.Program();Program.Config=new P.Settings("[System]\nId=base\nFleetId=mining\nBaseId=base\n"+config,"fleet","base"); var receiver=new Stub();var unicast=new Stub(); var igc=new Stub().Set("UnicastListener",unicast.Value).Method("RegisterBroadcastListener",a=>receiver.Value) .Method("SendBroadcastMessage",a=>{Capture(a[1] as string);return null;}).Method("SendUnicastMessage",a=>{Capture(a[2] as string);return true;}); TestRig.SetBase(Program,"IGC",igc.Value);Program.Bus=new P.Wire(Program); var controls=new List();var ports=new List(); controls.Add(new Stub().Set("CubeGrid",Program.Me.CubeGrid).Set("IsFunctional",true).Set("WorldMatrix",MatrixD.Identity).Set("CenterOfMass",Vector3D.Zero) .Method("GetPosition",a=>Vector3D.Zero).Method("GetShipVelocities",a=>new MyShipVelocities(Vector3D.Zero,Vector3D.Zero)).Value); for(int n=0;n().Set("EntityId",1000L+n); var remote=new Stub().Set("CubeGrid",grid.Value).Set("EntityId",2000L+n).Set("Status",connected?MyShipConnectorStatus.Connected:MyShipConnectorStatus.Unconnected); var port=new Stub().Set("CubeGrid",Program.Me.CubeGrid).Set("EntityId",100L+n).Set("IsFunctional",true).Set("WorldMatrix",matrix) .Set("Status",connected?MyShipConnectorStatus.Connected:MyShipConnectorStatus.Unconnected).Set("OtherConnector",connected?remote.Value:null).Method("GetPosition",a=>position); ports.Add(port.Value);portStubs.Add(port);remoteStubs.Add(remote); Miners.Add(new P.Telemetry {Id="miner-"+n,State=P.FlightState.Servicing,Position=position,Connected=connected,ConnectorId=2000L+n,PeerConnectorId=100L+n,Battery=1,Hydrogen=1,Radius=3,StoredMWh=1,CapacityMWh=1}); } var group=new Stub().Method("GetBlocksOfType",a=>Fill(a,ports)); var terminal=new Stub().Method("GetBlockGroupWithName",a=>(string)a[0]=="AMS Docks"?group.Value:null).Method("GetBlocksOfType",a=>Fill(a,controls)); TestRig.SetBase(Program,"GridTerminalSystem",terminal.Value);Fleet=new P.FleetController(Program); } static object Fill(object[] args,List blocks) { var list=(IList)args[0];var itemType=list.GetType().GetGenericArguments()[0];Delegate filter=args.Length>1?args[1] as Delegate:null; foreach(var block in blocks)if(itemType.IsInstanceOfType(block)&&(filter==null||(bool)filter.DynamicInvoke(block)))list.Add(block);return null; } void Capture(string value) {P.Packet packet;if(P.Packet.TryDecode(value,"mining",out packet))Sent.Add(packet);} public void Receive(string kind,P.Telemetry telemetry,long? source=null) { int index=Miners.IndexOf(telemetry);Fleet.Receive(new P.Packet {Kind=kind,From=telemetry.Id,Source=source??index+5000,Id="packet-"+(++sequence),Session="miner-session",Seq=sequence,Body=telemetry.ToIni()}); } public void RegisterAll() {foreach(var miner in Miners)Receive("HELLO",miner);} public void SetConnected(int index,bool connected) { portStubs[index].Set("Status",connected?MyShipConnectorStatus.Connected:MyShipConnectorStatus.Unconnected).Set("OtherConnector",connected?remoteStubs[index].Value:null); remoteStubs[index].Set("Status",connected?MyShipConnectorStatus.Connected:MyShipConnectorStatus.Unconnected);Miners[index].Connected=connected; } public void SetPort(int index,Vector3D position,Vector3D forward) { portStubs[index].Set("WorldMatrix",MatrixD.CreateWorld(position,forward,DataUp(forward))).Method("GetPosition",a=>position); } static Vector3D DataUp(Vector3D forward) {return P.Data.Perpendicular(forward,Vector3D.Up);} public void Tick(int steps) {for(int n=0;nj.Owner.Length>0).ToList();Assert.Equal(16,owned.Count);Assert.Equal(16,owned.Select(j=>j.Owner).Distinct().Count()); for(int i=0;i=8); var unknown=new P.Telemetry {Id="miner-17",Battery=1,Hydrogen=1};rig.Receive("HELLO",unknown);Assert.Equal(16,rig.Fleet.GetTelemetry().Count); } [Fact] public void IndependentPortPrioritizesUrgentReturnAndRetainsTheOtherUnknownOccupancy() { var rig=new Rig(2,false);rig.RegisterAll();rig.Tick(30);rig.Miners[0].Battery=.9;rig.Miners[1].Battery=.1; rig.Receive("DOCK_REQUEST",rig.Miners[0]);rig.Receive("DOCK_REQUEST",rig.Miners[1]);rig.Tick(12); P.Packet granted=Assert.Single(rig.Sent,p=>p.Kind=="DOCK_GRANT");Assert.Equal("miner-1",granted.Target); rig.Sent.Clear();rig.Tick(600);rig.Receive("DOCK_REQUEST",rig.Miners[0]);rig.Tick(12); var second=rig.Sent.First(p=>p.Kind=="DOCK_GRANT");Assert.Equal("miner-0",second.Target); Assert.NotEqual(P.DockFrame.FromIni(granted.Body).ConnectorId,P.DockFrame.FromIni(second.Body).ConnectorId); Assert.Equal(2,P.Data.ReadLong(rig.Save(),"fleet","LaneCount"));Assert.Contains("miner-1",rig.Fleet.Diagnostics); } [Fact] public void RestartRetainsOccupancyButNeedsFreshRequestAndNewSessionToken() { var before=new Rig(1,false);before.RegisterAll();before.Tick(30);before.Receive("DOCK_REQUEST",before.Miners[0]);before.Tick(12); string oldToken=P.DockFrame.FromIni(before.Sent.First(p=>p.Kind=="DOCK_GRANT").Body).Token;var saved=new MyIni();before.Fleet.Save(saved); var after=new Rig(1,false);after.Fleet.Load(saved);after.RegisterAll();after.Tick(30); Assert.DoesNotContain(after.Sent,p=>p.Kind=="DOCK_GRANT");Assert.All(after.Sent.Where(p=>p.Kind=="DOCKPOSE"),p=>Assert.Equal("",P.DockFrame.FromIni(p.Body).Token)); after.Receive("DOCK_REQUEST",after.Miners[0]);after.Tick(12); string renewed=P.DockFrame.FromIni(after.Sent.First(p=>p.Kind=="DOCK_GRANT").Body).Token; Assert.NotEqual(oldToken,renewed);Assert.Contains(after.Program.Bus.Session,renewed); } [Fact] public void MarkReceiptsRemainIdempotentAcrossFleetReload() { var before=new Rig(0,false);var body=new MyIni();body.Set("mark","Entry","0,0,0");body.Set("mark","Direction","0,0,-1");body.Set("mark","Up","0,1,0");body.Set("mark","Width",6);body.Set("mark","Height",6);body.Set("mark","Depth",10); var packet=new P.Packet {Kind="MARK",From="marker",Id="marker-session-1",Session="marker-session",Seq=1,Body=body};before.Fleet.Receive(packet);before.Fleet.Receive(packet);Assert.Single(before.Fleet.GetJobs()); var saved=new MyIni();before.Fleet.Save(saved);var after=new Rig(0,false);after.Fleet.Load(saved);after.Fleet.Receive(packet);Assert.Single(after.Fleet.GetJobs()); } [Fact] public void RecordStorageRestoresLargePlannerIncrementallyWithUnknownOwnership() { var before=new Rig(1,true);before.RegisterAll();before.Fleet.Command("survey 2000");before.Fleet.Command("start");before.Tick(40); string storage=null;int frames=0;while(!before.Fleet.CheckpointStorage(out storage)&&frames++<500){}Assert.InRange(frames,1,499);Assert.StartsWith("XFEAMS-FLEET-3\n",storage); var after=new Rig(1,true);Assert.True(after.Fleet.BeginRestoreStorage(storage));after.Fleet.RestoreStorageStep();Assert.True(after.Fleet.Restoring); frames=0;while(after.Fleet.Restoring&&frames++<500)after.Fleet.RestoreStorageStep();Assert.False(after.Fleet.Restoring); Assert.Equal(before.Fleet.GetJobs().Count,after.Fleet.GetJobs().Count);Assert.Equal(before.Fleet.GetJobs().Select(j=>j.Owner),after.Fleet.GetJobs().Select(j=>j.Owner));Assert.All(after.Fleet.GetTelemetry(),t=>Assert.True(t.ReceivedAt<0)); } [Fact] public void RecordStorageResumesIncompleteGenerationExactlyOnce() { var before=new Rig(0,false);before.Fleet.Command("survey 2000");Assert.Equal(8,before.Fleet.GetJobs().Count); string storage=null;int frames=0;while(!before.Fleet.CheckpointStorage(out storage)&&frames++<500){} var after=new Rig(0,false);after.Fleet.BeginRestoreStorage(storage);while(after.Fleet.Restoring)after.Fleet.RestoreStorageStep();before.Tick(40);after.Tick(40); Assert.Equal(294,after.Fleet.GetJobs().Count);Assert.Equal(before.Fleet.GetJobs().Select(j=>j.Id),after.Fleet.GetJobs().Select(j=>j.Id));Assert.Equal(before.Fleet.GetJobs().Select(j=>j.Entry),after.Fleet.GetJobs().Select(j=>j.Entry)); } [Fact] public void RecordStorageRejectsTruncationAndIdentityMismatch() { var before=new Rig(0,false);before.Fleet.Command("survey 2000");string storage=null;int frames=0;while(!before.Fleet.CheckpointStorage(out storage)&&frames++<500){} var after=new Rig(0,false);Assert.True(after.Fleet.BeginRestoreStorage(storage.Substring(0,storage.Length-1)));Assert.Throws(()=>{while(after.Fleet.Restoring)after.Fleet.RestoreStorageStep();}); after=new Rig(0,false);after.Program.Config=new P.Settings("[System]\nId=other\nFleetId=mining\n","fleet","other");after.Fleet.BeginRestoreStorage(storage);Assert.Throws(()=>after.Fleet.RestoreStorageStep()); } [Fact] public void FleetCheckpointIncludesFreshOwnershipAndCanRestoreFleet() { var rig=new Rig(1,true);rig.RegisterAll();rig.Fleet.Command("mark 300 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start"); var checkpoint=new MyIni();Assert.False(rig.Fleet.Checkpoint(checkpoint));rig.Tick(2);int frames=0;while(!rig.Fleet.Checkpoint(checkpoint)&&frames++<100){} Assert.True(frames<100);var restored=new Rig(1,true);restored.Fleet.Load(checkpoint);Assert.Equal(rig.Fleet.GetJobs().Count,restored.Fleet.GetJobs().Count);Assert.Single(restored.Fleet.GetJobs(),j=>j.Owner=="miner-0"); } [Fact] public void ClearTasksImmediatelyCommitsEmptyPlannerAndIndependentAirborneGeometry() { var rig=new Rig(1,true);rig.RegisterAll();rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start");rig.Tick(2); P.Job job=Assert.Single(rig.Fleet.GetJobs());Assert.Equal("miner-0",job.Owner); rig.SetConnected(0,false);rig.Miners[0].JobId=job.Id;rig.Miners[0].State=P.FlightState.Drilling;rig.Miners[0].Position=job.Entry+job.Direction*5;rig.Receive("TELEMETRY",rig.Miners[0]); rig.Sent.Clear();rig.Fleet.Command("clear-tasks");Assert.Empty(rig.Fleet.GetJobs());Assert.Equal(1,rig.Fleet.CancellationCount);Assert.Single(rig.Fleet.GetTelemetry()); Assert.Contains(rig.Sent,p=>p.Kind=="COMMAND"&&p.Body.Get("command","Text").ToString().StartsWith("cancel-through ")); Assert.StartsWith("XFEAMS-FLEET-3\n",rig.Program.Storage); var after=new Rig(1,false);after.Fleet.BeginRestoreStorage(rig.Program.Storage);int frames=0;while(after.Fleet.Restoring&&frames++<200)after.Fleet.RestoreStorageStep(); Assert.False(after.Fleet.Restoring);Assert.Empty(after.Fleet.GetJobs());Assert.Equal(1,after.Fleet.CancellationCount);Assert.Single(after.Fleet.GetTelemetry()); var saved=new MyIni();after.Fleet.Save(saved);Assert.Equal(0,P.Data.ReadLong(saved,"planner","Jobs"));Assert.Equal(1,P.Data.ReadLong(saved,"fleet","ClearanceCount")); var geometry=new MyIni();Assert.True(geometry.TryParse(saved.Get("fleet","ClearanceData0").ToString()));P.Job held=P.Job.FromIni(geometry);Assert.Equal(job.Entry,held.Entry);Assert.Equal(job.Id,held.Id);Assert.Equal(job.Direction,held.Direction); after.RegisterAll();Assert.Contains(after.Sent,p=>p.Kind=="COMMAND"&&p.Body.Get("command","Text").ToString().StartsWith("cancel-through ")); } [Fact] public void ClearRetainsPortLaneAndRequiresAppliedWatermarkPlusPhysicalDock() { var rig=new Rig(1,false);rig.RegisterAll();rig.Tick(30);rig.Receive("DOCK_REQUEST",rig.Miners[0]);rig.Tick(12); rig.Fleet.Command("clear-all");var state=new MyIni();rig.Fleet.Save(state);Assert.Equal("miner-0",state.Get("fleet","LaneOwner").ToString());long watermark=P.Data.ReadLong(state,"fleet","CancelledThrough"); rig.Miners[0].JobId="";rig.Receive("TELEMETRY",rig.Miners[0]);Assert.Equal(1,rig.Fleet.CancellationCount); rig.Miners[0].CancelledThrough=watermark;rig.Receive("TELEMETRY",rig.Miners[0]);Assert.Equal(1,rig.Fleet.CancellationCount); rig.SetConnected(0,true);rig.Receive("TELEMETRY",rig.Miners[0]);Assert.Equal(0,rig.Fleet.CancellationCount);state.Clear();rig.Fleet.Save(state);Assert.Equal("",state.Get("fleet","LaneOwner").ToString()); } [Fact] public void CancellationRetriesWithNewSequenceAndBlocksNewDispatchUntilConfirmed() { var rig=new Rig(1,true);rig.RegisterAll();rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start");rig.Tick(2);string oldId=rig.Fleet.GetJobs()[0].Id; rig.Sent.Clear();rig.Fleet.Command("clear-tasks");var cleared=new MyIni();rig.Fleet.Save(cleared);long watermark=P.Data.ReadLong(cleared,"fleet","CancelledThrough"); rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-300: GPS:inward:0:0:-301:");rig.Fleet.Command("start");rig.Tick(25);rig.Program.Bus.Tick(); Assert.DoesNotContain(rig.Sent,p=>p.Kind=="JOB");var cancels=rig.Sent.Where(p=>p.Kind=="COMMAND"&&p.Body.Get("command","Text").ToString().StartsWith("cancel-through ")).ToList();Assert.True(cancels.Select(p=>p.Seq).Distinct().Count()>=2); rig.Miners[0].CancelledThrough=watermark;rig.Miners[0].JobId="";rig.Receive("TELEMETRY",rig.Miners[0]);Assert.Equal(0,rig.Fleet.CancellationCount);rig.Tick(4); var assigned=Assert.Single(rig.Fleet.GetJobs());Assert.NotEqual(oldId,assigned.Id);Assert.Equal("miner-0",assigned.Owner); } [Fact] public void ClearKeepsExistingCompletedHoldAndNewOwnedHoleForTheSameMiner() { var rig=new Rig(1,true);rig.RegisterAll();rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start");rig.Tick(2);P.Job first=rig.Fleet.GetJobs()[0]; rig.Fleet.Receive(new P.Packet {Kind="RESULT",From="miner-0",Source=5000,Body=new P.JobReport {JobId=first.Id,Outcome="Complete",Progress=20}.ToIni()}); rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-400: GPS:inward:0:0:-401:");P.Job second=rig.Fleet.GetJobs().Last(); var planner=(P.TaskPlanner)typeof(P.FleetController).GetField("planner",System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic).GetValue(rig.Fleet);planner.SetOwner(second,"miner-0"); rig.SetConnected(0,false);rig.Miners[0].JobId=second.Id;rig.Miners[0].Position=second.Entry;rig.Receive("TELEMETRY",rig.Miners[0]);rig.Fleet.Command("clear-tasks"); var saved=new MyIni();rig.Fleet.Save(saved);Assert.Equal(2,P.Data.ReadLong(saved,"fleet","ClearanceCount"));Assert.Empty(rig.Fleet.GetJobs()); var restored=new Rig(1,false);restored.Fleet.BeginRestoreStorage(rig.Program.Storage);while(restored.Fleet.Restoring)restored.Fleet.RestoreStorageStep();saved.Clear();restored.Fleet.Save(saved);Assert.Equal(2,P.Data.ReadLong(saved,"fleet","ClearanceCount")); } [Fact] public void CancelledDepartureCanTurnItsReservedLaneIntoReturnWithoutReleasingIt() { var rig=new Rig(1,true);rig.RegisterAll();rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start");rig.Tick(30); rig.Miners[0].JobId=rig.Fleet.GetJobs()[0].Id;rig.Receive("DEPART_REQUEST",rig.Miners[0]);rig.Tick(12); string outbound=P.DockFrame.FromIni(rig.Sent.First(p=>p.Kind=="DEPART_GRANT").Body).Token; rig.SetConnected(0,false);rig.Miners[0].Position=new Vector3D(0,0,-20);rig.Miners[0].State=P.FlightState.Departing;rig.Receive("TELEMETRY",rig.Miners[0]);rig.Fleet.Command("clear-tasks");rig.Receive("DOCK_REQUEST",rig.Miners[0]); P.DockFrame inbound=P.DockFrame.FromIni(rig.Sent.Last(p=>p.Kind=="DOCK_GRANT").Body);Assert.Equal("miner-0",inbound.Owner);Assert.NotEqual(outbound,inbound.Token);Assert.Equal(1,rig.Fleet.CancellationCount); } static string BeginDeparture(Rig rig) { rig.RegisterAll();rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start");rig.Tick(30); rig.Miners[0].JobId=rig.Fleet.GetJobs()[0].Id;rig.Receive("DEPART_REQUEST",rig.Miners[0]);rig.Tick(12); string token=P.DockFrame.FromIni(rig.Sent.First(p=>p.Kind=="DEPART_GRANT").Body).Token; rig.SetConnected(0,false);rig.Miners[0].State=P.FlightState.Departing;return token; } [Fact] public void OutboundGrantSurvivesClearanceBoundaryUntilMinerCompletesDeparture() { var rig=new Rig(1,true);string token=BeginDeparture(rig); // Fleet clearance is 63 m, while the actual outbound target is 65 m. foreach(double distance in new[]{63.2,65.0}) { rig.Miners[0].Position=new Vector3D(0,0,-distance);rig.Receive("TELEMETRY",rig.Miners[0]);rig.Sent.Clear();rig.Tick(12); Assert.Equal("miner-0",rig.Save().Get("fleet","LaneOwner").ToString()); Assert.Equal(token,rig.Save().Get("fleet","LaneToken").ToString()); Assert.Contains(rig.Sent,p=>p.Kind=="DOCKPOSE"); Assert.All(rig.Sent.Where(p=>p.Kind=="DOCKPOSE"),p=>Assert.Equal(token,P.DockFrame.FromIni(p.Body).Token)); } rig.Miners[0].State=P.FlightState.Transit;rig.Receive("TELEMETRY",rig.Miners[0]);rig.Tick(12); Assert.Equal("",rig.Save().Get("fleet","LaneOwner").ToString()); } [Theory] [InlineData(P.FlightState.Survey)] [InlineData(P.FlightState.Align)] [InlineData(P.FlightState.Drilling)] [InlineData(P.FlightState.Retreat)] public void PostDeparturePhaseCanReconcileAReleaseWithoutObservingShortTransit(P.FlightState phase) { var rig=new Rig(1,true);BeginDeparture(rig);rig.Miners[0].Position=new Vector3D(0,0,-100);rig.Miners[0].State=phase; rig.Receive("TELEMETRY",rig.Miners[0]);rig.Tick(12);Assert.Equal("",rig.Save().Get("fleet","LaneOwner").ToString()); } [Theory] [InlineData(P.FlightState.Departing)] [InlineData(P.FlightState.DockRetreat)] [InlineData(P.FlightState.Returning)] [InlineData(P.FlightState.Paused)] [InlineData(P.FlightState.Manual)] public void InterruptedDepartureRetainsItsCorridorEvenBeyondClearance(P.FlightState phase) { var rig=new Rig(1,true);string token=BeginDeparture(rig);rig.Miners[0].Position=new Vector3D(0,0,-100);rig.Miners[0].State=phase; rig.Receive("TELEMETRY",rig.Miners[0]);rig.Tick(12);Assert.Equal(token,rig.Save().Get("fleet","LaneToken").ToString()); rig.Tick(600);Assert.Equal("miner-0",rig.Save().Get("fleet","LaneOwner").ToString()); } [Fact] public void PostDeparturePhaseAlsoRequiresFreshTelemetryAndPhysicalClearance() { var rig=new Rig(1,true);string token=BeginDeparture(rig);rig.Miners[0].State=P.FlightState.Transit; rig.Miners[0].Position=new Vector3D(0,0,-62);rig.Receive("TELEMETRY",rig.Miners[0]);rig.Tick(12); Assert.Equal(token,rig.Save().Get("fleet","LaneToken").ToString()); rig.Miners[0].Position=new Vector3D(0,0,-100);rig.Miners[0].Connected=true;rig.Receive("TELEMETRY",rig.Miners[0]);rig.Tick(12); Assert.Equal(token,rig.Save().Get("fleet","LaneToken").ToString()); rig.Miners[0].Connected=false;rig.Receive("TELEMETRY",rig.Miners[0]);rig.Program.Now+=4;rig.Tick(12); Assert.Equal(token,rig.Save().Get("fleet","LaneToken").ToString()); rig.Receive("TELEMETRY",rig.Miners[0]);rig.Tick(12);Assert.Equal("",rig.Save().Get("fleet","LaneOwner").ToString()); } [Fact] public void ExplicitOutboundReleaseStillRequiresItsTokenAndFreshSafeDistance() { var rig=new Rig(1,true);string token=BeginDeparture(rig);var body=new MyIni();body.Set("lease","Token",token); var release=new P.Packet {Kind="LANE_RELEASE",From="miner-0",Source=5000,Body=body}; rig.Miners[0].Position=new Vector3D(0,0,-20);rig.Receive("TELEMETRY",rig.Miners[0]);rig.Fleet.Receive(release); Assert.Equal(token,rig.Save().Get("fleet","LaneToken").ToString()); rig.Miners[0].Position=new Vector3D(0,0,-65);rig.Receive("TELEMETRY",rig.Miners[0]);body.Set("lease","Token","obsolete");rig.Fleet.Receive(release); Assert.Equal(token,rig.Save().Get("fleet","LaneToken").ToString()); body.Set("lease","Token",token);rig.Fleet.Receive(release);Assert.Equal("",rig.Save().Get("fleet","LaneOwner").ToString()); } [Fact] public void ConfirmedMinerCanTakeNewWorkAndFreeItsPortWhileAnotherAwaitsCancellation() { var rig=new Rig(2,true);rig.SetConnected(1,false);rig.RegisterAll();rig.Fleet.Command("clear-tasks"); var saved=new MyIni();rig.Fleet.Save(saved);long watermark=P.Data.ReadLong(saved,"fleet","CancelledThrough"); rig.Miners[0].CancelledThrough=watermark;rig.Miners[0].JobId="";rig.Receive("TELEMETRY",rig.Miners[0]);Assert.Equal(1,rig.Fleet.CancellationCount); rig.Sent.Clear();rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start");rig.Tick(30); Assert.Equal("miner-0",Assert.Single(rig.Fleet.GetJobs()).Owner); Assert.DoesNotContain(rig.Sent,p=>p.Kind=="COMMAND"&&p.Target=="miner-1"&&p.Body.Get("command","Text").ToString()=="resume"); rig.Miners[0].JobId=rig.Fleet.GetJobs()[0].Id;rig.Receive("DEPART_REQUEST",rig.Miners[0]);rig.Tick(12); Assert.Contains(rig.Sent,p=>p.Kind=="DEPART_GRANT"&&p.Target=="miner-0");Assert.Equal(1,rig.Fleet.CancellationCount); } [Fact] public void ClearPreservesUnregisteredLegacyOwnerGeometryWithoutExpandingRegisteredLedger() { var rig=new Rig(16,false);rig.RegisterAll();rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:"); var planner=(P.TaskPlanner)typeof(P.FleetController).GetField("planner",System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic).GetValue(rig.Fleet);planner.SetOwner(rig.Fleet.GetJobs()[0],"legacy-offline"); rig.Fleet.Command("clear-tasks");Assert.Equal(16,rig.Fleet.CancellationCount); var restored=new Rig(16,false);restored.Fleet.BeginRestoreStorage(rig.Program.Storage);while(restored.Fleet.Restoring)restored.Fleet.RestoreStorageStep();var saved=new MyIni();restored.Fleet.Save(saved); Assert.Equal(16,restored.Fleet.CancellationCount);Assert.Equal(1,P.Data.ReadLong(saved,"fleet","ClearanceCount"));Assert.Equal("legacy-offline",saved.Get("fleet","ClearanceOwner0").ToString()); } [Theory] [InlineData(5)] [InlineData(12)] public void ClearOfflineUsesStrictConfiguredAgeAndLeavesOnlineTelemetryUntouched(int seconds) { var rig=new Rig(2,false,"[Display]\nOfflineSeconds="+seconds+"\n");rig.RegisterAll();rig.Program.Now=seconds; rig.Fleet.Command("clear-offline");Assert.Equal(2,rig.Fleet.GetTelemetry().Count); rig.Receive("TELEMETRY",rig.Miners[1]);rig.Program.Now=seconds+.01;rig.Fleet.Command("remove-offline"); var online=Assert.Single(rig.Fleet.GetTelemetry());Assert.Equal("miner-1",online.Id);Assert.Equal(seconds,online.ReceivedAt); rig.Receive("HELLO",rig.Miners[0]);Assert.Equal(2,rig.Fleet.GetTelemetry().Count); } [Fact] public void ClearOfflineJournalSurvivesImmediateRestartAndDoesNotRestoreDeletedRegistration() { var before=new Rig(2,false);before.RegisterAll();string baseline=before.Checkpoint();before.Program.Now=6;before.Receive("TELEMETRY",before.Miners[1]); before.Fleet.Command("clear-offline");Assert.StartsWith(baseline,before.Program.Storage);Assert.Contains("RemoveMiners",before.Program.Storage); var after=new Rig(2,false);after.Restore(before.Program.Storage);Assert.Equal("miner-1",Assert.Single(after.Fleet.GetTelemetry()).Id); Assert.Equal(2,P.Data.ReadLong(after.Save(),"fleet","NextWaitingSlot")); after.Receive("HELLO",after.Miners[0]);Assert.Equal(2,after.Fleet.GetTelemetry().Count);Assert.Equal(2,after.Fleet.GetTelemetry().Single(t=>t.Id=="miner-0").WaitingSlot); } [Fact] public void RemovalInvalidatesPartialCheckpointWithoutDroppingOnlineJobOrMovingItsWaitPoint() { var rig=new Rig(2,true);rig.RegisterAll();rig.Fleet.Command("survey 2000");rig.Fleet.Command("start");rig.Tick(30);rig.Checkpoint(); P.Job live=rig.Planner.OwnedJobs["miner-1"];int slot=rig.Fleet.GetTelemetry().Single(t=>t.Id=="miner-1").WaitingSlot; string partial;Assert.False(rig.Fleet.CheckpointStorage(out partial));rig.Program.Now=10;rig.Receive("TELEMETRY",rig.Miners[1]);rig.Fleet.Command("clear-offline"); Assert.Same(live,rig.Planner.OwnedJobs["miner-1"]);Assert.Equal(slot,Assert.Single(rig.Fleet.GetTelemetry()).WaitingSlot); var after=new Rig(2,false);after.Restore(rig.Checkpoint());Assert.Equal("miner-1",Assert.Single(after.Fleet.GetTelemetry()).Id);Assert.Equal(live.Id,after.Planner.OwnedJobs["miner-1"].Id); } [Fact] public void OfflineRemovalSnapshotsNewUncheckpointedGeometryLaneAndSerial() { var before=new Rig(1,true);before.RegisterAll();before.Checkpoint();before.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");before.Fleet.Command("start");before.Tick(30); P.Job owned=Assert.Single(before.Fleet.GetJobs());before.Miners[0].JobId=owned.Id;before.Receive("DEPART_REQUEST",before.Miners[0]);before.Tick(12); Assert.Contains(before.Sent,p=>p.Kind=="DEPART_GRANT");before.SetConnected(0,false);before.Miners[0].Position=new Vector3D(0,0,-20);before.Receive("TELEMETRY",before.Miners[0]); long serial=before.Planner.TaskSerial;before.Program.Now+=6;before.Fleet.Command("clear-offline");Assert.Empty(before.Fleet.GetTelemetry());Assert.Equal("miner-0",owned.Owner); var after=new Rig(1,false);after.Restore(before.Program.Storage);var saved=after.Save();Assert.Empty(after.Fleet.GetTelemetry());Assert.Empty(after.Fleet.GetJobs()); Assert.Equal("miner-0",saved.Get("fleet","LaneOwner").ToString());Assert.Equal("",saved.Get("fleet","LaneToken").ToString());Assert.Equal(1,P.Data.ReadLong(saved,"fleet","ClearanceCount")); var body=new MyIni();Assert.True(body.TryParse(saved.Get("fleet","ClearanceData0").ToString()));Assert.Equal(owned.Entry,P.Job.FromIni(body).Entry);Assert.Equal(serial,after.Planner.TaskSerial); Assert.Equal(1,P.Data.ReadLong(saved,"fleet","ProtectedOwnerCount"));after.Receive("HELLO",after.Miners[0],7000);Assert.Empty(after.Fleet.GetTelemetry()); after.Receive("HELLO",after.Miners[0],5000);Assert.Single(after.Fleet.GetTelemetry());Assert.DoesNotContain(after.Sent,p=>p.Kind=="DEPART_GRANT"||p.Kind=="DOCK_GRANT"); } [Fact] public void SixteenDeletedOwnersDoNotAllowAnotherSixteenOwnershipsOrInvalidStorage() { var rig=new Rig(16,true);rig.RegisterAll();rig.Fleet.Command("mark 300 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start");rig.Tick(45);Assert.Equal(16,rig.Planner.OwnedJobs.Count); rig.Checkpoint();rig.Program.Now+=6;rig.Fleet.Command("clear-offline");Assert.Empty(rig.Fleet.GetTelemetry()); for(int n=0;n<16;n++) {rig.Miners[n].Id="renamed-"+n;rig.Receive("HELLO",rig.Miners[n],7000+n);}rig.Tick(20); Assert.Equal(16,rig.Fleet.GetTelemetry().Count);Assert.Equal(16,rig.Planner.OwnedJobs.Count);Assert.DoesNotContain(rig.Planner.OwnedJobs.Keys,id=>id.StartsWith("renamed-")); var after=new Rig(16,false);after.Restore(rig.Checkpoint());Assert.Equal(16,after.Planner.OwnedJobs.Count);Assert.Equal(16,after.Fleet.GetTelemetry().Count); } [Fact] public void WaitingSlotsIgnoreClientValuesRemainStableAndNeverReuseDeletedSlotsAfterRestart() { var rig=new Rig(3,false);foreach(var t in rig.Miners)t.WaitingSlot=500;rig.RegisterAll();Assert.Equal(new[]{0,1,2},rig.Fleet.GetTelemetry().Select(t=>t.WaitingSlot)); rig.Program.Now=6;rig.Receive("TELEMETRY",rig.Miners[1]);rig.Receive("TELEMETRY",rig.Miners[2]);rig.Fleet.Command("clear-offline"); Assert.Equal(new[]{1,2},rig.Fleet.GetTelemetry().Select(t=>t.WaitingSlot));rig.Miners[0].Id="renamed";rig.Receive("HELLO",rig.Miners[0]);Assert.Equal(3,rig.Fleet.GetTelemetry().Single(t=>t.Id=="renamed").WaitingSlot); var after=new Rig(3,false);after.Restore(rig.Checkpoint());after.Miners[0].Id="next";after.Receive("HELLO",after.Miners[0]);Assert.Equal(4,after.Fleet.GetTelemetry().Single(t=>t.Id=="next").WaitingSlot); after.Fleet.Command("clear-tasks");Assert.Equal(5,P.Data.ReadLong(after.Save(),"fleet","NextWaitingSlot")); } [Fact] public void ReusingIdleDeletedNameFromAnotherProgrammableBlockIsAllowed() { var rig=new Rig(1,false);rig.RegisterAll();rig.Program.Now=6;rig.Fleet.Command("clear-offline");rig.Receive("HELLO",rig.Miners[0],7000); Assert.Equal(7000,Assert.Single(rig.Fleet.GetTelemetry()).Address);Assert.Equal(0,P.Data.ReadLong(rig.Save(),"fleet","ProtectedOwnerCount")); } [Fact] public void RemovalClearsCancellationRequestsAndWireWorkWhileRetainingUnknownLane() { var rig=new Rig(1,false);rig.RegisterAll();rig.Tick(30);rig.Receive("DOCK_REQUEST",rig.Miners[0]);rig.Tick(12);rig.Fleet.Command("clear-tasks");Assert.Equal(1,rig.Fleet.CancellationCount); rig.Program.Now+=6;rig.Fleet.Command("clear-offline");Assert.Equal(0,rig.Fleet.CancellationCount);Assert.Equal("miner-0",rig.Save().Get("fleet","LaneOwner").ToString()); rig.Sent.Clear();rig.Program.Bus.Tick();Assert.DoesNotContain(rig.Sent,p=>p.Target=="miner-0"); var after=new Rig(1,false);after.Restore(rig.Program.Storage);Assert.Empty(after.Fleet.GetTelemetry());Assert.Equal(0,after.Fleet.CancellationCount);Assert.Equal("miner-0",after.Save().Get("fleet","LaneOwner").ToString()); } [Fact] public void RemovalRejectsOversizeJournalBeforeChangingRegistrationOrStorage() { var rig=new Rig(1,false);rig.RegisterAll();rig.Checkpoint();TestRig.SetBase(rig.Program,"Storage",rig.Program.Storage.PadRight(1500000,' '));string original=rig.Program.Storage;rig.Program.Now=6; Assert.Throws(()=>rig.Fleet.Command("clear-offline"));Assert.Single(rig.Fleet.GetTelemetry());Assert.Equal(original,rig.Program.Storage); } [Fact] public void LegacyWaitingSlotsAreAllocatedInRegistryOrderAndSavedWatermarkNeverRegresses() { var before=new Rig(2,false);before.RegisterAll();var legacy=before.Save();legacy.Set("fleet","NextWaitingSlot",7); for(int n=0;n<2;n++) {var miner=new MyIni();Assert.True(miner.TryParse(legacy.Get("fleet","Miner"+n).ToString()));miner.Delete("telemetry","WaitingSlot");legacy.Set("fleet","Miner"+n,miner.ToString());} var after=new Rig(2,false);after.Fleet.Load(legacy);Assert.Equal(new[]{7,8},after.Fleet.GetTelemetry().Select(t=>t.WaitingSlot));Assert.Equal(9,P.Data.ReadLong(after.Save(),"fleet","NextWaitingSlot")); } [Fact] public void OccupancyOverflowRejectsClearBeforeCancellingOrDeletingAnything() { var rig=new Rig(1,true);rig.RegisterAll();rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start");rig.Tick(2);P.Job owned=Assert.Single(rig.Fleet.GetJobs()); var holds=(Dictionary)typeof(P.FleetController).GetField("clearanceHolds",System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic).GetValue(rig.Fleet); for(int n=0;n<64;n++) {var hold=P.Job.FromIni(owned.ToIni());hold.Id="unknown-"+n;hold.Owner="unknown";holds.Add(hold.Id,hold);} Assert.Throws(()=>rig.Fleet.Command("clear-tasks"));Assert.Single(rig.Fleet.GetJobs());Assert.Equal(0,rig.Fleet.CancellationCount); rig.Program.Now+=6;Assert.Throws(()=>rig.Fleet.Command("clear-offline"));Assert.Single(rig.Fleet.GetTelemetry());Assert.Equal("miner-0",owned.Owner); } [Fact] public void DockedRenamedMinerCanReclaimCapacityAfterOldRegistrationWasAlreadyRemoved() { var before=new Rig(1,true);before.RegisterAll();before.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");before.Fleet.Command("start");before.Tick(30); P.Job job=Assert.Single(before.Fleet.GetJobs());job.Progress=7;before.Miners[0].JobId=job.Id;before.Receive("DEPART_REQUEST",before.Miners[0]);before.Tick(12);before.Checkpoint(); before.Program.Now+=6;before.Fleet.Command("clear-offline");Assert.Empty(before.Fleet.GetTelemetry());Assert.Equal("miner-0",job.Owner); var after=new Rig(1,true);after.Restore(before.Program.Storage);after.Miners[0].Id="renamed";after.Receive("HELLO",after.Miners[0]); Assert.Equal("",Assert.Single(after.Fleet.GetJobs()).Owner);Assert.Equal(7,after.Fleet.GetJobs()[0].Progress); Assert.Equal("",after.Save().Get("fleet","LaneOwner").ToString());Assert.Equal(0,P.Data.ReadLong(after.Save(),"fleet","ProtectedOwnerCount")); after.Tick(2);Assert.Equal("renamed",Assert.Single(after.Fleet.GetJobs()).Owner); } [Theory] [InlineData(false,5000)] [InlineData(true,7000)] public void RenamedIdWithoutPhysicalDockOrOriginalSourceCannotReleaseProtectedWork(bool connected,long source) { var r=new Rig(1,true);r.RegisterAll();r.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");r.Fleet.Command("start");r.Tick(2); r.Program.Now+=6;r.Fleet.Command("clear-offline");r.SetConnected(0,connected);r.Miners[0].Id="renamed";r.Miners[0].Connected=true;r.Receive("HELLO",r.Miners[0],source); Assert.Equal("miner-0",Assert.Single(r.Fleet.GetJobs()).Owner);Assert.Equal(1,P.Data.ReadLong(r.Save(),"fleet","ProtectedOwnerCount")); } [Fact] public void ConfirmedDockedRenameReleasesOldIdentityOccupancyAndPreservesTaskProgressOnReload() { var before=new Rig(1,true);before.RegisterAll();before.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");before.Fleet.Command("start");before.Tick(30); P.Job job=Assert.Single(before.Fleet.GetJobs());job.Progress=7;before.Miners[0].JobId=job.Id;before.Receive("DEPART_REQUEST",before.Miners[0]);before.Tick(12); before.Checkpoint();before.Program.Now+=6;before.Miners[0].Id="renamed";before.Miners[0].JobId="";before.Receive("HELLO",before.Miners[0]);before.Fleet.Command("clear-offline"); Assert.Equal("renamed",Assert.Single(before.Fleet.GetTelemetry()).Id);Assert.Equal("",job.Owner);Assert.Equal(7,job.Progress); var saved=before.Save();Assert.Equal("",saved.Get("fleet","LaneOwner").ToString());Assert.Equal(0,P.Data.ReadLong(saved,"fleet","ProtectedOwnerCount")); var after=new Rig(1,false);after.Restore(before.Program.Storage);P.Job restored=Assert.Single(after.Fleet.GetJobs());Assert.Equal("",restored.Owner);Assert.Equal(7,restored.Progress);Assert.Equal("",after.Save().Get("fleet","LaneOwner").ToString()); } } }