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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Collections.Concurrent;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Session;
using XFEExtension.NetCore.XUnit.Attributes;

namespace XFEEDToolBox.Test;

[TestFixture]
public class InterdictionProtectionTests
{
    private static PlayerState Ship(DateTimeOffset now,long flags=(1L<<24)|(1L<<23))=>new(){CommanderId="F1",CurrentShipId=7,InGame=true,LoadGameAt=now.AddMinutes(-1),StatusAt=now,StatusReadAt=now,
        Status=new(flags,0,1,1,0,null,null,null,null,[]),Ships=[new(){ShipId=7,HullHealth=1,ConfigurationAt=now.AddMinutes(-1),Modules=[]}]};
    private static GameSessionTarget Target(DateTimeOffset now)=>new(12,now.AddMinutes(-2).UtcTicks,"fixture-game","fixture-launcher",false);
    private static InterdictionExitGuard Guard(DateTimeOffset now){var guard=new InterdictionExitGuard();guard.Reset(now.AddSeconds(-1));return guard;}
    [Test] public void FreshInterdictionTriggersWithFullHullAndNoShieldAndOnlyOncePerSession()
    {
        var now=DateTimeOffset.UtcNow;var ship=Ship(now);var guard=Guard(now);
        Check.That(guard.ObserveStatus(ship,Target(now),now));Check.That(!guard.ObserveStatus(ship,Target(now),now));
        Check.That(guard.ObserveStatus(ship with{LoadGameAt=now.AddSeconds(-1)},Target(now) with{ProcessId=13,StartedUtcTicks=now.AddSeconds(-2).UtcTicks},now));
    }
    [Test] public void GeneralDangerHeatUnshieldedBaselineAndUnderAttackAreNotInterdictionWitnesses()
    {
        var now=DateTimeOffset.UtcNow;foreach(var flags in new[]{1L<<24,(1L<<24)|(1L<<22),(1L<<24)|(1L<<20)})
        {var ship=Ship(now,flags);Check.That(!Guard(now).ObserveStatus(ship,Target(now),now));}
        var attack=new ObservedJournalEvent(1,now,Check.Event(new{timestamp=now,@event="UnderAttack",Target="You"}));
        Check.That(!Guard(now).ObserveJournal(attack,Ship(now,1L<<24),Target(now),now));
    }
    [Test] public void HistoricalOtherSessionDockedFighterAndStaleStatusCannotExitTheGame()
    {
        var now=DateTimeOffset.UtcNow;var ship=Ship(now);
        foreach(var state in new[]{ship with{Docked=true},ship with{InGame=false},ship with{StatusReadAt=now.AddSeconds(-3)},ship with{StatusAt=now.AddMinutes(-3)},
            ship with{CurrentShipId=99},ship with{LoadGameAt=now.AddMinutes(-3)},ship with{Status=ship.Status! with{Flags=(1L<<23)|(1L<<25)}}})
            Check.That(!Guard(now).ObserveStatus(state,Target(now),now));
    }
    [Test] public void FreshVictimJournalIsAFallbackAndDoesNotRepeatAStatusExit()
    {
        var now=DateTimeOffset.UtcNow;var ship=Ship(now,1L<<24);var guard=Guard(now);
        var witness=new ObservedJournalEvent(1,now,Check.Event(new{timestamp=now,@event="Interdicted",Submitted=true,Interdictor="Snout",IsPlayer=false}));
        Check.That(guard.ObserveJournal(witness,ship,Target(now),now));Check.That(!guard.ObserveStatus(Ship(now),Target(now),now));
        Check.That(!Guard(now).ObserveJournal(witness with{Data=Check.Event(new{timestamp=now.AddMinutes(-1),@event="Interdicted"})},ship,Target(now),now));
    }
    [Test] public void JournalProjectionCanTriggerBeforeTheLiveEventCallbackButAttackAloneCannot()
    {
        var now=DateTimeOffset.UtcNow;var projector=new XFEEDToolBox.Core.Navigation.NavigationProjector();
        projector.Apply(Check.Event(new{timestamp=now,@event="Interdicted",Submitted=true}));
        var ship=Ship(now,1L<<24) with{Navigation=projector.State};Check.That(Guard(now).ObserveStatus(ship,Target(now),now));
        projector.Apply(Check.Event(new{timestamp=now.AddMilliseconds(1),@event="LoadGame"}));Check.That(projector.State.InterdictedAt is null);
        projector.Apply(Check.Event(new{timestamp=now.AddMilliseconds(2),@event="UnderAttack"}));
        Check.That(!Guard(now).ObserveStatus(ship with{Navigation=projector.State},Target(now),now));
    }
    private sealed class Sessions(DateTimeOffset now):IGameSessionDriver
    {
        public bool CanRelog=>false;
        public readonly ConcurrentQueue<string> Calls=new();
        public readonly TaskCompletionSource Killed=new(TaskCreationOptions.RunContinuationsAsynchronously);
        public TaskCompletionSource? LaunchGate;
        public bool MissingLauncher,WrongProcess;
        public GameSessionTarget Expected=Target(now);
        public Task<GameSessionTarget> FindTargetAsync(CancellationToken token)=>Task.FromResult(Expected);
        public Task<GameSessionTarget> FindLaunchTargetAsync(CancellationToken token)=>throw new NotSupportedException();
        public Task ValidateExitAsync(GameSessionTarget actual,CancellationToken token){Calls.Enqueue("validate-exit");Check.Equal(Expected,actual);if(WrongProcess)throw new InvalidOperationException("process changed");return Task.CompletedTask;}
        public Task ValidateRestartAsync(GameSessionTarget actual,CancellationToken token)=>throw new NotSupportedException();
        public async Task ValidateLaunchAsync(GameSessionTarget actual,CancellationToken token){Calls.Enqueue("validate-launch");if(MissingLauncher)throw new IOException("missing launcher");if(LaunchGate is{} gate)await gate.Task.WaitAsync(token);}
        public Task TerminateAsync(GameSessionTarget actual,CancellationToken token){Calls.Enqueue("exit");Killed.TrySetResult();return Task.CompletedTask;}
        public Task<bool> HasExitedAsync(GameSessionTarget actual,CancellationToken token)=>Task.FromResult(true);
        public Task LaunchAsync(GameSessionTarget actual,CancellationToken token){Calls.Enqueue("launch");return Task.CompletedTask;}
        public Task<GameSessionTarget?> FindReplacementAsync(GameSessionTarget actual,CancellationToken token)=>Task.FromResult<GameSessionTarget?>(actual with{ProcessId=13,StartedUtcTicks=now.UtcTicks});
        public Task RelogAsync(GameSessionTarget actual,CancellationToken token)=>throw new InvalidOperationException("Interdiction must not use menu relog");
    }
    [Test] public async Task ImmediateExitPrecedesLauncherChecksAndDoesNotWaitForWorkerOrUiCallbacks()
    {
        var now=DateTimeOffset.UtcNow;var player=Ship(now);var sessions=new Sessions(now){LaunchGate=new(TaskCreationOptions.RunContinuationsAsynchronously)};
        using var releaseWorker=new ManualResetEventSlim();await using var monitor=new ShipProtectionMonitor(sessions,()=>player,()=>releaseWorker.Wait());
        monitor.Progress+=_=>throw new InvalidOperationException("busy UI fixture");await monitor.EnableAsync();
        var trigger=Task.Run(()=>monitor.ObserveInterdiction(player,now));
        try{await sessions.Killed.Task.WaitAsync(TimeSpan.FromSeconds(2));Check.That(!trigger.IsCompleted);Check.Equal("validate-exit,exit",string.Join(',',sessions.Calls.Take(2)));}
        finally{releaseWorker.Set();sessions.LaunchGate.TrySetResult();}
        Check.That(await trigger);await monitor.Completion.WaitAsync(TimeSpan.FromSeconds(2));
        Check.Equal(ShipProtectionReason.Interdicted,monitor.LastTrigger!.Reason);Check.Equal(13,monitor.RestartTarget!.ProcessId);
        Check.That(!monitor.ObserveInterdiction(player,now));Check.Equal(1,sessions.Calls.Count(call=>call=="exit"));
    }
    [Test] public async Task MissingLauncherStillExitsButAChangedProcessIdentityCannotBeTerminated()
    {
        foreach(var wrongProcess in new[]{false,true})
        {
            var now=DateTimeOffset.UtcNow;var player=Ship(now);var sessions=new Sessions(now){MissingLauncher=true,WrongProcess=wrongProcess};
            await using var monitor=new ShipProtectionMonitor(sessions,()=>player,()=>{});await monitor.EnableAsync();Check.That(monitor.ObserveInterdiction(player,now));await monitor.Completion;
            Check.Equal(!wrongProcess,sessions.Calls.Contains("exit"));Check.That(!sessions.Calls.Contains("launch"));
        }
    }
    [Test] public async Task ExplicitObserverOnlyModeDoesNotTerminateAndDefaultPolicyDoes()
    {
        var now=DateTimeOffset.UtcNow;var player=Ship(now);var sessions=new Sessions(now);
        await using(var observer=new ShipProtectionMonitor(sessions,()=>player,()=>{},automaticExit:false))
        {await observer.EnableAsync();Check.That(!observer.ObserveInterdiction(player,now));Check.Equal(0,sessions.Calls.Count);}
        await using var active=new ShipProtectionMonitor(sessions,()=>player,()=>{});await active.EnableAsync();Check.That(active.ExitOnInterdiction);Check.That(active.ObserveInterdiction(player,now));await active.Completion;Check.That(sessions.Calls.Contains("exit"));
    }
}