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.Execution;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;

namespace XFEEDToolBox.Test;

[TestFixture]
public class EmptyStationValidationTests
{
    private static readonly DateTimeOffset Start=DateTimeOffset.FromUnixTimeMilliseconds(1700000000000);
    private static readonly string Hash=new('a',64),Owner=new('b',32);
    private static readonly SystemNavigationTarget Target=new(10,20,"Target",SystemTargetKind.Station,2);
    private static EmptyStationValidationPlan Plan=>new(Check.Station(1),Check.Station(2),Hash,5,192,5000,4);
    private static EmptyStationValidationCapabilities Caps=>new(true,true,true,true,true,true,true,false);
    private static SystemFlightFrame Flight(DateTimeOffset at,LocalFlightMode mode=LocalFlightMode.Supercruise,ulong entity=12,ulong world=24)=>new()
    {Sequence=1,ObservedAt=at,GameSha256=Hash,ShipEntity=entity,ReferenceWorld=world,SelectedTarget=Target,SystemAddress=10,InGame=true,GeometryVerified=true,Mode=mode,
        HeatPercent=45,TargetDistance=6000,SupercruiseAssist=NativeAssistState.Disabled,Forward=new(0,0,1),Up=new(0,1,0)};
    private static CruiseOverchargeObservation Observation(DateTimeOffset at,CruiseOverchargeState state=CruiseOverchargeState.Active)=>new()
    {ObservedAt=at,ShipEntity=12,ReferenceWorld=24,GameSha256=Hash,OriginalInterfacesVerified=true,OriginalCanEngage=true,NativeFlightConstraintsVerified=true,NativeGeometryVerified=true,State=state,HeatPercent=45,HeatRisePercentPerSecond=1,
        FuelMainTonnes=31,FuelCapacityTonnes=32,FuelBurnTonnesPerSecond=.1,FuelRateUpperTonnesPerSecond=.3};
    private static OriginalCruiseFuelRange Range(DateTimeOffset at)=>new(12,24,at,true,CruiseFuelRangeScope.OriginalScoConsumptionCurve,.05,.3);
    private static OriginalOverchargeReceipt Command(DateTimeOffset at,bool enable,long sequence=1,bool original=true)=>new(Guid.NewGuid().ToString("N"),11,111,Hash,Owner,1,12,24,sequence,at,at,at.AddMilliseconds(250),enable,true,original){CommandGeneration=sequence};
    private static EmptyStationFlightProof Proof()=>new(11,111,Hash,2,4);
    private static void CompleteSco(EmptyStationFlightProof proof)
    {
        proof.ObserveOriginalOverchargeReceipt(Command(Start,true),Start);
        for(var index=1;index<=5;index++)
        {var at=Start.AddMilliseconds(index*20);proof.ObserveCruise(Flight(at),Observation(at),Range(at),false,at);}
        var stopped=Start.AddMilliseconds(120);proof.ObserveOriginalOverchargeReceipt(Command(stopped,false,20),stopped);
        var idle=Start.AddMilliseconds(140);proof.ObserveCruise(Flight(idle),Observation(idle,CruiseOverchargeState.Cooling),Range(idle),false,idle);
    }
    private static SystemFlightFrame Grant(EmptyStationFlightProof proof)
    {
        var at=Start.AddMilliseconds(200);var frame=Flight(at,LocalFlightMode.Normal,13,25) with{Clearance=new(2,DockingPermission.Granted,4,at,at.AddMinutes(1))};
        proof.ObserveDockingGrant(frame,false,at);return frame;
    }
    private static JsonElement Reset(SystemFlightFrame frame,double throttle=0,bool original=true,bool aiWritten=false)
    {
        var at=frame.ObservedAt.AddMilliseconds(10);
        return JsonSerializer.SerializeToElement(new{applied=true,pending=false,failed=false,expired=false,originalResetCalled=original,entityId=frame.ShipEntity.ToString(),marketId=2,
            observedAtUnixMilliseconds=at.ToUnixTimeMilliseconds(),accumulatorBefore=0,accumulatorAfter=0,sourceAfter=0,actualThrottleAfter=throttle,controllerThrottleAfter=0,
            dockingStartRequested=true,originalDockingStartCalled=true,dockingStartAtUnixMilliseconds=at.AddMilliseconds(1).ToUnixTimeMilliseconds(),expiresAtUnixMilliseconds=frame.ObservedAt.AddMilliseconds(250).ToUnixTimeMilliseconds(),
            gameAccumulatorWrittenBySdk=false,gameOutputWrittenBySdk=false,activeFlagWritten=false,aiWritten});
    }
    private static void Complete(EmptyStationFlightProof proof)
    {
        CompleteSco(proof);var frame=Grant(proof);proof.ObserveOriginalDockingReset(Reset(frame),frame,Start.AddMilliseconds(211));
        var at=Start.AddMilliseconds(230);proof.ObserveAi(frame with{ObservedAt=at,DockingAssist=NativeAssistState.Engaged,Throttle=.7},at);
    }
    private sealed class Backend:IEmptyStationValidationBackend
    {
        public readonly List<string> Calls=[];
        public EmptyStationValidationCapabilities Capabilities=Caps;
        public DateTimeOffset At=Start;
        public bool Docked=true,Unknown,Sca,Healthy=true;
        public long Market=1,Credits=10000;
        public int ProcessId=11,Observer=33;
        public Action<EmptyStationFlightProof>? FlightEvidence=Complete;
        public Action? OnTravel;
        public Action? OnRecover;
        public EmptyStationValidationSnapshot Snapshot()=>new(ProcessId,111,Hash,"Commander",5,192,0,Credits,true,Docked,Market,5,Docked,Docked,Healthy,false,Unknown,Sca,false,Observer,222,true,At,At);
        public Task<EmptyStationValidationPreflight> InspectDockedAsync(EmptyStationValidationPlan p,CancellationToken t){Calls.Add("inspect");return Task.FromResult(new EmptyStationValidationPreflight(Capabilities,Snapshot()));}
        public Task<EmptyStationServiceEvidence> MaintainAsync(long market,CancellationToken t)
        {Calls.Add("service:"+market);var opening=Credits;var expense=market==1?10:20;Credits-=expense;return Task.FromResult(new EmptyStationServiceEvidence(market,opening,Credits,expense,true,false));}
        public Task NavigateAsync(Station destination,EmptyStationFlightProof proof,CancellationToken t)
        {Calls.Add("navigate");Docked=false;OnTravel?.Invoke();t.ThrowIfCancellationRequested();FlightEvidence?.Invoke(proof);At=Start.AddMilliseconds(250);Docked=true;Market=destination.MarketId;return Task.CompletedTask;}
        public Task<EmptyStationValidationSnapshot> ReadSnapshotAsync(CancellationToken t){Calls.Add("read");return Task.FromResult(Snapshot());}
        public Task ReleaseNavigationAsync(){Calls.Add("release");return Task.CompletedTask;}
        public Task RecoverSoloSameProcessAsync(int pid,long ticks,CancellationToken t){Calls.Add("recover");Check.Equal(11,pid);Check.Equal(111L,ticks);OnRecover?.Invoke();return Task.CompletedTask;}
    }
    [Test]public async Task MissingNativeCapabilityRefusesWhileDockedBeforeMaintenanceOrNavigation()
    {
        foreach(var caps in new[]{Caps with{OriginalScoCommands=false},Caps with{OriginalScoFuelBounds=false},Caps with{OriginalInterdictionEscape=false},Caps with{OriginalThrottleReset=false},
            Caps with{OriginalDockingAi=false},Caps with{SameProcessSoloRecovery=false},Caps with{OriginalServerRead=false},Caps with{UsesGameSupercruiseAssist=true}})
        {
            using var temp=new TemporaryDirectory();var backend=new Backend{Capabilities=caps};var result=await new EmptyStationValidationWorkflow(backend,()=>backend.At).RunAsync(Plan,temp.File("job"));
            Check.That(!result.Passed&&backend.Docked);Check.Equal("inspect",string.Join(',',backend.Calls));
        }
    }
    [Test]public async Task OneBackendCompletesOnlyWithActualScoZeroAiDockedServicesAndWallet()
    {
        using var temp=new TemporaryDirectory();var backend=new Backend();var result=await new EmptyStationValidationWorkflow(backend,()=>backend.At).RunAsync(Plan,temp.File("job"));
        Check.That(!backend.Capabilities.OriginalScoControlQualified&&backend.Capabilities.AvailableForEmptyTrial);
        Check.That(result.Passed&&result.ActualDocked&&result.Maintained&&!result.RecoveredSameProcess&&result.Flight.ActiveSamples==5&&!result.Flight.NonzeroResetReproduced);
        Check.Equal("inspect,service:1,read,navigate,read,service:2,read,release",string.Join(',',backend.Calls));Check.Equal(9970L,result.FinalSnapshot!.Credits);
    }
    [Test]public async Task CompletedDockingWithoutScoEvidenceCannotPassAndStillFinishesStationServices()
    {
        using var temp=new TemporaryDirectory();var backend=new Backend{FlightEvidence=null};var result=await new EmptyStationValidationWorkflow(backend,()=>backend.At).RunAsync(Plan,temp.File("job"));
        Check.That(!result.Passed&&result.ActualDocked&&result.Maintained&&!backend.Calls.Contains("recover"));Check.Equal(1,backend.Calls.Count(x=>x=="navigate"));
    }
    [Test]public async Task UnknownMoneyOrEnabledScaCannotStartEvenWithAllCapabilities()
    {
        foreach(var backend in new[]{new Backend{Unknown=true},new Backend{Sca=true},new Backend{Healthy=false}})
        {
            using var temp=new TemporaryDirectory();var result=await new EmptyStationValidationWorkflow(backend,()=>backend.At).RunAsync(Plan,temp.File("job"));
            Check.That(!result.Passed);Check.Equal("inspect",string.Join(',',backend.Calls));
        }
    }
    [Test]public async Task OneOrdinaryFailureUsesSameProcessRecoveryWithoutRepeatingTravel()
    {
        using var temp=new TemporaryDirectory();var backend=new Backend{OnTravel=()=>throw new IOException("Original flight failure")};
        var result=await new EmptyStationValidationWorkflow(backend,()=>backend.At).RunAsync(Plan,temp.File("job"));
        Check.That(!result.Passed&&result.RecoveredSameProcess);Check.Equal(1,backend.Calls.Count(x=>x=="navigate"));Check.Equal(1,backend.Calls.Count(x=>x=="recover"));Check.Equal(1,backend.Calls.Count(x=>x=="release"));
    }
    [Test]public async Task UserCancellationDoesNotEnterRecoveryOrStartAnotherFlight()
    {
        using var temp=new TemporaryDirectory();using var stop=new CancellationTokenSource();var backend=new Backend{OnTravel=stop.Cancel};
        var result=await new EmptyStationValidationWorkflow(backend,()=>backend.At).RunAsync(Plan,temp.File("job"),stop.Token);
        Check.That(!result.Passed&&!result.RecoveredSameProcess&&!backend.Calls.Contains("recover"));Check.Equal(1,backend.Calls.Count(x=>x=="navigate"));
    }
    [Test]public async Task AReplacementClientOrObserverCannotBeCalledSameProcessRecovery()
    {
        foreach(var replaceObserver in new[]{false,true})
        {
            using var temp=new TemporaryDirectory();var backend=new Backend{OnTravel=()=>throw new IOException("Original flight failure")};
            backend.OnRecover=()=>{if(replaceObserver)backend.Observer=34;else backend.ProcessId=12;};
            var result=await new EmptyStationValidationWorkflow(backend,()=>backend.At).RunAsync(Plan,temp.File("job"));
            Check.That(!result.Passed&&!result.RecoveredSameProcess&&result.RecoveryFailure is not null);Check.Equal(1,backend.Calls.Count(x=>x=="navigate"));
        }
    }
    [Test]public async Task PreviousAttemptIsPreservedAndNoBackendIsTouched()
    {
        using var temp=new TemporaryDirectory();var path=temp.File("job");Directory.CreateDirectory(path);await File.WriteAllTextAsync(Path.Combine(path,"result.json"),"original");var backend=new Backend();
        var result=await new EmptyStationValidationWorkflow(backend,()=>backend.At).RunAsync(Plan,path);
        Check.That(!result.Passed&&backend.Calls.Count==0);Check.Equal("original",await File.ReadAllTextAsync(Path.Combine(path,"result.json")));
    }
    [Test]public void AcknowledgementsAndStartingStateCannotSubstituteForActualActiveSamples()
    {
        var proof=Proof();proof.ObserveOriginalOverchargeReceipt(Command(Start,true),Start);
        var at=Start.AddMilliseconds(10);proof.ObserveCruise(Flight(at),Observation(at,CruiseOverchargeState.Starting),Range(at),false,at);
        proof.ObserveOriginalOverchargeReceipt(Command(at.AddMilliseconds(1),false,2),at.AddMilliseconds(1));at=at.AddMilliseconds(2);
        proof.ObserveCruise(Flight(at),Observation(at,CruiseOverchargeState.Idle),Range(at),false,at);
        Check.That(!proof.OverchargeCompleted);Check.Throws<InvalidDataException>(proof.RequireComplete);
    }
    [Test]public void MissingFuelLowerBoundsOrScaOrUnsafeHeatRefuseEvidence()
    {
        var at=Start.AddMilliseconds(20);
        foreach(var range in new[]{Range(at) with{Verified=false},Range(at) with{Scope=CruiseFuelRangeScope.Unverified},Range(at) with{LowerTonnesPerSecond=double.NaN},Range(at) with{LowerTonnesPerSecond=.4}})
        {var proof=Proof();proof.ObserveOriginalOverchargeReceipt(Command(Start,true),Start);Check.Throws<InvalidDataException>(()=>proof.ObserveCruise(Flight(at),Observation(at),range,false,at));}
        foreach(var pair in new[]{(true,45d),(false,100d)})
        {var proof=Proof();proof.ObserveOriginalOverchargeReceipt(Command(Start,true),Start);Check.Throws<InvalidDataException>(()=>proof.ObserveCruise(Flight(at),Observation(at) with{HeatPercent=pair.Item2},Range(at),pair.Item1,at));}
        var unknown=Proof();unknown.ObserveOriginalOverchargeReceipt(Command(Start,true),Start);
        Check.Throws<InvalidDataException>(()=>unknown.ObserveCruise(Flight(at),Observation(at),Range(at),null,at));
    }
    [Test]public void WrongExpiredOrRenewalOnlyCommandsCannotEstablishOriginalControl()
    {
        foreach(var command in new[]{Command(Start,true) with{ProcessId=99},Command(Start,true) with{ProcessStartedUtcTicks=112},Command(Start,true) with{AppliedAt=Start.AddMilliseconds(251)},Command(Start,true,original:false)})
            Check.Throws<InvalidDataException>(()=>Proof().ObserveOriginalOverchargeReceipt(command,command.AppliedAt>Start.AddMilliseconds(250)?command.AppliedAt:Start.AddMilliseconds(10)));
        var proof=Proof();var first=Command(Start,true);proof.ObserveOriginalOverchargeReceipt(first,Start);proof.ObserveOriginalOverchargeReceipt(first,Start);
        Check.Equal(1,proof.Snapshot().OriginalCommands.Count);Check.Throws<InvalidDataException>(()=>proof.ObserveOriginalOverchargeReceipt(first with{Enable=false},Start));
    }
    [Test]public void AtcCannotRunBeforeVerifiedScoStopAndSourceZeroCannotBeFaked()
    {
        Check.Throws<InvalidOperationException>(()=>Grant(Proof()));
        foreach(var invalid in new[]{0,1,2})
        {
            var proof=Proof();CompleteSco(proof);var frame=Grant(proof);
            var receipt=Reset(frame,throttle:invalid==0?.75:0,original:invalid!=1,aiWritten:invalid==2);
            Check.Throws<InvalidDataException>(()=>proof.ObserveOriginalDockingReset(receipt,frame,Start.AddMilliseconds(211)));
        }
    }
    [Test]public void ReobservingGrantCannotExtendTwoSecondAiWindow()
    {
        var proof=Proof();CompleteSco(proof);var frame=Grant(proof);proof.ObserveOriginalDockingReset(Reset(frame),frame,Start.AddMilliseconds(211));
        var later=Start.AddMilliseconds(2201);var repeated=frame with{ObservedAt=later,DockingAssist=NativeAssistState.Engaged,Clearance=frame.Clearance! with{ObservedAt=later}};
        proof.ObserveDockingGrant(repeated,false,later);Check.Throws<InvalidDataException>(()=>proof.ObserveAi(repeated,later));
    }
}