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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Session;
using XFEExtension.NetCore.XUnit.Attributes;

namespace XFEEDToolBox.Test;

[TestFixture]
public sealed class FlightConnectionRecoveryTests
{
    private const string Hash="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
    private static FlightConnectionBinding Binding=>new(74916,639286347139045612,Hash,"F1",8,"Solo",DateTimeOffset.UtcNow.AddMinutes(-20),1200);
    private static JsonElement Menu(long sequence,string state="FailedToConnect",bool available=true,DateTimeOffset? at=null)=>JsonSerializer.SerializeToElement(new
    {
        schema=1,name="XFEEDToolBox.Session",processId=74916,gameSha256=Hash,available,sequence,
        observedAtUnixMilliseconds=(at??DateTimeOffset.UtcNow).ToUnixTimeMilliseconds(),state,nextState=state,
        actions=Array.Empty<object>(),lastRequestId="",actionApplied=false,actionFailed=false
    });
    private static JsonElement Navigation(bool live=false,bool pose=false,bool heat=false,string hash=Hash)=>JsonSerializer.SerializeToElement(new
    {schema=1,name="XFEEDToolBox.Navigation",processId=74916,gameSha256=hash,verified=true,ready=true,inGame=live,continuousPose=pose,continuousHeat=heat});
    private static Exception Exhausted()=>new InvalidOperationException("星系内飞行数据恢复未成功,已停止并交还控制。",new InvalidOperationException("Native player telemetry unavailable or stale"));
    private static Task<Exception> Classify(Exception original,FlightConnectionBinding binding,Func<CancellationToken,Task<(JsonElement,JsonElement)>> read,
        Func<CancellationToken,Task>? verify=null,CancellationToken token=default,TimeSpan? timeout=null)
        =>FlightConnectionRecovery.ClassifyAsync(original,binding,read,verify??(_=>Task.CompletedTask),token,
            confirmation:TimeSpan.FromMilliseconds(30),timeout:timeout??TimeSpan.FromSeconds(2),pollInterval:TimeSpan.FromMilliseconds(10));

    [Test]public async Task MauveAdderExhaustedLocalRecoveryNeedsFreshAdvancingFailureAndNoLiveCockpit()
    {
        var original=Exhausted();var reads=0;var checks=0;var binding=Binding;
        var result=await Classify(original,binding,_=>Task.FromResult((Menu(++reads),Navigation())),_=>{checks++;return Task.CompletedTask;});
        Check.That(result is FlightConnectionRecoveryRequiredException);
        var confirmed=(FlightConnectionRecoveryRequiredException)result;
        Check.That(ReferenceEquals(original,confirmed.InnerException));Check.Equal(binding,confirmed.Binding);
        Check.That(reads>=2&&checks==2*reads);
        Check.That(ReferenceEquals(confirmed,FlightConnectionRecoveryRequiredException.Find(new InvalidOperationException("travel failed",confirmed))));
        Check.That(!SavingSceneRecoveryRequiredException.Present(result));
        Check.That(!NavigationRecovery.CanRetry(result,default)&&!FlightJourneyFailure.HasTransientCause(result,default));
        try{new FlightAutomationResult(NavigationRunState.Failed,result).RequireCompleted(default);}
        catch(FlightConnectionRecoveryRequiredException observed){Check.That(ReferenceEquals(result,observed));return;}
        throw new InvalidOperationException("Positive connection proof did not reach the owning trade entry.");
    }
    [Test]public async Task LiveFlightLoadingUnknownUnavailableAndWrongBuildNeverRequestAnOfficialRestart()
    {
        var original=Exhausted();
        foreach(var observation in new[]{(Menu(1,"EnteringLocation"),Navigation()),(Menu(1,"Unknown"),Navigation()),
            (Menu(1,available:false),Navigation()),(Menu(1),Navigation(live:true)),(Menu(1),Navigation(pose:true)),
            (Menu(1),Navigation(heat:true)),(Menu(1),Navigation(hash:new string('b',64))),
            (Menu(1,at:DateTimeOffset.UtcNow.AddSeconds(-2)),Navigation())})
        {
            var reads=0;var result=await Classify(original,Binding,_=>{reads++;return Task.FromResult(observation);});
            Check.That(ReferenceEquals(original,result));Check.Equal(1,reads);
        }
    }
    [Test]public async Task ARepeatedPublicationOrBudgetExhaustionCannotBecomeAConnectionProof()
    {
        var original=Exhausted();var repeated=Menu(1);var reads=0;
        var result=await Classify(original,Binding,_=>{reads++;return Task.FromResult((repeated,Navigation()));},timeout:TimeSpan.FromMilliseconds(65));
        Check.That(ReferenceEquals(original,result)&&reads>1);
        result=await Classify(original,Binding,async token=>{await Task.Delay(10000,token);return(Menu(1),Navigation());},timeout:TimeSpan.FromMilliseconds(20));
        Check.That(ReferenceEquals(original,result));
    }
    [Test]public async Task IdentityHealthManualOwnershipAndCancelledJourneysNeverProbeMenus()
    {
        foreach(var original in new Exception[]{new InvalidDataException("changed ship"),new FlightHealthPauseException(),new FlightThreatPauseException(),
            new InvalidOperationException("玩家已主动接管离站,自动航程已停止。"),new InvalidOperationException("Another controller owns local navigation"),
            new DockingHandoffException("failed original handoff",1),new AggregateException(new IOException("transport"),new InvalidDataException("PID changed")),
            new AggregateException(new IOException("transport"),new OperationCanceledException("user Stop")),
            new IOException("wrapped cancellation",new OperationCanceledException("user Stop"))})
        {
            var result=await Classify(original,Binding,_=>throw new InvalidOperationException("Must not read a menu for this failure"));
            Check.That(ReferenceEquals(original,result));
        }
        using var stop=new CancellationTokenSource();stop.Cancel();var cancelled=false;
        try{await Classify(Exhausted(),Binding,_=>throw new InvalidOperationException("Must not probe after Stop"),token:stop.Token);}
        catch(OperationCanceledException){cancelled=true;}
        Check.That(cancelled);
    }
    [Test]public async Task IdentityChangeDuringConsecutiveMenuReadsRemainsTerminal()
    {
        var checks=0;var rejected=false;
        try{await Classify(Exhausted(),Binding,_=>Task.FromResult((Menu(1),Navigation())),_=>
            {if(++checks==2)throw new InvalidDataException("process start or commander changed after read");return Task.CompletedTask;});}
        catch(InvalidDataException){rejected=true;}
        Check.That(rejected);Check.Equal(2,checks);
    }
    [Test]public async Task StopAfterTheReadStillWinsWhenAReadCallbackIgnoresItsCancellationToken()
    {
        using var stop=new CancellationTokenSource();var cancelled=false;
        try{await Classify(Exhausted(),Binding,_=>{stop.Cancel();return Task.FromResult((Menu(1),Navigation()));},token:stop.Token);}
        catch(OperationCanceledException){cancelled=true;}
        Check.That(cancelled);
    }
    [Test]public void APositiveConnectionProofCannotHideAProtectedOrUnknownCleanupFailure()
    {
        var confirmed=new FlightConnectionRecoveryRequiredException(Binding,Menu(1),Navigation(),Exhausted());
        foreach(var protectedFailure in new Exception[]{new InvalidDataException("identity"),new FlightHealthPauseException(),new FlightThreatPauseException(),
            new OperationCanceledException("user Stop"),new InvalidOperationException("Another controller owns navigation"),
            new InvalidOperationException("wrapped cancellation",new OperationCanceledException("user Stop")),
            new DockingHandoffException("handoff",1),new FlightDestinationChangedException("old","new",new IOException("stale"))})
            Check.That(FlightConnectionRecoveryRequiredException.Find(new AggregateException(confirmed,protectedFailure)) is null);
        Check.That(ReferenceEquals(confirmed,FlightConnectionRecoveryRequiredException.Find(new AggregateException(confirmed,new IOException("transport cleanup")))));
    }
    [Test]public void RecoveringAFlightKeepsItsProcessCommanderShipModeSessionAndCargoBinding()
    {
        var binding=Binding;var player=new PlayerState{CommanderId="F1",CurrentShipId=8,GameMode="Solo",LoadGameAt=binding.SessionStartedAt,CargoCount=1200,
            Ships=[new(){ShipId=8,HullHealth=1}]};
        FlightConnectionRecovery.RequireCurrentIdentity(binding,player,default);
        foreach(var changed in new[]{player with{CommanderId="F2"},player with{CurrentShipId=9},player with{GameMode="Open"},
            player with{LoadGameAt=binding.SessionStartedAt.AddSeconds(1)},player with{CargoCount=0}})
        {
            var rejected=false;try{FlightConnectionRecovery.RequireCurrentIdentity(binding,changed,default);}catch(InvalidDataException){rejected=true;}
            Check.That(rejected);
        }
        foreach(var unsafePlayer in new[]{player with{Navigation=player.Navigation with{ExternalThreat=true}},player with{Ships=[new(){ShipId=8,HullHealth=0}]}})
        {
            var rejected=false;try{FlightConnectionRecovery.RequireCurrentIdentity(binding,unsafePlayer,default);}catch(FlightSafetyPauseException){rejected=true;}
            Check.That(rejected);
        }
        var executable=Path.Combine(Path.GetTempPath(),"game","EliteDangerous64.exe");
        var client=new GameSessionTarget(binding.ProcessId,binding.StartedUtcTicks,executable,"launcher",false);
        FlightConnectionRecovery.RequireOriginalClient(binding,client,executable);
        foreach(var changed in new[]{client with{ProcessId=1},client with{StartedUtcTicks=binding.StartedUtcTicks+1},client with{Executable=executable+".other"}})
        {
            var rejected=false;try{FlightConnectionRecovery.RequireOriginalClient(binding,changed,executable);}catch(InvalidDataException){rejected=true;}
            Check.That(rejected);
        }
    }
}