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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

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

namespace XFEEDToolBox.Test;

[TestFixture]
public class ExecutionTests
{
    private static RoutePlan Plan => new()
    {
        Request = Check.Request with { MaxLegs = 1 },
        Legs = [new() { Number = 1, From = Check.Station(1), To = Check.Station(2, 10), Orders = [new("gold", "Gold", 3, 100, 180)], EstimatedMinutes = 5 }]
    };
    private static PlayerState Player => new() { Ship = "Type9", InGame = true, Docked = true, MarketId = 1, Credits = 10000, CreditsReliable = true, CargoCapacity = 10, StatusAt = DateTimeOffset.UtcNow };
    private static CommodityQuote Quote(long station, string key) => Check.Quote(station, key, station == 1 ? 100 : 200, station == 1 ? 90 : 180);
    [Test]public async Task LiveLargeHullRejectsAStaleSmallShipPlanBeforeAnyNativeDriverCall()
    {
        var driver=new Driver();var runner=new TradeRunner(()=>Player,Quote);var original=Plan;
        var unsafePlan=original with{Request=original.Request with{RequiredPad=LandingPad.Small,ActualShipType="SideWinder"},
            Legs=[original.Legs[0] with{To=original.Legs[0].To with{MaxLandingPad=LandingPad.Medium}}]};
        await runner.RunAsync(unsafePlan,ExecutionMode.NativeBridge,driver);
        Check.Equal(RunState.Failed,runner.State);Check.Equal(0,driver.Calls);
    }
    private sealed class Driver : ITradeCommandDriver,IConfirmedDockedServiceDriver,ITradeFlightSafetyScope
    {
        public bool FlightSafetyActive { get; set; }
        public int Calls; public bool Released; public Func<TradeAction, Task>? OnAction;
        public List<DockedServiceRequest> Services=[];
        public Func<DockedServiceRequest,CancellationToken,Task>? OnServices;
        public event Action<ConfirmedDockedServiceReceipt>? ServiceConfirmed;
        public void ConfirmService(long market,string action,long amount) => ServiceConfirmed?.Invoke(new(Guid.NewGuid().ToString("N"),action,market,10000,10000-amount,DateTimeOffset.UtcNow));
        public Task PreflightAsync(CancellationToken token) { Calls++; return Task.CompletedTask; }
        public async Task ServiceDockedAsync(DockedServiceRequest request,Func<CancellationToken,Task> beforeInput,CancellationToken token)
        {await beforeInput(token);Services.Add(request);if(OnServices is not null)await OnServices(request,token);}
        public async Task ExecuteAsync(TradeAction action, Func<CancellationToken, Task> beforeInput, CancellationToken token) { await beforeInput(token); Calls++; if (OnAction != null) await OnAction(action); }
        public Task ReleaseAllAsync() { Released = true; return Task.CompletedTask; }
    }

    [Test] public async Task SimulationDoesNotSendGameInput()
    {
        var driver = new Driver(); var runner = new TradeRunner(() => Player with { InGame = false, InDanger = true }, Quote);
        await runner.RunAsync(Plan, ExecutionMode.Simulation, driver);
        Check.Equal(0, driver.Calls); Check.Equal(RunState.Completed, runner.State); Check.That(driver.Released);
    }
    [Test]public async Task NativeFlightOwnsGenericHeatWarningsOnlyWhileItsSafetyScopeIsActive()
    {
        var driver=new Driver {FlightSafetyActive=true};var player=Player;var observed=false;
        var runner=new TradeRunner(()=>player,Quote);
        runner.Progress+=progress=>
        {
            if(observed||!progress.Message.StartsWith("开始执行"))return;
            observed=true;player=player with{InDanger=true};runner.ObservePlayer(player);
            Check.Equal(RunState.Running,runner.State);
            driver.FlightSafetyActive=false;runner.ObservePlayer(player);Check.Equal(RunState.Paused,runner.State);
            driver.FlightSafetyActive=true;runner.Resume();Check.Equal(RunState.Running,runner.State);
            player=player with{Navigation=player.Navigation with{ExternalThreat=true}};
            runner.ObservePlayer(player);Check.Equal(RunState.Paused,runner.State);
            Check.Throws<InvalidOperationException>(runner.Resume);runner.Stop();
        };
        await runner.RunAsync(Plan,ExecutionMode.NativeBridge,driver);
        Check.That(observed);Check.Equal(RunState.Stopped,runner.State);Check.That(driver.Released);
    }
    [Test]public async Task JournalGuidanceCannotDelegateItsDangerGuardToADriver()
    {
        var driver=new Driver {FlightSafetyActive=true};var player=Player;var observed=false;
        var runner=new TradeRunner(()=>player,Quote);
        runner.Progress+=progress=>
        {
            if(observed||!progress.Message.StartsWith("开始执行"))return;
            observed=true;player=player with{InDanger=true};runner.ObservePlayer(player);
            Check.Equal(RunState.Paused,runner.State);Check.Throws<InvalidOperationException>(runner.Resume);runner.Stop();
        };
        await runner.RunAsync(Plan,ExecutionMode.JournalGuide,driver);
        Check.That(observed);Check.Equal(RunState.Stopped,runner.State);Check.Equal(0,driver.Calls);
    }
    [Test] public async Task MacroModeRequiresCorrectStartingStation()
    {
        var driver = new Driver(); var runner = new TradeRunner(() => Player with { MarketId = 99 }, Quote);
        await runner.RunAsync(Plan, ExecutionMode.NativeBridge, driver);
        Check.Equal(0, driver.Calls); Check.Equal(RunState.Failed, runner.State);
    }
    [Test] public async Task MacroModeRequiresReliableCredits()
    {
        var driver = new Driver(); var runner = new TradeRunner(() => Player with { CreditsReliable = false }, Quote);
        await runner.RunAsync(Plan, ExecutionMode.NativeBridge, driver); Check.Equal(0, driver.Calls);
    }
    [Test] public async Task MissingCargoSpaceBlocksExecution()
    {
        var driver = new Driver(); var runner = new TradeRunner(() => Player with { CargoCount = 8 }, Quote);
        await runner.RunAsync(Plan, ExecutionMode.NativeBridge, driver); Check.Equal(0, driver.Calls);
    }
    private static PlayerState OccupiedPlayer=>Player with{CommanderId="occupied-test",CurrentShipId=7,LoadGameAt=DateTimeOffset.UtcNow.AddMinutes(-1),
        CargoReliable=true,CargoCount=2,Inventory=[new("gold","Gold",2)]};
    [Test]public async Task NativeOccupiedPlanKeepsTheEmptyCargoAdmissionGuardBeforeAnyDriverCall()
    {
        var driver=new Driver();var runner=new TradeRunner(()=>OccupiedPlayer,Quote);RunProgress? last=null;runner.Progress+=value=>last=value;
        await runner.RunAsync(Plan with{Request=Plan.Request with{OccupiedCargo=2}},ExecutionMode.NativeBridge,driver);
        Check.Equal(RunState.Failed,runner.State);Check.Equal(0,driver.Calls);Check.That(last!.Message.Contains("空货舱"));
    }
    [Test]public async Task OccupiedJournalPlanUsesFreeCapacityAndLeavesTheOriginalSameCommodityUntouched()
    {
        var player=OccupiedPlayer;var driver=new Driver();var runner=new TradeRunner(()=>player,Quote);long sequence=0;
        void Emit(object value)=>runner.Observe(new(++sequence,DateTimeOffset.UtcNow,Check.Event(value)));
        runner.Progress+=value=>
        {
            if(value.Message.StartsWith("买入 ")&&value.Message.Contains("等待实际交易确认"))
            {player=player with{Credits=9700,CargoCount=5,Inventory=[new("gold","Gold",5)]};Emit(new{@event="MarketBuy",MarketID=1,Type="gold",Count=3,TotalCost=300});}
            else if(value.Message.StartsWith("前往 "))
            {player=player with{MarketId=2};Emit(new{@event="Docked",MarketID=2});}
            else if(value.Message.StartsWith("卖出 ")&&value.Message.Contains("等待实际交易确认"))
            {player=player with{Credits=10240,CargoCount=2,Inventory=[new("gold","Gold",2)]};Emit(new{@event="MarketSell",MarketID=2,Type="gold",Count=3,TotalSale=540});}
        };
        using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(2));
        await runner.RunAsync(Plan with{Request=Plan.Request with{OccupiedCargo=2}},ExecutionMode.JournalGuide,driver,timeout.Token);
        Check.Equal(RunState.Completed,runner.State);Check.Equal(0,driver.Calls);Check.Equal(2,player.CargoCount);Check.Equal(2,player.Inventory.Single().Count);
    }
    [Test]public async Task UnknownOccupiedInventoryAndChangedOccupiedCountCannotStartGuidance()
    {
        foreach(var player in new[]{OccupiedPlayer with{Inventory=[]},OccupiedPlayer with{CargoCount=3,Inventory=[new("gold","Gold",3)]},OccupiedPlayer with{CurrentShipId=null}})
        {
            var driver=new Driver();var runner=new TradeRunner(()=>player,Quote);
            await runner.RunAsync(Plan with{Request=Plan.Request with{OccupiedCargo=2}},ExecutionMode.JournalGuide,driver);
            Check.Equal(RunState.Failed,runner.State);Check.Equal(0,driver.Calls);
        }
    }
    [Test]public async Task MissingNewCargoCannotBeReplacedByThePreservedSameCommodityAtSale()
    {
        var player=OccupiedPlayer;var runner=new TradeRunner(()=>player,Quote);long sequence=0;var saleRequested=false;
        void Emit(object value)=>runner.Observe(new(++sequence,DateTimeOffset.UtcNow,Check.Event(value)));
        runner.Progress+=value=>
        {
            if(value.Message.StartsWith("买入 ")&&value.Message.Contains("等待实际交易确认"))
            {player=player with{Credits=9700,CargoCount=5,Inventory=[new("gold","Gold",5)]};Emit(new{@event="MarketBuy",MarketID=1,Type="gold",Count=3,TotalCost=300});}
            else if(value.Message.StartsWith("前往 "))
            {player=player with{MarketId=2,CargoCount=3,Inventory=[new("gold","Gold",3)]};Emit(new{@event="Docked",MarketID=2});}
            else if(value.Message.StartsWith("卖出 ")&&value.Message.Contains("等待实际交易确认"))saleRequested=true;
        };
        using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(2));
        await runner.RunAsync(Plan with{Request=Plan.Request with{OccupiedCargo=2}},ExecutionMode.JournalGuide,token:timeout.Token);
        Check.Equal(RunState.Failed,runner.State);Check.That(!saleRequested);Check.Equal(3,player.CargoCount);
    }
    [Test] public void NativeModeRequiresConnectedBridge()
    {
        var driver = new XFEEDToolBox.Core.Native.NativeTradeDriver(new XFEEDToolBox.Core.Native.NativeBridgeClient());
        Check.Throws<InvalidOperationException>(() => driver.PreflightAsync(CancellationToken.None));
    }
    [Test] public async Task CapturedQuoteCannotBeUsedForRealRun()
    {
        var driver = new Driver(); var runner = new TradeRunner(() => Player, (id, key) => Quote(id, key) with { Source = DataSource.Capture });
        await runner.RunAsync(Plan, ExecutionMode.NativeBridge, driver); Check.Equal(1, driver.Calls); Check.Equal(RunState.Failed, runner.State);
    }
    [Test] public async Task ChangedPriceStopsBeforeBuying()
    {
        var driver = new Driver(); var runner = new TradeRunner(() => Player, (id, key) => Quote(id, key) with { BuyPrice = 1000 });
        await runner.RunAsync(Plan, ExecutionMode.NativeBridge, driver); Check.Equal(1, driver.Calls); Check.Equal(RunState.Failed, runner.State);
    }
    [Test] public async Task ConfirmsFullTradeCycleFromJournalEvents()
    {
        var p = Player; var driver = new Driver(); var runner = new TradeRunner(() => p, Quote); long seq = 0;
        void Emit(object e) => runner.Observe(new(++seq, DateTimeOffset.UtcNow, Check.Event(e)));
        driver.OnAction = action =>
        {
            switch (action.Kind)
            {
                case TradeActionKind.Buy:
                    p = p with { Credits = 9700, CargoCount = 3, Inventory = [new("gold", "Gold", 3)] };
                    Emit(new { @event = "MarketBuy", MarketID = 1, Type = "gold", Count = 3, TotalCost = 300 }); break;
                case TradeActionKind.Travel:
                    p = p with { MarketId = 2 };
                    Emit(new { @event = "Docked", MarketID = 2 }); break;
                case TradeActionKind.Sell:
                    p = p with { Credits = 10240, CargoCount = 0, Inventory = [] };
                    Emit(new { @event = "MarketSell", MarketID = 2, Type = "gold", Count = 3, TotalSale = 540 }); break;
            }
            return Task.CompletedTask;
        };
        RunProgress? final = null; runner.Progress += x => final = x;
        await runner.RunAsync(Plan, ExecutionMode.NativeBridge, driver);
        Check.Equal(RunState.Completed, runner.State); Check.Equal(240m, final!.ActualProfit); Check.Equal(4, driver.Calls);
    }
    [Test] public async Task ServicesAndTransactionsRunTogetherButBothMustFinishBeforeDepartureAndCompletion()
    {
        var p=Player;var driver=new Driver();var runner=new TradeRunner(()=>p,Quote);long seq=0;
        var sourceService=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var destinationService=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var bought=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var sold=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var travelled=false;
        void Emit(object value)=>runner.Observe(new(++seq,DateTimeOffset.UtcNow,Check.Event(value)));
        driver.OnServices=(request,token)=>(request.MarketId==1?sourceService.Task:destinationService.Task).WaitAsync(token);
        driver.OnAction=action=>
        {
            if(action.Kind==TradeActionKind.Buy)
            {
                Check.That(!sourceService.Task.IsCompleted); // purchase does not wait for maintenance
                p=p with{CargoCount=3,Inventory=[new("gold","Gold",3)]};
                Emit(new{@event="MarketBuy",MarketID=1,Type="gold",Count=3,TotalCost=300});bought.SetResult();
            }
            else if(action.Kind==TradeActionKind.Travel)
            {
                Check.That(sourceService.Task.IsCompletedSuccessfully);travelled=true;p=p with{MarketId=2};
                Emit(new{@event="Docked",MarketID=2});
            }
            else
            {
                Check.That(!destinationService.Task.IsCompleted);
                p=p with{CargoCount=0,Inventory=[]};
                Emit(new{@event="MarketSell",MarketID=2,Type="gold",Count=3,TotalSale=540});sold.SetResult();
            }
            return Task.CompletedTask;
        };
        var run=runner.RunAsync(Plan,ExecutionMode.NativeBridge,driver);
        await bought.Task.WaitAsync(TimeSpan.FromSeconds(2));Check.That(!travelled&&!run.IsCompleted);
        sourceService.SetResult();
        await sold.Task.WaitAsync(TimeSpan.FromSeconds(2));Check.That(!run.IsCompleted);
        destinationService.SetResult();await run.WaitAsync(TimeSpan.FromSeconds(2));
        Check.Equal(RunState.Completed,runner.State);Check.Equal(2,driver.Services.Count);
        Check.That(driver.Services.All(x=>x.ReservedCargo==Plan.Request.CargoCapacity));
    }
    [Test] public async Task ConfirmedServicesReduceLiveNetProfitAndTheirJournalCopiesAreNotCountedTwice()
    {
        var p=Player;var driver=new Driver();var runner=new TradeRunner(()=>p,Quote);long seq=0;
        var progress=new List<RunProgress>();runner.Progress+=progress.Add;
        void Emit(object e)=>runner.Observe(new(++seq,DateTimeOffset.UtcNow,Check.Event(e)));
        driver.OnServices=(request,_)=>
        {
            var amount=request.MarketId==1?5:7;
            driver.ConfirmService(request.MarketId,request.MarketId==1?"refuel":"repair",amount);
            Emit(new{@event="RepairAll",MarketID=request.MarketId,Cost=amount});
            return Task.CompletedTask;
        };
        driver.OnAction=action=>
        {
            if(action.Kind==TradeActionKind.Buy)
            {
                p=p with{CargoCount=3,Inventory=[new("gold","Gold",3)]};
                Emit(new{@event="MarketBuy",MarketID=1,Type="gold",Count=3,TotalCost=300});
            }
            else if(action.Kind==TradeActionKind.Travel)
            {p=p with{MarketId=2};Emit(new{@event="Docked",MarketID=2});}
            else
            {p=p with{CargoCount=0,Inventory=[]};Emit(new{@event="MarketSell",MarketID=2,Type="gold",Count=3,TotalSale=540});}
            return Task.CompletedTask;
        };
        await runner.RunAsync(Plan,ExecutionMode.NativeBridge,driver);
        Check.Equal(RunState.Completed,runner.State);
        Check.Equal(228m,progress[^1].ActualProfit);
        Check.Equal(5m,progress[^1].Accounting.Fuel);Check.Equal(7m,progress[^1].Accounting.Repairs);
        Check.That(progress.Any(x=>x.Accounting.CargoInvestment==300&&x.ActualProfit==-5));
        Check.That(progress.Any(x=>x.State==RunState.Running&&x.ActualProfit==228));
    }
    [Test] public async Task ServiceFailureInterruptsAPendingTradeConfirmationAndPreventsDeparture()
    {
        var driver=new Driver();var runner=new TradeRunner(()=>Player,Quote);
        var service=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var bought=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        driver.OnServices=(_,token)=>service.Task.WaitAsync(token);
        driver.OnAction=action=>{Check.Equal(TradeActionKind.Buy,action.Kind);bought.SetResult();return Task.CompletedTask;};
        var run=runner.RunAsync(Plan,ExecutionMode.NativeBridge,driver);
        await bought.Task.WaitAsync(TimeSpan.FromSeconds(2));service.SetException(new IOException("维修结果尚未确认"));
        await run.WaitAsync(TimeSpan.FromSeconds(2));
        Check.Equal(RunState.Failed,runner.State);Check.Equal(2,driver.Calls);Check.That(driver.Released);
    }
    [Test] public async Task UnexpectedTradeStopsAllLaterActions()
    {
        var driver = new Driver(); var runner = new TradeRunner(() => Player, Quote);
        driver.OnAction = _ => { runner.Observe(new(1, DateTimeOffset.UtcNow, Check.Json("""{"event":"MarketBuy","MarketID":1,"Type":"silver","Count":3,"TotalCost":300}"""))); return Task.CompletedTask; };
        await runner.RunAsync(Plan, ExecutionMode.NativeBridge, driver); Check.Equal(RunState.Failed, runner.State); Check.Equal(2, driver.Calls); Check.That(driver.Released);
    }
    [Test] public async Task StopInterruptsPendingJournalWait()
    {
        var runner = new TradeRunner(() => Player, Quote);
        var task = runner.RunAsync(Plan, ExecutionMode.JournalGuide);
        await Task.Delay(20); runner.Stop(); await task.WaitAsync(TimeSpan.FromSeconds(1)); Check.Equal(RunState.Stopped, runner.State);
    }
    [Test] public async Task InvalidPlanCannotCallDriver()
    {
        var driver = new Driver(); var runner = new TradeRunner(() => Player, Quote);
        var plan = Plan with { Legs = [Plan.Legs[0] with { Orders = [new("gold", "Gold", 100, 100, 180)] }] };
        await runner.RunAsync(plan, ExecutionMode.NativeBridge, driver); Check.Equal(0, driver.Calls);
    }
}