using System.IO.Pipes;
using System.Text.Json;
using XFEEDToolBox.Core.Galaxy;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class NativeGalaxyCatalogueProfileTests
{
private static async Task Serve(int id,object result,CancellationToken token,Action<JsonElement>? inspect=null)
{
await using var server=new NamedPipeServerStream($"XFEEDToolBox.Navigation.{id}",PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous);
await server.WaitForConnectionAsync(token);var buffer=new byte[65536];var count=0;
while(true)
{
var read=await server.ReadAsync(buffer.AsMemory(count),token);if(read==0)throw new IOException("Empty request");count+=read;
var parsed=Parse(buffer,count);if(parsed is{} request)
{inspect?.Invoke(request);if(result is Func<JsonElement,object> choose)result=choose(request);break;}
if(count==buffer.Length)throw new IOException("Request exceeded limit");
}
await server.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(new{ok=true,result}),token);await server.WriteAsync(new byte[]{10},token);
if(OperatingSystem.IsWindows())server.WaitForPipeDrain();
static JsonElement? Parse(byte[] bytes,int count)
{
var reader=new Utf8JsonReader(bytes.AsSpan(0,count),false,default);
if(!JsonDocument.TryParseValue(ref reader,out var d))return null;
using(d)return d.RootElement.Clone();
}
}
[Test] public async Task ProfileReadsNativeChosenCentreAndActualInputsWithoutDefaultingQuality()
{
var id=Random.Shared.Next(111000000,112000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var server=Task.Run(async()=>
{
await Serve(id,new{generation=9,done=false,error="",profile=(object?)null},stop.Token,r=>
{Check.Equal("galaxy.catalogue.profile",r.GetProperty("op").GetString());Check.Equal(3d,r.GetProperty("eye")[2].GetDouble());Check.Equal(6d,r.GetProperty("focus")[2].GetDouble());});
await Serve(id,new{generation=9,done=true,error="",profile=new{origin=new[]{49985d,40985d,24105d},queryCentre=new[]{4d,5d,6d},
surfaceDensity=.4,qualityStarCount=0u,runtimeRadiusMultiplier=1d,sparseExpansion=4d,smallMode=false,originalViewMode=2,debugEnabled=false,colourBlend=.375,referenceKind="camera-eye"}},stop.Token,
r=>Check.Equal("galaxy.catalogue.profile.result",r.GetProperty("op").GetString()));
});
var result=await new NativeGalaxyCatalog(id).CatalogueProfileAsync(new(1,2,3),new(4,5,6),stop.Token);await server;
Check.That(result.HasLodInputs);Check.Equal(0u,result.QualityStarCount!.Value);Check.Equal(4d,result.QueryCentre!.X);Check.Equal(49985d,result.Origin.X);
Check.Equal(.375,result.ColourBlend!.Value);
}
[Test] public void UninitialisedMapKeepsOriginAndUnknownLodInputs()
{
using var json=JsonDocument.Parse("{\"origin\":[1,2,3],\"queryCentre\":null,\"surfaceDensity\":null,\"qualityStarCount\":null,\"referenceKind\":\"camera-eye\"}");
var result=NativeGalaxyCatalog.ReadCatalogueProfile(json.RootElement);Check.That(!result.HasLodInputs);
Check.Equal(2d,result.Origin.Y);Check.That(result.QualityStarCount is null&&result.RuntimeRadiusMultiplier is null&&result.SmallMode is null);
}
[Test] public void ProfileRejectsUnboundSamplingAndUnsupportedProtocol()
{
using var malformed=JsonDocument.Parse("{\"origin\":[1,2,3],\"queryCentre\":[4,5,6],\"surfaceDensity\":0,\"referenceKind\":\"camera-eye\"}");
Check.Throws<InvalidDataException>(()=>NativeGalaxyCatalog.ReadCatalogueProfile(malformed.RootElement));
var catalog=new NativeGalaxyCatalog(123);using var description=JsonDocument.Parse("{\"processId\":123}");catalog.ApplyProtocolDescription(description.RootElement);
Check.Throws<NotSupportedException>(()=>catalog.CatalogueProfileAsync(new(0,0,0),new(0,0,0)).GetAwaiter().GetResult());
}
[Test] public async Task CancellationRetiresOnlyTheAcceptedProfileGeneration()
{
var id=Random.Shared.Next(112000000,113000000);using var deadline=new CancellationTokenSource(TimeSpan.FromSeconds(5));using var caller=new CancellationTokenSource();
var cancelled=false;var server=Task.Run(async()=>
{
await Serve(id,new{generation=42,done=false,error="",profile=(object?)null},deadline.Token,_=>caller.Cancel());
await Serve(id,new{generation=42,done=true,cancelled=true,error="",profile=(object?)null},deadline.Token,r=>
{cancelled=r.GetProperty("op").GetString()=="galaxy.catalogue.profile.cancel"&&r.GetProperty("generation").GetInt64()==42;});
});
try{await new NativeGalaxyCatalog(id).CatalogueProfileAsync(new(0,0,0),new(0,0,0),caller.Token);throw new Exception("Cancellation was ignored");}
catch(OperationCanceledException){}
await server;Check.That(cancelled);
}
[Test] public async Task ProfileCompletesWhileTheOldCellJobIsStillBusy()
{
var id=Random.Shared.Next(113000000,114000000);using var deadline=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var started=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);var profileFinished=false;var cellsFinished=false;
var server=Task.Run(async()=>
{
while(!cellsFinished||!profileFinished)
await Serve(id,new Func<JsonElement,object>(r=>
{
var op=r.GetProperty("op").GetString();
if(op=="galaxy.catalogue.profile")
{
profileFinished=true;
return new{generation=92,done=true,error="",profile=new{origin=new[]{1d,2d,3d},referenceKind="camera-eye"}};
}
if(op is "galaxy.cells" or "galaxy.result")
{
started.TrySetResult();cellsFinished=profileFinished;
return new{generation=91,done=cellsFinished,error="",total=0,items=Array.Empty<object>(),scope="requested-cells",requestedCells=1,
minimumMassCode=0,processedCells=cellsFinished?1:0,totalCells=1,failedCells=0,truncated=false,complete=cellsFinished,origin=new[]{1d,2d,3d}};
}
return new{generation=91,done=true};
}),deadline.Token);
});
var catalogue=new NativeGalaxyCatalog(id);var cells=catalogue.CellsAsync(new(0,0,0),Enumerable.Repeat(1d,8).ToArray(),new[]{0UL},deadline.Token);
await started.Task.WaitAsync(deadline.Token);
var profile=await catalogue.CatalogueProfileAsync(new(0,0,0),new(0,0,0),deadline.Token).WaitAsync(TimeSpan.FromSeconds(2));
Check.Equal(1d,profile.Origin.X);Check.That(profileFinished);var result=await cells;await server;Check.That(result.AreRequestedCellsCovered);
}
[Test] public void OptionalColourBlendKeepsItsOwnUnknownStateAndRejectsNonUnitWireValues()
{
const string fields="\"origin\":[1,2,3],\"queryCentre\":[4,5,6],\"surfaceDensity\":0.4,\"qualityStarCount\":0,\"runtimeRadiusMultiplier\":1,\"sparseExpansion\":4,\"smallMode\":false,\"originalViewMode\":2,\"debugEnabled\":false,\"referenceKind\":\"camera-eye\"";
static GalaxyCatalogueProfile Parse(string body)
{using var doc=JsonDocument.Parse(body);return NativeGalaxyCatalog.ReadCatalogueProfile(doc.RootElement);}
foreach(var suffix in new[]{"",",\"colourBlend\":null"})
{var profile=Parse("{"+fields+suffix+"}");Check.That(profile.HasLodInputs&&profile.ColourBlend is null);}
foreach(var value in new[]{"0","0.375","1"})
{var profile=Parse("{"+fields+",\"colourBlend\":"+value+"}");Check.That(profile.HasLodInputs);Check.Equal(double.Parse(value,System.Globalization.CultureInfo.InvariantCulture),profile.ColourBlend!.Value);}
foreach(var invalid in new[]{"-0.01","1.01","1e309","\"0.5\"","true","[]","{}"})
Check.Throws<InvalidDataException>(()=>Parse("{"+fields+",\"colourBlend\":"+invalid+"}"));
}
}
using System.IO.Pipes;
using System.Text.Json;
using XFEEDToolBox.Core.Galaxy;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class NativeGalaxyCatalogueProfileTests
{
private static async Task Serve(int id,object result,CancellationToken token,Action<JsonElement>? inspect=null)
{
await using var server=new NamedPipeServerStream($"XFEEDToolBox.Navigation.{id}",PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous);
await server.WaitForConnectionAsync(token);var buffer=new byte[65536];var count=0;
while(true)
{
var read=await server.ReadAsync(buffer.AsMemory(count),token);if(read==0)throw new IOException("Empty request");count+=read;
var parsed=Parse(buffer,count);if(parsed is{} request)
{inspect?.Invoke(request);if(result is Func<JsonElement,object> choose)result=choose(request);break;}
if(count==buffer.Length)throw new IOException("Request exceeded limit");
}
await server.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(new{ok=true,result}),token);await server.WriteAsync(new byte[]{10},token);
if(OperatingSystem.IsWindows())server.WaitForPipeDrain();
static JsonElement? Parse(byte[] bytes,int count)
{
var reader=new Utf8JsonReader(bytes.AsSpan(0,count),false,default);
if(!JsonDocument.TryParseValue(ref reader,out var d))return null;
using(d)return d.RootElement.Clone();
}
}
[Test] public async Task ProfileReadsNativeChosenCentreAndActualInputsWithoutDefaultingQuality()
{
var id=Random.Shared.Next(111000000,112000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var server=Task.Run(async()=>
{
await Serve(id,new{generation=9,done=false,error="",profile=(object?)null},stop.Token,r=>
{Check.Equal("galaxy.catalogue.profile",r.GetProperty("op").GetString());Check.Equal(3d,r.GetProperty("eye")[2].GetDouble());Check.Equal(6d,r.GetProperty("focus")[2].GetDouble());});
await Serve(id,new{generation=9,done=true,error="",profile=new{origin=new[]{49985d,40985d,24105d},queryCentre=new[]{4d,5d,6d},
surfaceDensity=.4,qualityStarCount=0u,runtimeRadiusMultiplier=1d,sparseExpansion=4d,smallMode=false,originalViewMode=2,debugEnabled=false,colourBlend=.375,referenceKind="camera-eye"}},stop.Token,
r=>Check.Equal("galaxy.catalogue.profile.result",r.GetProperty("op").GetString()));
});
var result=await new NativeGalaxyCatalog(id).CatalogueProfileAsync(new(1,2,3),new(4,5,6),stop.Token);await server;
Check.That(result.HasLodInputs);Check.Equal(0u,result.QualityStarCount!.Value);Check.Equal(4d,result.QueryCentre!.X);Check.Equal(49985d,result.Origin.X);
Check.Equal(.375,result.ColourBlend!.Value);
}
[Test] public void UninitialisedMapKeepsOriginAndUnknownLodInputs()
{
using var json=JsonDocument.Parse("{\"origin\":[1,2,3],\"queryCentre\":null,\"surfaceDensity\":null,\"qualityStarCount\":null,\"referenceKind\":\"camera-eye\"}");
var result=NativeGalaxyCatalog.ReadCatalogueProfile(json.RootElement);Check.That(!result.HasLodInputs);
Check.Equal(2d,result.Origin.Y);Check.That(result.QualityStarCount is null&&result.RuntimeRadiusMultiplier is null&&result.SmallMode is null);
}
[Test] public void ProfileRejectsUnboundSamplingAndUnsupportedProtocol()
{
using var malformed=JsonDocument.Parse("{\"origin\":[1,2,3],\"queryCentre\":[4,5,6],\"surfaceDensity\":0,\"referenceKind\":\"camera-eye\"}");
Check.Throws<InvalidDataException>(()=>NativeGalaxyCatalog.ReadCatalogueProfile(malformed.RootElement));
var catalog=new NativeGalaxyCatalog(123);using var description=JsonDocument.Parse("{\"processId\":123}");catalog.ApplyProtocolDescription(description.RootElement);
Check.Throws<NotSupportedException>(()=>catalog.CatalogueProfileAsync(new(0,0,0),new(0,0,0)).GetAwaiter().GetResult());
}
[Test] public async Task CancellationRetiresOnlyTheAcceptedProfileGeneration()
{
var id=Random.Shared.Next(112000000,113000000);using var deadline=new CancellationTokenSource(TimeSpan.FromSeconds(5));using var caller=new CancellationTokenSource();
var cancelled=false;var server=Task.Run(async()=>
{
await Serve(id,new{generation=42,done=false,error="",profile=(object?)null},deadline.Token,_=>caller.Cancel());
await Serve(id,new{generation=42,done=true,cancelled=true,error="",profile=(object?)null},deadline.Token,r=>
{cancelled=r.GetProperty("op").GetString()=="galaxy.catalogue.profile.cancel"&&r.GetProperty("generation").GetInt64()==42;});
});
try{await new NativeGalaxyCatalog(id).CatalogueProfileAsync(new(0,0,0),new(0,0,0),caller.Token);throw new Exception("Cancellation was ignored");}
catch(OperationCanceledException){}
await server;Check.That(cancelled);
}
[Test] public async Task ProfileCompletesWhileTheOldCellJobIsStillBusy()
{
var id=Random.Shared.Next(113000000,114000000);using var deadline=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var started=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);var profileFinished=false;var cellsFinished=false;
var server=Task.Run(async()=>
{
while(!cellsFinished||!profileFinished)
await Serve(id,new Func<JsonElement,object>(r=>
{
var op=r.GetProperty("op").GetString();
if(op=="galaxy.catalogue.profile")
{
profileFinished=true;
return new{generation=92,done=true,error="",profile=new{origin=new[]{1d,2d,3d},referenceKind="camera-eye"}};
}
if(op is "galaxy.cells" or "galaxy.result")
{
started.TrySetResult();cellsFinished=profileFinished;
return new{generation=91,done=cellsFinished,error="",total=0,items=Array.Empty<object>(),scope="requested-cells",requestedCells=1,
minimumMassCode=0,processedCells=cellsFinished?1:0,totalCells=1,failedCells=0,truncated=false,complete=cellsFinished,origin=new[]{1d,2d,3d}};
}
return new{generation=91,done=true};
}),deadline.Token);
});
var catalogue=new NativeGalaxyCatalog(id);var cells=catalogue.CellsAsync(new(0,0,0),Enumerable.Repeat(1d,8).ToArray(),new[]{0UL},deadline.Token);
await started.Task.WaitAsync(deadline.Token);
var profile=await catalogue.CatalogueProfileAsync(new(0,0,0),new(0,0,0),deadline.Token).WaitAsync(TimeSpan.FromSeconds(2));
Check.Equal(1d,profile.Origin.X);Check.That(profileFinished);var result=await cells;await server;Check.That(result.AreRequestedCellsCovered);
}
[Test] public void OptionalColourBlendKeepsItsOwnUnknownStateAndRejectsNonUnitWireValues()
{
const string fields="\"origin\":[1,2,3],\"queryCentre\":[4,5,6],\"surfaceDensity\":0.4,\"qualityStarCount\":0,\"runtimeRadiusMultiplier\":1,\"sparseExpansion\":4,\"smallMode\":false,\"originalViewMode\":2,\"debugEnabled\":false,\"referenceKind\":\"camera-eye\"";
static GalaxyCatalogueProfile Parse(string body)
{using var doc=JsonDocument.Parse(body);return NativeGalaxyCatalog.ReadCatalogueProfile(doc.RootElement);}
foreach(var suffix in new[]{"",",\"colourBlend\":null"})
{var profile=Parse("{"+fields+suffix+"}");Check.That(profile.HasLodInputs&&profile.ColourBlend is null);}
foreach(var value in new[]{"0","0.375","1"})
{var profile=Parse("{"+fields+",\"colourBlend\":"+value+"}");Check.That(profile.HasLodInputs);Check.Equal(double.Parse(value,System.Globalization.CultureInfo.InvariantCulture),profile.ColourBlend!.Value);}
foreach(var invalid in new[]{"-0.01","1.01","1e309","\"0.5\"","true","[]","{}"})
Check.Throws<InvalidDataException>(()=>Parse("{"+fields+",\"colourBlend\":"+invalid+"}"));
}
}