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<JsonElement>? 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<string,object?> 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<string,object?> 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<Dictionary<string,object?>>[]
{
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);
}
}
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<JsonElement>? 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<string,object?> 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<string,object?> 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<Dictionary<string,object?>>[]
{
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);
}
}