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<P.Packet> Sent=new List<P.Packet>();
public readonly List<P.Telemetry> Miners=new List<P.Telemetry>();
readonly List<Stub<IMyShipConnector>> portStubs=new List<Stub<IMyShipConnector>>(),remoteStubs=new List<Stub<IMyShipConnector>>();
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<IMyBroadcastListener>();var unicast=new Stub<IMyUnicastListener>();
var igc=new Stub<IMyIntergridCommunicationSystem>().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<IMyTerminalBlock>();var ports=new List<IMyTerminalBlock>();
controls.Add(new Stub<IMyShipController>().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<count;n++)
{
Vector3D position=new Vector3D(n*40,0,0);MatrixD matrix=MatrixD.CreateWorld(position,Vector3D.Forward,Vector3D.Up);
var grid=new Stub<IMyCubeGrid>().Set("EntityId",1000L+n);
var remote=new Stub<IMyShipConnector>().Set("CubeGrid",grid.Value).Set("EntityId",2000L+n).Set("Status",connected?MyShipConnectorStatus.Connected:MyShipConnectorStatus.Unconnected);
var port=new Stub<IMyShipConnector>().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<IMyBlockGroup>().Method("GetBlocksOfType",a=>Fill(a,ports));
var terminal=new Stub<IMyGridTerminalSystem>().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<IMyTerminalBlock> 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;n<steps;n++){Program.Now+=0.1;Fleet.Tick(0.1);}}
public string Checkpoint()
{
string value=null;int frames=0;while(!Fleet.CheckpointStorage(out value)&&frames++<2000){}Assert.NotNull(value);TestRig.SetBase(Program,"Storage",value);return value;
}
public void Restore(string value)
{
TestRig.SetBase(Program,"Storage",value);Assert.True(Fleet.BeginRestoreStorage(value));int frames=0;while(Fleet.Restoring&&frames++<2000)Fleet.RestoreStorageStep();Assert.False(Fleet.Restoring);
}
public MyIni Save() {var ini=new MyIni();Fleet.Save(ini);return ini;}
public P.TaskPlanner Planner {get{return (P.TaskPlanner)typeof(P.FleetController).GetField("planner",System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic).GetValue(Fleet);}}
}
[Fact]
public void SixteenMinersReceiveDistinctNonIntersectingReservedHoles()
{
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);
var owned=rig.Fleet.GetJobs().Where(j=>j.Owner.Length>0).ToList();Assert.Equal(16,owned.Count);Assert.Equal(16,owned.Select(j=>j.Owner).Distinct().Count());
for(int i=0;i<owned.Count;i++)for(int k=i+1;k<owned.Count;k++)
Assert.True(P.Data.SegmentDistance(owned[i].Entry-owned[i].Direction*15,owned[i].Entry+owned[i].Direction*20,owned[k].Entry-owned[k].Direction*15,owned[k].Entry+owned[k].Direction*20)>=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 IndependentPortsAreGrantedOnEachRequestAndRetainOtherUnknownOccupancy()
{
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]);Assert.Equal("miner-0",Assert.Single(rig.Sent,p=>p.Kind=="DOCK_GRANT").Target);
rig.Receive("DOCK_REQUEST",rig.Miners[1]);
P.Packet granted=rig.Sent.Last(p=>p.Kind=="DOCK_GRANT");Assert.Equal("miner-1",granted.Target);Assert.Equal(2,rig.Sent.Count(p=>p.Kind=="DOCK_GRANT"));
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]);
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<ArgumentException>(()=>{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<ArgumentException>(()=>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 ClearRetainsConnectorAssignmentAndRequiresAppliedWatermarkPlusPhysicalDock()
{
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 CancelledDepartureCanReuseItsAssignedConnectorForReturnWithoutReleasingIt()
{
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]);
Assert.Equal("miner-0",rig.Save().Get("fleet","LaneOwner").ToString());rig.Tick(12);
P.DockFrame inbound=P.DockFrame.FromIni(rig.Sent.Last(p=>p.Kind=="DOCK_GRANT").Body);Assert.Equal("miner-0",inbound.Owner);Assert.Equal(100,inbound.ConnectorId);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 OutboundConnectorAssignmentRemainsUntilMinerConfirmsDepartureComplete()
{
var rig=new Rig(1,true);string token=BeginDeparture(rig);
// Passing the straight departure distance alone does not complete the
// departure state or release this connector for another miner.
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 InterruptedDepartureKeepsItsConnectorAssignmentAfterFlyingAway(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 OfflineRemovalSnapshotsNewMiningGeometryConnectorAndSerial()
{
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 RemovalClearsCancellationRequestsAndWireWorkWhileRetainingUnknownConnectorAssignment()
{
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<ArgumentException>(()=>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<string,P.Job>)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<ArgumentException>(()=>rig.Fleet.Command("clear-tasks"));Assert.Single(rig.Fleet.GetJobs());Assert.Equal(0,rig.Fleet.CancellationCount);
rig.Program.Now+=6;Assert.Throws<ArgumentException>(()=>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());
}
}
}
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<P.Packet> Sent=new List<P.Packet>();
public readonly List<P.Telemetry> Miners=new List<P.Telemetry>();
readonly List<Stub<IMyShipConnector>> portStubs=new List<Stub<IMyShipConnector>>(),remoteStubs=new List<Stub<IMyShipConnector>>();
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<IMyBroadcastListener>();var unicast=new Stub<IMyUnicastListener>();
var igc=new Stub<IMyIntergridCommunicationSystem>().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<IMyTerminalBlock>();var ports=new List<IMyTerminalBlock>();
controls.Add(new Stub<IMyShipController>().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<count;n++)
{
Vector3D position=new Vector3D(n*40,0,0);MatrixD matrix=MatrixD.CreateWorld(position,Vector3D.Forward,Vector3D.Up);
var grid=new Stub<IMyCubeGrid>().Set("EntityId",1000L+n);
var remote=new Stub<IMyShipConnector>().Set("CubeGrid",grid.Value).Set("EntityId",2000L+n).Set("Status",connected?MyShipConnectorStatus.Connected:MyShipConnectorStatus.Unconnected);
var port=new Stub<IMyShipConnector>().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<IMyBlockGroup>().Method("GetBlocksOfType",a=>Fill(a,ports));
var terminal=new Stub<IMyGridTerminalSystem>().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<IMyTerminalBlock> 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;n<steps;n++){Program.Now+=0.1;Fleet.Tick(0.1);}}
public string Checkpoint()
{
string value=null;int frames=0;while(!Fleet.CheckpointStorage(out value)&&frames++<2000){}Assert.NotNull(value);TestRig.SetBase(Program,"Storage",value);return value;
}
public void Restore(string value)
{
TestRig.SetBase(Program,"Storage",value);Assert.True(Fleet.BeginRestoreStorage(value));int frames=0;while(Fleet.Restoring&&frames++<2000)Fleet.RestoreStorageStep();Assert.False(Fleet.Restoring);
}
public MyIni Save() {var ini=new MyIni();Fleet.Save(ini);return ini;}
public P.TaskPlanner Planner {get{return (P.TaskPlanner)typeof(P.FleetController).GetField("planner",System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic).GetValue(Fleet);}}
}
[Fact]
public void SixteenMinersReceiveDistinctNonIntersectingReservedHoles()
{
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);
var owned=rig.Fleet.GetJobs().Where(j=>j.Owner.Length>0).ToList();Assert.Equal(16,owned.Count);Assert.Equal(16,owned.Select(j=>j.Owner).Distinct().Count());
for(int i=0;i<owned.Count;i++)for(int k=i+1;k<owned.Count;k++)
Assert.True(P.Data.SegmentDistance(owned[i].Entry-owned[i].Direction*15,owned[i].Entry+owned[i].Direction*20,owned[k].Entry-owned[k].Direction*15,owned[k].Entry+owned[k].Direction*20)>=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 IndependentPortsAreGrantedOnEachRequestAndRetainOtherUnknownOccupancy()
{
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]);Assert.Equal("miner-0",Assert.Single(rig.Sent,p=>p.Kind=="DOCK_GRANT").Target);
rig.Receive("DOCK_REQUEST",rig.Miners[1]);
P.Packet granted=rig.Sent.Last(p=>p.Kind=="DOCK_GRANT");Assert.Equal("miner-1",granted.Target);Assert.Equal(2,rig.Sent.Count(p=>p.Kind=="DOCK_GRANT"));
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]);
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<ArgumentException>(()=>{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<ArgumentException>(()=>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 ClearRetainsConnectorAssignmentAndRequiresAppliedWatermarkPlusPhysicalDock()
{
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 CancelledDepartureCanReuseItsAssignedConnectorForReturnWithoutReleasingIt()
{
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]);
Assert.Equal("miner-0",rig.Save().Get("fleet","LaneOwner").ToString());rig.Tick(12);
P.DockFrame inbound=P.DockFrame.FromIni(rig.Sent.Last(p=>p.Kind=="DOCK_GRANT").Body);Assert.Equal("miner-0",inbound.Owner);Assert.Equal(100,inbound.ConnectorId);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 OutboundConnectorAssignmentRemainsUntilMinerConfirmsDepartureComplete()
{
var rig=new Rig(1,true);string token=BeginDeparture(rig);
// Passing the straight departure distance alone does not complete the
// departure state or release this connector for another miner.
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 InterruptedDepartureKeepsItsConnectorAssignmentAfterFlyingAway(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 OfflineRemovalSnapshotsNewMiningGeometryConnectorAndSerial()
{
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 RemovalClearsCancellationRequestsAndWireWorkWhileRetainingUnknownConnectorAssignment()
{
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<ArgumentException>(()=>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<string,P.Job>)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<ArgumentException>(()=>rig.Fleet.Command("clear-tasks"));Assert.Single(rig.Fleet.GetJobs());Assert.Equal(0,rig.Fleet.CancellationCount);
rig.Program.Now+=6;Assert.Throws<ArgumentException>(()=>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());
}
}
}