using XFEEDToolBox.Core.Execution; using XFEEDToolBox.Core.Models; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public sealed class TradeRunMenuScopeTests { private static PlayerState Player=>new(){Ship="Type9",InGame=true,Docked=true,MarketId=1,Credits=10000,CreditsReliable=true,CargoCapacity=10,StatusAt=DateTimeOffset.UtcNow}; private static RoutePlan Plan=>new(){Request=Check.Request with{MaxLegs=1},Legs=[new(){Number=1,From=Check.Station(1),To=Check.Station(2),Orders=[new("gold","Gold",3,100,180)]}]}; private sealed class Lease(Func running):IDisposable { public bool Disposed;public bool Allowed=>running(); public void Dispose(){Disposed=true;Check.That(!Allowed,"Permission must be revoked before disposing its scope.");} } private sealed class Driver:ITradeCommandDriver,ITradeRunMenuRecovery { public List Leases=[];public bool Released,RejectPreflight,RejectScope,RejectAction;public Action? BeforeScopeReturned; public IDisposable EnterRunningMenuRecovery(Func isRunning,CancellationToken token) { if(RejectScope)throw new InvalidOperationException("scope fixture failure"); var scope=new Lease(isRunning);Leases.Add(scope);Check.That(scope.Allowed);BeforeScopeReturned?.Invoke();return scope; } public Task PreflightAsync(CancellationToken token){if(RejectPreflight)throw new InvalidOperationException("preflight fixture failure");return Task.CompletedTask;} public Task ServiceDockedAsync(DockedServiceRequest request,Func beforeInput,CancellationToken token)=>beforeInput(token); public async Task ExecuteAsync(TradeAction action,Func beforeInput,CancellationToken token) {await beforeInput(token);if(RejectAction)throw new InvalidOperationException("action fixture failure");await Task.Delay(Timeout.Infinite,token);} public Task ReleaseAllAsync(){Released=true;return Task.CompletedTask;} } private static TradeRunner Runner(Driver driver)=>new(()=>Player,(id,key)=>Check.Quote(id,key,id==1?100:200,id==1?90:180)); [Test]public async Task NativeRunningPauseResumeAndStopOwnDifferentRevocableScopes() { var driver=new Driver();var runner=Runner(driver);var checkedState=false; runner.Progress+=progress=> { if(checkedState||!progress.Message.StartsWith("开始执行"))return;checkedState=true; var first=driver.Leases.Single();Check.That(first.Allowed&&!first.Disposed); runner.Pause();Check.Equal(RunState.Paused,runner.State);Check.That(first.Disposed&&!first.Allowed); runner.Resume();Check.Equal(RunState.Running,runner.State);Check.Equal(2,driver.Leases.Count); var second=driver.Leases[1];Check.That(second.Allowed&&!second.Disposed&&!first.Allowed); runner.Stop();Check.That(second.Disposed&&!second.Allowed);runner.Resume();Check.Equal(2,driver.Leases.Count); }; await runner.RunAsync(Plan,ExecutionMode.NativeBridge,driver).WaitAsync(TimeSpan.FromSeconds(2)); Check.That(checkedState&&driver.Released);Check.Equal(RunState.Stopped,runner.State);Check.That(driver.Leases.All(lease=>lease.Disposed&&!lease.Allowed)); } [Test]public async Task GuideSimulationAndRejectedPreflightNeverAuthorizeMenuActions() { foreach(var mode in new[]{ExecutionMode.JournalGuide,ExecutionMode.Simulation,ExecutionMode.NativeBridge}) { var driver=new Driver{RejectPreflight=mode==ExecutionMode.NativeBridge};var runner=Runner(driver); runner.Progress+=progress=>{if(progress.Message.StartsWith("开始执行"))runner.Stop();}; await runner.RunAsync(Plan,mode,driver).WaitAsync(TimeSpan.FromSeconds(2)); Check.Equal(0,driver.Leases.Count);Check.That(driver.Released); } } [Test]public async Task CallerCancellationRevokesPermissionBeforeRunUnwinds() { using var cancellation=new CancellationTokenSource();var driver=new Driver();var runner=Runner(driver);var observed=false; runner.Progress+=progress=> { if(observed||!progress.Message.StartsWith("开始执行"))return;observed=true; Check.That(driver.Leases.Single().Allowed);cancellation.Cancel();Check.That(!driver.Leases.Single().Allowed); }; await runner.RunAsync(Plan,ExecutionMode.NativeBridge,driver,cancellation.Token).WaitAsync(TimeSpan.FromSeconds(2)); Check.That(observed&&driver.Leases.Single().Disposed&&driver.Released);Check.Equal(RunState.Stopped,runner.State); } [Test]public async Task DriverFailureAndScopeCreationFailureReleasePermissionAndControls() { foreach(var rejectScope in new[]{false,true}) { var driver=new Driver{RejectScope=rejectScope,RejectAction=!rejectScope};var runner=Runner(driver); await runner.RunAsync(Plan,ExecutionMode.NativeBridge,driver).WaitAsync(TimeSpan.FromSeconds(2)); Check.Equal(RunState.Failed,runner.State);Check.That(driver.Released); Check.That(driver.Leases.All(lease=>lease.Disposed&&!lease.Allowed)); } } [Test]public async Task PauseDuringScopeCreationRejectsTheLateScopeAndRunningNotification() { var driver=new Driver();var runner=Runner(driver);var paused=false;var lateRunning=false; driver.BeforeScopeReturned=()=>runner.Pause(); runner.Progress+=progress=> { if(progress.State==RunState.Paused){paused=true;runner.Stop();} if(paused&&progress.State==RunState.Running)lateRunning=true; }; await runner.RunAsync(Plan,ExecutionMode.NativeBridge,driver).WaitAsync(TimeSpan.FromSeconds(2)); Check.That(paused&&!lateRunning&&driver.Released);Check.Equal(1,driver.Leases.Count);Check.That(driver.Leases[0].Disposed&&!driver.Leases[0].Allowed); Check.Equal(RunState.Stopped,runner.State); } }