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.Session;
using XFEExtension.NetCore.XUnit.Attributes;

namespace XFEEDToolBox.Test;

[TestFixture]
public sealed class StartupConnectionRecoveryTests
{
    private static readonly DateTimeOffset Now=DateTimeOffset.UtcNow;
    private static readonly GameSessionTarget Client=new(10,100,"game","launcher",false);
    private static NativeSessionFrame Failure(long sequence=1,double seconds=0)=>new(sequence,Now.AddSeconds(seconds),"FailedToConnect","FailedToConnect",[]);
    private static TimeSpan Immediate(int count)=>TimeSpan.Zero;
    [Test]public void FailureNeedsAdvancingFreshStableFramesAndNeverGuessesLoadingOrUnknownStates()
    {
        var observer=new NativeConnectionFailureObserver();Check.That(!observer.Observe(Failure(),Now));
        Check.That(!observer.Observe(Failure(),Now.AddMilliseconds(100)));
        for(var index=1;index<20;index++)Check.That(!observer.Observe(Failure(index+1,index*.1),Now.AddSeconds(index*.1)));
        Check.That(observer.Observe(Failure(21,2),Now.AddSeconds(2)));
        foreach(var state in new[]{"EnteringLocation","SaveGameOnEnterLocation","MultiplayerLoad","Unknown",""})
        {
            var next=new NativeConnectionFailureObserver{ConfirmationPeriod=TimeSpan.FromMilliseconds(1)};
            Check.That(!next.Observe(Failure() with{State=state,NextState=state},Now));
            Check.That(!next.Observe(Failure(2,.01) with{State=state,NextState=state},Now.AddMilliseconds(10)));
        }
        foreach(var invalid in new[]{Failure() with{Available=false},Failure() with{NextState="MainMenu"},Failure() with{ObservedAt=Now.AddSeconds(-2)}})
            Check.That(!new NativeConnectionFailureObserver{ConfirmationPeriod=TimeSpan.Zero}.Observe(invalid,Now));
    }
    private static JsonElement Menu(string state="FailedToConnect",bool available=true)=>JsonSerializer.SerializeToElement(new
    {schema=1,name="XFEEDToolBox.Session",processId=10,gameSha256="hash",available,sequence=1,observedAtUnixMilliseconds=Now.ToUnixTimeMilliseconds(),state,nextState=state,actions=Array.Empty<string>()});
    private static JsonElement Navigation(bool inGame=false,bool pose=false,bool heat=false,string hash="hash")=>JsonSerializer.SerializeToElement(new
    {schema=1,name="XFEEDToolBox.Navigation",processId=10,gameSha256=hash,verified=true,inGame,continuousPose=pose,continuousHeat=heat,ready=true});
    [Test]public void AnInitializedServiceCanBeFailedWhileAnyLiveFlightOrWrongIdentityRefusesTermination()
    {
        Check.That(NativeSessionStartupReadiness.FailedWithoutLiveFlight(Menu(),Navigation(),10,"hash",Now));
        foreach(var navigation in new[]{Navigation(inGame:true),Navigation(pose:true),Navigation(heat:true),Navigation(hash:"other")})
            Check.That(!NativeSessionStartupReadiness.FailedWithoutLiveFlight(Menu(),navigation,10,"hash",Now));
        foreach(var menu in new[]{Menu("EnteringLocation"),Menu(available:false),Menu("Unknown")})
            Check.That(!NativeSessionStartupReadiness.FailedWithoutLiveFlight(menu,Navigation(),10,"hash",Now));
        Check.That(!NativeSessionStartupReadiness.FailedWithoutLiveFlight(Menu(),Navigation(),10,"hash",Now.AddSeconds(1)));
    }
    [Test]public void ALoadGameEventAndOldCargoLocationNeverProveTheNewCockpitReady()
    {
        var frame=new NativeSessionFrame(1,Now,"InGame","InGame",[]);
        var player=new PlayerState{InGame=true,LoadGameAt=Now,LocationAt=Now.AddMinutes(-1),CargoAt=Now.AddMinutes(-1),CargoReliable=true,SystemName="Old station",Navigation=new(){CurrentSystemAddress=42}};
        Check.That(!NativeSessionStartupReadiness.Ready(player,frame,Now,Now.AddMinutes(-1)));
        player=player with{LocationAt=Now,CargoAt=Now};Check.That(NativeSessionStartupReadiness.Ready(player,frame,Now,Now.AddMinutes(-1)));
        Check.That(!NativeSessionStartupReadiness.Ready(player,Failure(),Now,Now.AddMinutes(-1)));
        Check.That(!NativeSessionStartupReadiness.Ready(player with{CargoReliable=false},frame,Now));
    }
    [Test]public async Task OwnedConnectionFailureRetriesTheWorkflowWithoutReplayingAnyOtherException()
    {
        var attempts=0;var recoveries=0;
        await StartupConnectionRecovery.RunAsync((own,token)=>
        {own(Client);if(++attempts==1)throw new NativeSessionConnectionFailedException(Failure(),Client);return Task.CompletedTask;},
            (client,error,owned,count,token)=>{Check.That(owned&&client==Client&&count==1);recoveries++;return Task.CompletedTask;},default,retryDelay:Immediate);
        Check.Equal(2,attempts);Check.Equal(1,recoveries);
        foreach(var error in new Exception[]{new IOException("unknown I/O"),new TimeoutException("normal loading"),new InvalidDataException("identity changed")})
        {
            var count=0;try{await StartupConnectionRecovery.RunAsync((own,token)=>{count++;own(Client);throw error;},
                (_,_,_,_,_)=>throw new Exception("Must not recover"),default,retryDelay:Immediate);}catch(Exception caught){Check.That(ReferenceEquals(error,caught));}
            Check.Equal(1,count);
        }
    }
    [Test]public async Task ExistingClientsNeedExplicitAdoptionAndTheRecoveryBudgetIsFinite()
    {
        var attempts=0;var recoveries=0;
        try{await StartupConnectionRecovery.RunAsync((_,_)=>{attempts++;throw new NativeSessionConnectionFailedException(Failure(),Client);},
            (_,_,_,_,_)=>{recoveries++;return Task.CompletedTask;},default,retryDelay:Immediate);}catch(NativeSessionConnectionFailedException){}
        Check.Equal(1,attempts);Check.Equal(0,recoveries);attempts=0;
        try{await StartupConnectionRecovery.RunAsync((_,_)=>{attempts++;throw new NativeSessionConnectionFailedException(Failure(),Client);},
            (_,_,owned,_,_)=>{Check.That(!owned);recoveries++;return Task.CompletedTask;},default,allowVerifiedExisting:true,retryDelay:Immediate);}catch(StartupConnectionRecoveryExhaustedException){}
        Check.Equal(3,attempts);Check.Equal(2,recoveries);
    }
    [Test]public async Task CancellationAndTheGlobalDeadlineAlwaysWinBeforeAnotherRestart()
    {
        using var stop=new CancellationTokenSource();var recoveries=0;
        try{await StartupConnectionRecovery.RunAsync((own,_)=>{own(Client);stop.Cancel();throw new NativeSessionConnectionFailedException(Failure(),Client);},
            (_,_,_,_,_)=>{recoveries++;return Task.CompletedTask;},stop.Token,retryDelay:Immediate);}catch(NativeSessionConnectionFailedException){}
        Check.Equal(0,recoveries);
        var timed=false;
        try{await StartupConnectionRecovery.RunAsync(async (_,token)=>await Task.Delay(10000,token),(_,_,_,_,_)=>Task.CompletedTask,
            default,totalTimeout:TimeSpan.FromMilliseconds(20));}catch(TimeoutException){timed=true;}
        Check.That(timed);
    }
    [Test]public async Task LongRunningSupervisionOnlyWaitsForAnExhaustedTypedConnectionFailure()
    {
        var attempts=0;var waits=0;
        await StartupConnectionRecovery.SuperviseAsync(_=>
        {if(++attempts==1)throw new StartupConnectionRecoveryExhaustedException(new(Failure(),Client),2);return Task.CompletedTask;},
            (_,count,_)=>{waits++;Check.Equal(1,count);return Task.CompletedTask;},default);
        Check.Equal(2,attempts);Check.Equal(1,waits);
        foreach(var error in new Exception[]{new TimeoutException("unknown loading"),new InvalidDataException("identity"),new NativeSessionConnectionFailedException(Failure())})
        {
            try{await StartupConnectionRecovery.SuperviseAsync(_=>throw error,(_,_,_)=>throw new Exception("Must not supervise this failure"),default);}
            catch(Exception caught){Check.That(ReferenceEquals(caught,error));}
        }
        using var stop=new CancellationTokenSource();var cancelled=false;
        try{await StartupConnectionRecovery.SuperviseAsync(_=>throw new StartupConnectionRecoveryExhaustedException(new(Failure(),Client),2),
            async(_,_,token)=>{stop.Cancel();await Task.Delay(1000,token);},stop.Token);}catch(OperationCanceledException){cancelled=true;}
        Check.That(cancelled);
    }
    [Test]public async Task AChangedMenuIdentityOrCancellationAfterEvidenceWritesNeverTerminatesTheClient()
    {
        foreach(var cause in new[]{"changed-menu","identity","cancel","unchanged"})
        {
            using var stop=new CancellationTokenSource();var terminations=0;var rejected=false;
            try
            {
                await StartupConnectionRecovery.TerminateWithFreshEvidenceAsync(_=>
                {
                    if(cause=="identity")throw new InvalidDataException("PID/start/hash changed");
                    if(cause=="cancel")stop.Cancel();
                    return Task.FromResult(cause!="changed-menu");
                },_=>{terminations++;return Task.CompletedTask;},stop.Token);
            }
            catch(InvalidDataException)when(cause is "changed-menu" or "identity"){rejected=true;}
            catch(OperationCanceledException)when(cause=="cancel"){rejected=true;}
            Check.Equal(cause=="unchanged"?1:0,terminations);Check.Equal(cause!="unchanged",rejected);
        }
    }
}