using XFEEDToolBox.Core.Data;
using System.Collections.Concurrent;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Models;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class ConfirmedTradeReceiptTests
{
private sealed class Fixture:IConfirmedTradeCommandDriver
{
public PlayerState Player=new(){InGame=true,Docked=true,MarketId=1,CommanderId="receipt-test",CurrentShipId=7,Ship="Type9",
LoadGameAt=DateTimeOffset.UtcNow,Credits=10000,CreditsReliable=true,CargoReliable=true,CargoCapacity=10};
public readonly TradeRunner Runner;
public readonly List<long> Serviced=[];
public readonly HashSet<string> JournalNames=[];
public readonly ConcurrentQueue<string> Messages=new();
public Func<ConfirmedTradeReceipt,ConfirmedTradeReceipt>? ChangeReceipt;
public int Trades,Travel;public bool Released;public bool RepeatKey;
private readonly string firstKey=Guid.NewGuid().ToString("N");
public Fixture()
{Runner=new(()=>Player,(id,key)=>Check.Quote(id,key,id==1?100:200,id==1?90:180));Runner.Progress+=progress=>Messages.Enqueue(progress.Message);}
public 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)]}]};
public Task PreflightAsync(CancellationToken token)=>Task.CompletedTask;
public async Task ServiceDockedAsync(DockedServiceRequest request,Func<CancellationToken,Task> beforeInput,CancellationToken token)
{
await Task.Delay(30,token);await beforeInput(token);
Player=Player with{Credits=Player.Credits-11};Serviced.Add(request.MarketId);
}
public async Task<ConfirmedTradeReceipt> ExecuteTradeAsync(TradeAction action,Func<CancellationToken,Task> beforeInput,CancellationToken token)
{
await beforeInput(token);Trades++;var before=Player;var buy=action.Kind==TradeActionKind.Buy;
var delta=buy?3:-3;var price=buy?100:180;
Player=Player with{Credits=Player.Credits-price*delta,CargoCount=before.CargoCount+delta,Inventory=buy?[new("gold","Gold",3)]:[]};
var receipt=new ConfirmedTradeReceipt(RepeatKey||buy?firstKey:Guid.NewGuid().ToString("N"),Player.MarketId!.Value,"gold",delta,price,
(long)before.Credits!.Value,(long)Player.Credits!.Value,before.CargoCount,Player.CargoCount,Player.CommanderId,7,Player.LoadGameAt!.Value,DateTimeOffset.UtcNow,true,true);
return ChangeReceipt?.Invoke(receipt)??receipt;
}
public async Task ExecuteAsync(TradeAction action,Func<CancellationToken,Task> beforeInput,CancellationToken token)
{
if(action.Kind!=TradeActionKind.Travel)throw new InvalidOperationException("Receipted orders must not execute twice.");
await beforeInput(token);Check.That(Serviced.Contains(1));Travel++;Player=Player with{MarketId=2};JournalNames.Add("Docked");
Runner.Observe(new(1,DateTimeOffset.UtcNow,Check.Event(new{@event="Docked",MarketID=2})));
}
public Task ReleaseAllAsync(){Released=true;return Task.CompletedTask;}
}
[Test] public async Task ServerReceiptsCompleteWithBothMaintenanceTasksAndOnlyAnActualDockingEvent()
{
var fixture=new Fixture();decimal profit=0;fixture.Runner.Progress+=p=>profit=p.ActualProfit;
await fixture.Runner.RunAsync(Fixture.Plan,ExecutionMode.NativeBridge,fixture).WaitAsync(TimeSpan.FromSeconds(2));
Check.Equal(RunState.Completed,fixture.Runner.State);Check.Equal(2,fixture.Trades);Check.Equal(1,fixture.Travel);
Check.Equal(2,fixture.Serviced.Count);Check.Equal(240m,profit);Check.Equal(10218m,fixture.Player.Credits);
Check.Equal(1,fixture.JournalNames.Count);Check.That(fixture.JournalNames.Contains("Docked"));Check.That(fixture.Released);
}
[Test] public async Task WrongServerOrderOrSessionStopsBeforeDeparture()
{
foreach(var change in new Func<ConfirmedTradeReceipt,ConfirmedTradeReceipt>[]
{
r=>r with{MarketId=99},r=>r with{QuantityDelta=1},r=>r with{ServerConfirmed=false},r=>r with{ClientApplied=false},
r=>r with{CreditsAfter=r.CreditsAfter+1},r=>r with{CargoAfter=9},r=>r with{ShipId=8},r=>r with{SessionStartedAt=r.SessionStartedAt.AddSeconds(-1)},
r=>r with{ConfirmedAt=r.ConfirmedAt.AddMinutes(-5)},r=>r with{OperationKey=""}
})
{
var fixture=new Fixture{ChangeReceipt=change};
await fixture.Runner.RunAsync(Fixture.Plan,ExecutionMode.NativeBridge,fixture).WaitAsync(TimeSpan.FromSeconds(2));
Check.Equal(RunState.Failed,fixture.Runner.State);Check.Equal(1,fixture.Trades);Check.Equal(0,fixture.Travel);Check.That(fixture.Released);
}
}
[Test] public async Task ReusedNativeOperationKeyCannotCountASecondTrade()
{
var fixture=new Fixture{RepeatKey=true};
await fixture.Runner.RunAsync(Fixture.Plan,ExecutionMode.NativeBridge,fixture).WaitAsync(TimeSpan.FromSeconds(2));
Check.Equal(RunState.Failed,fixture.Runner.State);Check.Equal(2,fixture.Trades);Check.Equal(1,fixture.Travel);Check.That(fixture.Released);
}
[Test] public async Task UnknownActualShipStopsBeforeAnyServiceTradeOrDeparture()
{
var fixture=new Fixture();fixture.Player=fixture.Player with{Ship=""};
await fixture.Runner.RunAsync(Fixture.Plan,ExecutionMode.NativeBridge,fixture).WaitAsync(TimeSpan.FromSeconds(2));
Check.Equal(RunState.Failed,fixture.Runner.State);Check.Equal(0,fixture.Trades);Check.Equal(0,fixture.Travel);Check.Equal(0,fixture.Serviced.Count);
Check.That(fixture.Released);Check.That(fixture.Messages.Any(message=>message.Contains("实际船型尺寸",StringComparison.Ordinal)));
}
[Test] public async Task TooSmallDestinationPadStopsBeforeAnyServiceTradeOrDeparture()
{
var fixture=new Fixture();var plan=Fixture.Plan;var leg=plan.Legs[0];
plan=plan with{Legs=[leg with{To=leg.To with{MaxLandingPad=LandingPad.Medium}}]};
await fixture.Runner.RunAsync(plan,ExecutionMode.NativeBridge,fixture).WaitAsync(TimeSpan.FromSeconds(2));
Check.Equal(RunState.Failed,fixture.Runner.State);Check.Equal(0,fixture.Trades);Check.Equal(0,fixture.Travel);Check.Equal(0,fixture.Serviced.Count);
Check.That(fixture.Released);Check.That(fixture.Messages.Any(message=>message.Contains("停机坪",StringComparison.Ordinal)));
}
}
using XFEEDToolBox.Core.Data;
using System.Collections.Concurrent;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Models;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class ConfirmedTradeReceiptTests
{
private sealed class Fixture:IConfirmedTradeCommandDriver
{
public PlayerState Player=new(){InGame=true,Docked=true,MarketId=1,CommanderId="receipt-test",CurrentShipId=7,Ship="Type9",
LoadGameAt=DateTimeOffset.UtcNow,Credits=10000,CreditsReliable=true,CargoReliable=true,CargoCapacity=10};
public readonly TradeRunner Runner;
public readonly List<long> Serviced=[];
public readonly HashSet<string> JournalNames=[];
public readonly ConcurrentQueue<string> Messages=new();
public Func<ConfirmedTradeReceipt,ConfirmedTradeReceipt>? ChangeReceipt;
public int Trades,Travel;public bool Released;public bool RepeatKey;
private readonly string firstKey=Guid.NewGuid().ToString("N");
public Fixture()
{Runner=new(()=>Player,(id,key)=>Check.Quote(id,key,id==1?100:200,id==1?90:180));Runner.Progress+=progress=>Messages.Enqueue(progress.Message);}
public 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)]}]};
public Task PreflightAsync(CancellationToken token)=>Task.CompletedTask;
public async Task ServiceDockedAsync(DockedServiceRequest request,Func<CancellationToken,Task> beforeInput,CancellationToken token)
{
await Task.Delay(30,token);await beforeInput(token);
Player=Player with{Credits=Player.Credits-11};Serviced.Add(request.MarketId);
}
public async Task<ConfirmedTradeReceipt> ExecuteTradeAsync(TradeAction action,Func<CancellationToken,Task> beforeInput,CancellationToken token)
{
await beforeInput(token);Trades++;var before=Player;var buy=action.Kind==TradeActionKind.Buy;
var delta=buy?3:-3;var price=buy?100:180;
Player=Player with{Credits=Player.Credits-price*delta,CargoCount=before.CargoCount+delta,Inventory=buy?[new("gold","Gold",3)]:[]};
var receipt=new ConfirmedTradeReceipt(RepeatKey||buy?firstKey:Guid.NewGuid().ToString("N"),Player.MarketId!.Value,"gold",delta,price,
(long)before.Credits!.Value,(long)Player.Credits!.Value,before.CargoCount,Player.CargoCount,Player.CommanderId,7,Player.LoadGameAt!.Value,DateTimeOffset.UtcNow,true,true);
return ChangeReceipt?.Invoke(receipt)??receipt;
}
public async Task ExecuteAsync(TradeAction action,Func<CancellationToken,Task> beforeInput,CancellationToken token)
{
if(action.Kind!=TradeActionKind.Travel)throw new InvalidOperationException("Receipted orders must not execute twice.");
await beforeInput(token);Check.That(Serviced.Contains(1));Travel++;Player=Player with{MarketId=2};JournalNames.Add("Docked");
Runner.Observe(new(1,DateTimeOffset.UtcNow,Check.Event(new{@event="Docked",MarketID=2})));
}
public Task ReleaseAllAsync(){Released=true;return Task.CompletedTask;}
}
[Test] public async Task ServerReceiptsCompleteWithBothMaintenanceTasksAndOnlyAnActualDockingEvent()
{
var fixture=new Fixture();decimal profit=0;fixture.Runner.Progress+=p=>profit=p.ActualProfit;
await fixture.Runner.RunAsync(Fixture.Plan,ExecutionMode.NativeBridge,fixture).WaitAsync(TimeSpan.FromSeconds(2));
Check.Equal(RunState.Completed,fixture.Runner.State);Check.Equal(2,fixture.Trades);Check.Equal(1,fixture.Travel);
Check.Equal(2,fixture.Serviced.Count);Check.Equal(240m,profit);Check.Equal(10218m,fixture.Player.Credits);
Check.Equal(1,fixture.JournalNames.Count);Check.That(fixture.JournalNames.Contains("Docked"));Check.That(fixture.Released);
}
[Test] public async Task WrongServerOrderOrSessionStopsBeforeDeparture()
{
foreach(var change in new Func<ConfirmedTradeReceipt,ConfirmedTradeReceipt>[]
{
r=>r with{MarketId=99},r=>r with{QuantityDelta=1},r=>r with{ServerConfirmed=false},r=>r with{ClientApplied=false},
r=>r with{CreditsAfter=r.CreditsAfter+1},r=>r with{CargoAfter=9},r=>r with{ShipId=8},r=>r with{SessionStartedAt=r.SessionStartedAt.AddSeconds(-1)},
r=>r with{ConfirmedAt=r.ConfirmedAt.AddMinutes(-5)},r=>r with{OperationKey=""}
})
{
var fixture=new Fixture{ChangeReceipt=change};
await fixture.Runner.RunAsync(Fixture.Plan,ExecutionMode.NativeBridge,fixture).WaitAsync(TimeSpan.FromSeconds(2));
Check.Equal(RunState.Failed,fixture.Runner.State);Check.Equal(1,fixture.Trades);Check.Equal(0,fixture.Travel);Check.That(fixture.Released);
}
}
[Test] public async Task ReusedNativeOperationKeyCannotCountASecondTrade()
{
var fixture=new Fixture{RepeatKey=true};
await fixture.Runner.RunAsync(Fixture.Plan,ExecutionMode.NativeBridge,fixture).WaitAsync(TimeSpan.FromSeconds(2));
Check.Equal(RunState.Failed,fixture.Runner.State);Check.Equal(2,fixture.Trades);Check.Equal(1,fixture.Travel);Check.That(fixture.Released);
}
[Test] public async Task UnknownActualShipStopsBeforeAnyServiceTradeOrDeparture()
{
var fixture=new Fixture();fixture.Player=fixture.Player with{Ship=""};
await fixture.Runner.RunAsync(Fixture.Plan,ExecutionMode.NativeBridge,fixture).WaitAsync(TimeSpan.FromSeconds(2));
Check.Equal(RunState.Failed,fixture.Runner.State);Check.Equal(0,fixture.Trades);Check.Equal(0,fixture.Travel);Check.Equal(0,fixture.Serviced.Count);
Check.That(fixture.Released);Check.That(fixture.Messages.Any(message=>message.Contains("实际船型尺寸",StringComparison.Ordinal)));
}
[Test] public async Task TooSmallDestinationPadStopsBeforeAnyServiceTradeOrDeparture()
{
var fixture=new Fixture();var plan=Fixture.Plan;var leg=plan.Legs[0];
plan=plan with{Legs=[leg with{To=leg.To with{MaxLandingPad=LandingPad.Medium}}]};
await fixture.Runner.RunAsync(plan,ExecutionMode.NativeBridge,fixture).WaitAsync(TimeSpan.FromSeconds(2));
Check.Equal(RunState.Failed,fixture.Runner.State);Check.Equal(0,fixture.Trades);Check.Equal(0,fixture.Travel);Check.Equal(0,fixture.Serviced.Count);
Check.That(fixture.Released);Check.That(fixture.Messages.Any(message=>message.Contains("停机坪",StringComparison.Ordinal)));
}
}