using System.IO.Pipes; using System.Text.Json; using XFEEDToolBox.Core.Galaxy; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public sealed class NativeGalaxyCurrentTradeFlowsTests { private const long First=9007199254740993,Second=9007199254740997; private static async Task Serve(int pid,object result,CancellationToken token,Action? inspect=null) { await using var server=new NamedPipeServerStream($"XFEEDToolBox.Navigation.{pid}",PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous); await server.WaitForConnectionAsync(token);var bytes=new byte[8192];var count=await server.ReadAsync(bytes,token); using(var request=JsonDocument.Parse(bytes.AsMemory(0,count)))inspect?.Invoke(request.RootElement); await server.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(new{ok=true,result}),token);await server.WriteAsync(new byte[]{10},token); if(OperatingSystem.IsWindows())server.WaitForPipeDrain(); } private static Dictionary Row(long from=First,long to=Second,ushort token=62000,string? name="原商品·茶",ulong timestamp=1000,float npc=.25f,float player=.5f)=>new() { ["fromSystemAddress"]=from.ToString(),["toSystemAddress"]=to.ToString(),["commodityToken"]=token,["commodityName"]=name, ["timestamp"]=timestamp.ToString(),["npcAmount"]=npc,["playerAmount"]=player }; private static Dictionary Page(int total,int offset,int? next,ulong clock=1000,int unresolved=0,params object[] items)=>new() { ["available"]=true,["generation"]=17UL,["entityId"]="9007199254740999",["source"]=GalaxyCurrentTradeFlowsSnapshot.Source,["complete"]=false, ["originalClock"]=clock.ToString(),["lifetimeSeconds"]=100UL,["refreshSeconds"]=50UL,["revision"]=7U,["unresolvedCount"]=unresolved, ["total"]=total,["offset"]=offset,["nextOffset"]=next,["items"]=items }; [Test] public async Task ExactAddressesUtf8ZeroAmountsAndMonotonicOriginalClockSurvivePagination() { var pid=Random.Shared.Next(180000000,190000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var server=Task.Run(async()=> { await Serve(pid,Page(2,0,1,1000,1,Row(timestamp:901,npc:0,player:0)),stop.Token,r=> {Check.Equal("galaxy.tradeflows",r.GetProperty("op").GetString());Check.Equal(16,r.GetProperty("limit").GetInt32());}); await Serve(pid,Page(2,1,null,1001,1,Row(Second,First,name:null,timestamp:1001)),stop.Token,r=> {Check.Equal(17UL,r.GetProperty("generation").GetUInt64());Check.Equal(1,r.GetProperty("offset").GetInt32());}); }); var result=await new NativeGalaxyAccountCatalog(pid).ReadCurrentTradeFlowsAsync(stop.Token);await server; Check.That(result.Available&&!result.Complete);Check.Equal(1001UL,result.OriginalClock);Check.Equal(7U,result.Revision); Check.Equal(First,result.Flows[0].FromSystemAddress);Check.Equal(Second,result.Flows[0].ToSystemAddress);Check.Equal((ushort)62000,result.Flows[0].CommodityToken); Check.Equal("原商品·茶",result.Flows[0].CommodityName);Check.Equal(0f,result.Flows[0].NpcAmount);Check.Equal(0f,result.Flows[0].PlayerAmount); Check.That(result.Flows[1].CommodityName is null);Check.Equal(1,result.UnresolvedCount); } [Test] public async Task SourceRevisionEntityClockAndPaginationChangesCannotMixSnapshots() { foreach(var change in new Action>[] { page=>page["source"]="historical-trd",page=>page["revision"]=8U,page=>page["entityId"]="other", page=>page["originalClock"]="999",page=>page["lifetimeSeconds"]=101UL,page=>page["generation"]=18UL, page=>page["offset"]=0,page=>page["nextOffset"]=2,page=>page["complete"]=true }) { var pid=Random.Shared.Next(180000000,190000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var last=Page(2,1,null,1000,0,Row(Second,First));change(last); var server=Task.Run(async()=>{await Serve(pid,Page(2,0,1,1000,0,Row()),stop.Token);await Serve(pid,last,stop.Token);}); var rejected=false;try{await new NativeGalaxyAccountCatalog(pid).ReadCurrentTradeFlowsAsync(stop.Token);}catch(InvalidDataException){rejected=true;} await server;Check.That(rejected); } } [Test] public async Task FutureExpiredDuplicateUnknownNameCountAndInvalidRowsAreRejected() { var duplicate=Row(); var badName=Row(name:"bad\nname");var badNpc=Row(npc:1.1f);var sameEndpoint=Row(to:First); foreach(var page in new[] { Page(1,0,null,1000,0,Row(timestamp:1001)),Page(1,0,null,1000,0,Row(timestamp:899)), Page(2,0,null,1000,0,duplicate,duplicate),Page(1,0,null,1000,0,Row(name:null)), Page(1,0,null,1000,0,badName),Page(1,0,null,1000,0,badNpc),Page(1,0,null,1000,0,sameEndpoint) }) { var pid=Random.Shared.Next(180000000,190000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var server=Task.Run(()=>Serve(pid,page,stop.Token));var rejected=false; try{await new NativeGalaxyAccountCatalog(pid).ReadCurrentTradeFlowsAsync(stop.Token);}catch(InvalidDataException){rejected=true;} await server;Check.That(rejected); } } [Test] public async Task FinalClockCanExpireAnEarlierPageAndUnderflowNeverBecomesZeroCutoff() { var pid=Random.Shared.Next(180000000,190000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var server=Task.Run(async()=> { await Serve(pid,Page(2,0,1,1000,0,Row(timestamp:900)),stop.Token); await Serve(pid,Page(2,1,null,1001,0,Row(Second,First,timestamp:1001)),stop.Token); }); var rejected=false;try{await new NativeGalaxyAccountCatalog(pid).ReadCurrentTradeFlowsAsync(stop.Token);}catch(InvalidDataException){rejected=true;} await server;Check.That(rejected); var underflow=Page(0,0,null,50);var secondPid=Random.Shared.Next(180000000,190000000); var secondServer=Task.Run(()=>Serve(secondPid,underflow,stop.Token)); var result=await new NativeGalaxyAccountCatalog(secondPid).ReadCurrentTradeFlowsAsync(stop.Token);await secondServer; Check.That(result.Available&&!result.Complete&&result.Flows.Count==0);Check.Equal(50UL,result.OriginalClock); } }