using System.IO.Pipes; using System.Text.Json; using XFEEDToolBox.Core.Galaxy; using XFEEDToolBox.Core.Models; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public sealed class NativeGalaxyCellBudgetTests { private const int DenseCount=24001; private const ulong Cell=7; private sealed record WireRow(string name,string systemAddress,double[] position,string starClass); private static WireRow Row(int index)=>new("Dense fixture "+index,(((ulong)index+1)<<23|Cell).ToString(),[index%1000,5,5],"M"); private static GalacticSystem[] DenseRows()=>Enumerable.Range(0,DenseCount).Select(i=> new GalacticSystem("Dense fixture "+i,new(i%1000,5,5),(long)(((ulong)i+1)<<23|Cell))).ToArray(); private static object Receipt(long generation,bool done,int total,WireRow[] rows,int rowLimit=131072,int processed=1)=> new{generation,done,error="",total,items=rows,scope="requested-cells",requestedCells=1,minimumMassCode=7, processedCells=processed,totalCells=1,failedCells=0,truncated=false,complete=done,rowLimit,origin=new[]{0d,0d,0d}}; private static async Task Serve(int id,Func reply,CancellationToken token) { 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;JsonElement request; while(true) { var read=await server.ReadAsync(buffer.AsMemory(count),token);if(read==0)throw new IOException("Empty budget-test request");count+=read; var parsed=ParseRequest(buffer,count);if(parsed is{} value){request=value;break;} if(count==buffer.Length)throw new IOException("Oversized budget-test request"); } var result=reply(request);var bytes=JsonSerializer.SerializeToUtf8Bytes(new{ok=true,result}); Check.That(bytes.Length<48000,"An enlarged cell still uses bounded individual IPC pages."); await server.WriteAsync(bytes,token);await server.WriteAsync(new byte[]{10},token); if(OperatingSystem.IsWindows())server.WaitForPipeDrain(); } private static JsonElement? ParseRequest(byte[] data,int count) { var reader=new Utf8JsonReader(data.AsSpan(0,count),false,default); if(!JsonDocument.TryParseValue(ref reader,out var document))return null; using(document)return document.RootElement.Clone(); } [Test] public async Task CompleteDenseSingleCellDownloads24001ActualRowsAcrossBoundedPagesAndPublishesBeforeDone() { var id=Random.Shared.Next(116000000,117000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(15)); var offset=0;var pages=0;var nativeDone=false;var publishedBeforeDone=false; var published=new HashSet();var duplicateProgress=false;var finalProgress=false; var server=Task.Run(async()=> { await Serve(id,r=> { Check.Equal("galaxy.cells",r.GetProperty("op").GetString());Check.Equal("7",r.GetProperty("cellAddresses")[0].GetString()); return Receipt(81,false,0,[],processed:0); },stop.Token); while(offset { Check.Equal("galaxy.result",r.GetProperty("op").GetString());Check.Equal(81L,r.GetProperty("generation").GetInt64()); Check.Equal(offset,r.GetProperty("offset").GetInt32());Check.Equal(96,r.GetProperty("limit").GetInt32()); var count=Math.Min(96,DenseCount-offset);var rows=Enumerable.Range(offset,count).Select(Row).ToArray(); var first=offset==0;offset+=count;pages++;nativeDone=!first; // The first owning-thread page is still busy. The next // snapshot has finished copying the cell; the client must // nevertheless download all remaining stable pages. return Receipt(81,!first,first?96:DenseCount,rows,processed:first?0:1); },stop.Token); }); var catalogue=new NativeGalaxyCatalog(id); catalogue.RegionProgress+=progress=> { Check.Equal(131072,progress.Coverage.RowLimit); if(progress.Systems.Count>0&&!nativeDone)publishedBeforeDone=true; foreach(var row in progress.Systems)if(!published.Add(row.SystemAddress!.Value))duplicateProgress=true; if(progress.Done){finalProgress=true;Check.Equal(DenseCount,progress.DownloadedCount);} }; var result=await catalogue.CellsAsync(new(5,5,5),Enumerable.Repeat(1d,8).ToArray(),new[]{Cell},stop.Token);await server; Check.Equal((DenseCount+95)/96,pages);Check.Equal(DenseCount,result.Systems.Count);Check.Equal(DenseCount,published.Count); Check.That(publishedBeforeDone&&finalProgress&&!duplicateProgress); Check.That(result.WithinReturnedRowBudget&&result.AreRequestedCellsCovered&&!result.Truncated&&!result.IsFullyCovered&&!result.NeedsSubdivision); Check.Equal(131072,result.RowLimit);Check.Equal(Row(DenseCount-1).name,result.Systems[^1].Name); Check.That(result.Systems.All(s=>GalaxyCellQueryPlan.CellAddress((ulong)s.SystemAddress!.Value)==Cell)); } [Test] public void RowBudgetRejectsNonSingleCellExpansionAndEveryExplicitInvalidLimit() { static GalaxyRegionResult Read(string text) {using var json=JsonDocument.Parse(text);return NativeGalaxyCatalog.ReadCoverage(json.RootElement,[]);} const string prefix="{\"scope\":\"requested-cells\",\"requestedCells\":1,\"minimumMassCode\":7,\"processedCells\":1,\"totalCells\":1,\"failedCells\":0,\"complete\":true"; foreach(var invalid in new[]{"-1","0","24001","131073","1.5","\"131072\"","null","true"}) Check.Throws(()=>Read(prefix+",\"rowLimit\":"+invalid+"}")); Check.Throws(()=>Read("{\"scope\":\"region-sphere\",\"rowLimit\":131072}")); Check.Throws(()=>Read("{\"scope\":\"requested-cells\",\"requestedCells\":2,\"totalCells\":2,\"rowLimit\":131072}")); Check.Equal(24000,Read(prefix+"}").RowLimit); var allowed=Read(prefix+",\"rowLimit\":131072}");Check.That(allowed.AreRequestedCellsCovered&&!allowed.IsFullyCovered); var exactCap=allowed with{Systems=Enumerable.Repeat(new GalacticSystem("Budget fixture",new(5,5,5),15),131072).ToArray()}; Check.That(exactCap.WithinReturnedRowBudget&&exactCap.AreRequestedCellsCovered); Check.That(!(exactCap with{Systems=exactCap.Systems.Append(exactCap.Systems[0]).ToArray()}).WithinReturnedRowBudget); } [Test] public async Task ChangingTheAcceptedRowBudgetRejectsTheGenerationAndRetiresItsQueueBeforeRetry() { var id=Random.Shared.Next(117000000,118000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var cancelled=false;var drained=false;var server=Task.Run(async()=> { await Serve(id,_=>Receipt(91,false,0,[],24000,0),stop.Token); await Serve(id,_=>Receipt(91,false,0,[],131072,0),stop.Token); await Serve(id,r=> {cancelled=r.GetProperty("op").GetString()=="galaxy.cancel"&&r.GetProperty("generation").GetInt64()==91;return new{generation=91,done=true};},stop.Token); await Serve(id,r=> {drained=r.GetProperty("op").GetString()=="galaxy.result"&&r.GetProperty("generation").GetInt64()==91;return new{generation=91,done=true};},stop.Token); await Serve(id,r=> {Check.Equal("galaxy.cells",r.GetProperty("op").GetString());return Receipt(92,true,0,[]);},stop.Token); }); var catalogue=new NativeGalaxyCatalog(id);var rejected=false; try{await catalogue.CellsAsync(new(5,5,5),Enumerable.Repeat(1d,8).ToArray(),new[]{Cell},stop.Token);} catch(InvalidDataException){rejected=true;} Check.That(rejected);var retry=await catalogue.CellsAsync(new(5,5,5),Enumerable.Repeat(1d,8).ToArray(),new[]{Cell},stop.Token);await server; Check.That(cancelled&&drained&&retry.AreRequestedCellsCovered); } [Test] public async Task CellStreamingCachesTheCompleteDenseSingleCellWithoutSubdivisionOrASecondNativeQuery() { var centre=new Coordinates(5,5,5);var descriptor=GalaxyOriginalCatalogueLod.Create(centre,0,500,0,1,.001); var plan=new GalaxyCellQueryPlan(GalaxyCamera.Create(centre,1600,900,0,0,4),descriptor,new(0,0,0)); Check.Equal(1,plan.Cells.Count);Check.Equal(Cell,plan.Cells[0].Address); using var session=new GalaxyCellStreamingSession(new(){MaximumCachedSystems=50000});var calls=0;var updates=new List(); Task Query(Coordinates _,IReadOnlyList radii,IReadOnlyList cells,CancellationToken token) { calls++;Check.Equal(1,cells.Count);Check.Equal(Cell,cells[0]); return Task.FromResult(new GalaxyRegionResult(DenseRows(),7,1,1,false,true) {Scope="requested-cells",RequestedCells=1,Origin=new(0,0,0),RowLimit=131072}); } await foreach(var update in session.StreamAsync(plan,Query))updates.Add(update); Check.Equal(1,calls);Check.Equal(DenseCount,updates.SelectMany(u=>u.Systems).Count()); Check.That(updates[^1].CoverageComplete&&updates[^1].UnverifiedCells==0&&updates[^1].RefinedBatches==0); var cached=new List(); await foreach(var update in session.StreamAsync(plan,(_,_,_,_)=>throw new Exception("A complete dense cell must not be queried again")))cached.Add(update); Check.That(cached[^1].CoverageComplete&&cached.Any(u=>u.FromCache&&u.Systems.Count==DenseCount));Check.Equal(1,session.CachedCells); } }