XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Globalization;
using System.IO.Pipes;
using System.Text.Json;
using XFEEDToolBox.Core.Galaxy;
using XFEExtension.NetCore.XUnit.Attributes;

namespace XFEEDToolBox.Test;

[TestFixture]
public sealed class NativeGalaxyCivilizationServicesWireTests
{
    private const long First=9007199254740993;
    private static object Civilization(string id="9007199254740995",string key="guardian",bool defaulted=false)=>new{definitionId=id,key,defaulted};
    private static object Services(int mask=513,bool defaulted=false)=>new{mask,defaulted};
    private static object Row(long address,params (string Name,object? Value)[] fields)
    {
        var row=new Dictionary<string,object?>{{"systemAddress",address.ToString(CultureInfo.InvariantCulture)}};
        foreach(var field in fields)row.Add(field.Name,field.Value);
        return row;
    }
    private static object Page(string origin,int total,bool done,params object[] rows)=>new
        {generation=17UL,contextSystemAddress=origin,total,done,error="",items=rows};

    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 buffer=new byte[8192];var count=0;
        // The byte pipe has no message boundary. Accumulate a complete JSON
        // request rather than assuming one ReadAsync returns every byte.
        while(true)
        {
            if(count==buffer.Length)throw new InvalidDataException("Fixture request exceeds its bound.");
            var read=await server.ReadAsync(buffer.AsMemory(count),token);
            if(read==0)throw new EndOfStreamException();count+=read;
            JsonDocument request;
            try{request=JsonDocument.Parse(buffer.AsMemory(0,count));}catch(JsonException){continue;}
            using(request)inspect?.Invoke(request.RootElement);
            break;
        }
        await server.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(new{ok=true,result}),token);
        await server.WriteAsync(new byte[]{10},token);await server.FlushAsync(token);
        if(OperatingSystem.IsWindows())server.WaitForPipeDrain();
    }

    private static void InspectRequest(JsonElement request,bool civilization,bool services)
    {
        Check.Equal("galaxy.facts",request.GetProperty("op").GetString());
        Check.Equal(civilization,request.GetProperty("includeCivilization").GetBoolean());
        Check.Equal(services,request.GetProperty("includeServices").GetBoolean());
        Check.Equal(First.ToString(CultureInfo.InvariantCulture),request.GetProperty("systemAddresses")[0].GetString());
    }

    private static async Task<GalaxyNativeFilterFact> ReadOne(object row,bool civilization,bool services)
    {
        var pid=Random.Shared.Next(190000000,200000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));
        var server=Task.Run(()=>Serve(pid,Page("42",1,true,row),stop.Token,r=>InspectRequest(r,civilization,services)));
        var result=await new NativeGalaxyFilterFactsCatalog(pid).ReadAsync([First],stop.Token,includeCivilization:civilization,includeServices:services);
        await server;Check.Equal(42L,result.OriginSystemAddress);return result.Rows.Single();
    }

    private static async Task RejectAndRecover(object row,bool civilization=true,bool services=true)
    {
        var pid=Random.Shared.Next(190000000,200000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));
        var server=Task.Run(async()=>
        {
            await Serve(pid,Page("42",1,true,row),stop.Token,r=>InspectRequest(r,civilization,services));
            await Serve(pid,new{cancelled=true},stop.Token,r=>
            {Check.Equal("galaxy.facts.cancel",r.GetProperty("op").GetString());Check.Equal(17UL,r.GetProperty("generation").GetUInt64());});
            await Serve(pid,Page("42",1,true,Row(First,("civilization",null),("services",null))),stop.Token,r=>InspectRequest(r,civilization,services));
        });
        var catalog=new NativeGalaxyFilterFactsCatalog(pid);var rejected=false;
        try{await catalog.ReadAsync([First],stop.Token,includeCivilization:civilization,includeServices:services);}catch(InvalidDataException){rejected=true;}
        Check.That(rejected,"Malformed or unsolicited classifier receipt was accepted.");
        // Reusing the same client verifies both original-generation cleanup
        // and release of the client's semaphore after rejection.
        var recovered=await catalog.ReadAsync([First],stop.Token,includeCivilization:civilization,includeServices:services);
        await server;var result=recovered.Rows.Single();
        Check.That(result.Civilization is null&&result.ServiceMask is null&&result.ServicesDefaulted is null);
        Check.Equal(civilization,result.CivilizationRequested);Check.Equal(services,result.ServicesRequested);
    }

    [Test] public async Task RequestedExplicitNullKeepsIndependentQueryBindingsWithoutInventingNegativeFacts()
    {
        foreach(var request in new[]{(Civilization:true,Services:false),(Civilization:false,Services:true),(Civilization:true,Services:true)})
        {
            var fact=await ReadOne(Row(First,("civilization",null),("services",null)),request.Civilization,request.Services);
            Check.Equal(request.Civilization,fact.CivilizationRequested);Check.Equal(request.Services,fact.ServicesRequested);
            Check.That(fact.Civilization is null&&fact.ServiceMask is null&&fact.ServicesDefaulted is null);
        }
    }

    [Test] public async Task MissingRequestedFieldsAndUnsolicitedNonNullResultsAreRejectedWithCleanup()
    {
        await RejectAndRecover(Row(First,("services",Services())));
        await RejectAndRecover(Row(First,("civilization",Civilization())));
        await RejectAndRecover(Row(First,("civilization",Civilization()),("services",null)),civilization:false,services:true);
        await RejectAndRecover(Row(First,("civilization",null),("services",Services())),civilization:true,services:false);
        await RejectAndRecover(Row(First,("civilization",Civilization())),civilization:false,services:false);
        await RejectAndRecover(Row(First,("services",Services())),civilization:false,services:false);
    }

    [Test] public async Task UnrequestedMissingOrExplicitNullFieldsNeverRetainPreviousClientResults()
    {
        var pid=Random.Shared.Next(190000000,200000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));
        var server=Task.Run(async()=>
        {
            await Serve(pid,Page("42",1,true,Row(First,("civilization",Civilization()),("services",Services()))),stop.Token,r=>InspectRequest(r,true,true));
            await Serve(pid,Page("42",1,true,Row(First)),stop.Token,r=>InspectRequest(r,false,false));
            await Serve(pid,Page("42",1,true,Row(First,("civilization",null),("services",null))),stop.Token,r=>InspectRequest(r,false,false));
        });
        var catalog=new NativeGalaxyFilterFactsCatalog(pid);
        var known=(await catalog.ReadAsync([First],stop.Token,includeCivilization:true,includeServices:true)).Rows.Single();
        Check.That(known.Civilization?.Key=="guardian"&&known.ServiceMask==513);
        for(var i=0;i<2;i++)
        {
            var fact=(await catalog.ReadAsync([First],stop.Token)).Rows.Single();
            Check.That(!fact.CivilizationRequested&&!fact.ServicesRequested&&fact.Civilization is null&&fact.ServiceMask is null&&fact.ServicesDefaulted is null);
        }
        await server;
    }

    [Test] public async Task ServiceMaskAndDefaultedMustBeCompleteExactAndConsistent()
    {
        foreach(var invalid in new[]{"{}","{\"mask\":0}","{\"mask\":null,\"defaulted\":false}","{\"mask\":-1,\"defaulted\":false}",
            "{\"mask\":1024,\"defaulted\":false}","{\"mask\":65536,\"defaulted\":false}","{\"mask\":1.5,\"defaulted\":false}",
            "{\"mask\":\"1\",\"defaulted\":false}","{\"mask\":1,\"defaulted\":true}","{\"mask\":0,\"defaulted\":null}",
            "{\"mask\":0,\"defaulted\":1}","{\"mask\":0,\"defaulted\":\"true\"}","[]","false"})
            await RejectAndRecover(Row(First,("civilization",null),("services",Check.Json(invalid))));
        foreach(var sample in new[]{(Mask:0,Defaulted:true),(Mask:0,Defaulted:false),(Mask:1023,Defaulted:false)})
        {
            var result=await ReadOne(Row(First,("services",Services(sample.Mask,sample.Defaulted))),false,true);
            Check.Equal((ushort?)sample.Mask,result.ServiceMask);Check.Equal((bool?)sample.Defaulted,result.ServicesDefaulted);
            Check.That(result.ServicesRequested&&!result.CivilizationRequested&&result.Civilization is null);
        }
    }

    [Test] public async Task CivilizationDefinitionIdsStayU64AndInvalidDefaultsCannotBecomeNone()
    {
        foreach(var sample in new[]{(Id:"9007199254740995",Key:"guardian",Defaulted:false),(Id:"9223372036854775809",Key:"human",Defaulted:false),
            (Id:"18446744073709551615",Key:"none",Defaulted:true)})
        {
            var result=await ReadOne(Row(First,("civilization",Civilization(sample.Id,sample.Key,sample.Defaulted))),true,false);
            Check.Equal(ulong.Parse(sample.Id,CultureInfo.InvariantCulture),result.Civilization!.DefinitionId);
            Check.Equal(sample.Key,result.Civilization.Key);Check.Equal(sample.Defaulted,result.Civilization.Defaulted);
            Check.That(result.CivilizationRequested&&!result.ServicesRequested&&result.ServiceMask is null);
        }
        var future=await ReadOne(Row(First,("civilization",Civilization("9007199254740995","future"))),true,false);
        Check.That(future.Civilization is{DefinitionId:9007199254740995UL,Key:null,Defaulted:false});
        foreach(var invalid in new[]{"{}","{\"definitionId\":9007199254740995,\"key\":\"human\"}","{\"definitionId\":\"18446744073709551616\",\"key\":\"none\"}",
            "{\"definitionId\":\"-1\",\"key\":\"none\"}","{\"definitionId\":\"1.0\",\"key\":\"human\"}","{\"definitionId\":null,\"key\":\"human\"}",
            "{\"definitionId\":\"1\"}","{\"definitionId\":\"1\",\"key\":1}","{\"definitionId\":\"1\",\"key\":\"none\",\"defaulted\":true}",
            "{\"definitionId\":\"18446744073709551615\",\"key\":\"human\",\"defaulted\":true}","{\"definitionId\":\"1\",\"key\":\"human\",\"defaulted\":\"true\"}","[]"})
            await RejectAndRecover(Row(First,("civilization",Check.Json(invalid)),("services",null)));
    }

    [Test] public async Task ProgressivePagesKeepEachClassifierIndependentForEachAddress()
    {
        var pid=Random.Shared.Next(190000000,200000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));
        var server=Task.Run(async()=>
        {
            await Serve(pid,Page("42",2,false,Row(First,("civilization",Civilization()),("services",null))),stop.Token,r=>InspectRequest(r,true,true));
            await Serve(pid,Page("42",2,true,Row(First+1,("civilization",null),("services",Services(0,true)))),stop.Token,r=>
            {Check.Equal("galaxy.facts.result",r.GetProperty("op").GetString());Check.Equal(1,r.GetProperty("offset").GetInt32());Check.Equal(17UL,r.GetProperty("generation").GetUInt64());});
        });
        var result=await new NativeGalaxyFilterFactsCatalog(pid).ReadAsync([First,First+1],stop.Token,includeCivilization:true,includeServices:true);await server;
        Check.That(result.Rows.All(row=>row.CivilizationRequested&&row.ServicesRequested));
        Check.That(result.Rows[0] is{SystemAddress:First,Civilization.Key:"guardian",ServiceMask:null,ServicesDefaulted:null});
        Check.That(result.Rows[1] is{Civilization:null,ServiceMask:0,ServicesDefaulted:true});Check.Equal(First+1,result.Rows[1].SystemAddress);
    }

    [Test] public async Task ChangedOriginCannotPublishMixedClassifierPagesAndNextReadStartsClean()
    {
        var pid=Random.Shared.Next(190000000,200000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));
        var server=Task.Run(async()=>
        {
            await Serve(pid,Page("42",2,false,Row(First,("civilization",Civilization()),("services",Services()))),stop.Token);
            await Serve(pid,Page("43",2,true,Row(First+1,("civilization",Civilization(key:"human")),("services",Services(1023)))),stop.Token);
            await Serve(pid,new{cancelled=true},stop.Token,r=>Check.Equal("galaxy.facts.cancel",r.GetProperty("op").GetString()));
            await Serve(pid,Page("43",1,true,Row(First,("civilization",null),("services",null))),stop.Token);
        });
        var catalog=new NativeGalaxyFilterFactsCatalog(pid);var rejected=false;
        try{await catalog.ReadAsync([First,First+1],stop.Token,includeCivilization:true,includeServices:true);}catch(InvalidDataException){rejected=true;}
        Check.That(rejected);var fresh=await catalog.ReadAsync([First],stop.Token,includeCivilization:true,includeServices:true);await server;
        Check.Equal(43L,fresh.OriginSystemAddress);Check.That(fresh.Rows.Single() is{Civilization:null,ServiceMask:null,ServicesDefaulted:null});
    }

    private static GalaxySystemData Data(GalaxyNativeFilterFact row,string account="F1",int pid=42,long origin=100)=>new()
    {NativeFilterFacts=new(account,pid,origin){Civilization=row.Civilization,CivilizationRequested=row.CivilizationRequested,
        ServiceMask=row.ServiceMask,ServicesRequested=row.ServicesRequested,ServicesDefaulted=row.ServicesDefaulted}};

    [Test] public async Task IndependentWireUpdatesPreserveUnrequestedFactsButExplicitUnknownClearsThem()
    {
        var initial=await ReadOne(Row(First,("civilization",Civilization()),("services",Services(0,true))),true,true);
        var civilizationOnly=await ReadOne(Row(First,("civilization",Civilization(key:"human"))),true,false);
        var serviceOnly=await ReadOne(Row(First,("services",Services(513))),false,true);
        var explicitUnknown=await ReadOne(Row(First,("civilization",null),("services",null)),true,true);
        var state=Data(initial).Merge(Data(civilizationOnly));
        Check.That(state.NativeFilterFacts is{CivilizationRequested:true,Civilization.Key:"human",ServicesRequested:true,ServiceMask:0,ServicesDefaulted:true});
        state=state.Merge(Data(serviceOnly));
        Check.That(state.NativeFilterFacts is{CivilizationRequested:true,Civilization.Key:"human",ServicesRequested:true,ServiceMask:513,ServicesDefaulted:false});
        state=state.Merge(Data(explicitUnknown));
        Check.That(state.NativeFilterFacts is{CivilizationRequested:true,Civilization:null,ServicesRequested:true,ServiceMask:null,ServicesDefaulted:null});
    }

    [Test] public async Task AccountProcessAndOriginResetsNeverCarryUnrequestedClassifierFacts()
    {
        var original=Data(await ReadOne(Row(First,("civilization",Civilization()),("services",Services())),true,true));
        var unrelated=await ReadOne(Row(First),false,false);
        foreach(var context in new[]{(Account:"F2",Pid:42,Origin:100L),(Account:"F1",Pid:43,Origin:100L),(Account:"F1",Pid:42,Origin:101L)})
        {
            var reset=original.ForNativeContext(context.Account,context.Pid,context.Origin);Check.That(reset.NativeFilterFacts is null);
            var replaced=original.Merge(Data(unrelated,context.Account,context.Pid,context.Origin));
            Check.That(replaced.NativeFilterFacts is{CivilizationRequested:false,Civilization:null,ServicesRequested:false,ServiceMask:null,ServicesDefaulted:null});
        }
    }
}