using System.Collections.Concurrent;
using XFEExtension.NetCore.XUnit.Attributes;
using XFEEDToolBox.Core.Data;using XFEEDToolBox.Core.Models;using XFEEDToolBox.Core.Planning;
namespace XFEEDToolBox.Test;
[TestFixture]public sealed class AllStationTradePlanningTests
{
private sealed class InlineProgress(Action<TradePlanningProgress> observe):IProgress<TradePlanningProgress>{public void Report(TradePlanningProgress value)=>observe(value);}
private static MarketBook LargeBook()
{
var stations=Enumerable.Range(1,501).Select(id=>Check.Station(id,id*.01)).ToArray();
var quotes=stations.Select(station=>Check.Quote(station.MarketId,"water",0,0,0,0)).ToList();
quotes.RemoveAll(quote=>quote.MarketId is 250 or 251);
quotes.Add(Check.Quote(250,"water",10,9));quotes.Add(Check.Quote(251,"water",0,20));
return new(stations,quotes);
}
[Test]public void Beyond180AllStationsParticipateAndARealAlternateOriginIncludesItsDeadhead()
{
var events=new ConcurrentQueue<TradePlanningProgress>();var request=Check.Request with{Loop=false,MaxLegs=1,SearchRadiusLy=500};
var result=new TradeRoutePlanner().CalculateCandidatesWithDiagnostics(LargeBook(),request,progress:new InlineProgress(events.Enqueue));
var plan=result.Candidates.First();Check.Equal(250L,plan.Request.StartMarketId);Check.Equal(501,plan.StationsConsidered);Check.Equal(2.5,plan.DeadheadDistanceLy);
Check.That(!result.Diagnostics.InputTruncated);Check.Equal(501,result.Diagnostics.CompatibleStations);
Check.That(events.Any(value=>value.Stage=="完成"&&value.Processed==value.Total));
Check.That(events.All(value=>value.Total is null||value.Processed<=value.Total));
}
[Test]public void SourceKindsDoNotExcludeCarrierDestinationsOrAnEmptyReturn()
{
var carrier=Check.Station(2,10) with{IsFleetCarrier=true,Type="FleetCarrier",MaxLandingPad=LandingPad.Unknown};
var book=new MarketBook([Check.Station(1),carrier],[Check.Quote(1,"water",10,9),Check.Quote(2,"water",0,20)]);
var request=Check.Request with{AllowedSourceKinds=StationSourceKinds.Starport,Loop=true,MaxLegs=2,IncludeCarriers=false};
var plan=new TradeRoutePlanner().CalculateCandidates(book,request).First();
Check.Equal(2L,plan.Legs[0].To.MarketId);Check.Equal(0,plan.Legs[1].Quantity);
Check.That(plan.Legs.Where(leg=>leg.Quantity>0).All(leg=>!leg.From.IsFleetCarrier));
}
[Test]public void UnknownHullAndUnknownPadNeverBecomeAnAssumedSmallDestination()
{
var planner=new TradeRoutePlanner();
Check.Throws<InvalidOperationException>(()=>planner.CalculateCandidates(Check.TwoStations,Check.Request with{ActualShipType="Unverified future ship",RequiredPad=LandingPad.Small}));
var request=Check.Request with{ActualShipType="Type9",RequiredPad=LandingPad.Small,AllowUnknownMetadata=true};
Check.That(!TradeRoutePlanner.IsStationCompatible(Check.Station(2) with{MaxLandingPad=LandingPad.Medium},request,false));
Check.That(!TradeRoutePlanner.IsStationCompatible(Check.Station(2) with{MaxLandingPad=LandingPad.Unknown},request,false));
}
[Test]public void TheSeventeenthAffordableDestinationIsNotDiscardedByAGrossPriceHeuristic()
{
var stations=Enumerable.Range(1,18).Select(id=>Check.Station(id,id*.01)).ToArray();var quotes=new List<CommodityQuote>();
for(var id=2;id<=17;id++){quotes.Add(Check.Quote(1,"expensive"+id,1_000_000_000,0));quotes.Add(Check.Quote(id,"expensive"+id,0,2_000_000_000));}
quotes.Add(Check.Quote(1,"water",100,90));quotes.Add(Check.Quote(18,"water",0,180));
var plans=new TradeRoutePlanner().CalculateCandidates(new(stations,quotes),Check.Request with{Loop=false,MaxLegs=1});
Check.Equal(18L,plans.First().Legs.Single().To.MarketId);
}
[Test]public void CancellationDuringParallelIndexingNeverReportsCompletion()
{
using var cancellation=new CancellationTokenSource();var events=new ConcurrentQueue<TradePlanningProgress>();
var progress=new InlineProgress(value=>{events.Enqueue(value);if(value.Stage=="建立商品与空间索引"&&value.Processed>0)cancellation.Cancel();});
Check.Throws<OperationCanceledException>(()=>new TradeRoutePlanner().CalculateCandidates(LargeBook(),Check.Request,token:cancellation.Token,progress:progress));
Check.That(!events.Any(value=>value.Stage=="完成"));
}
[Test]public async Task PlanningReadLoadsMoreThan180FreshCompatibleStationsWithoutDroppingRows()
{
using var temporary=new TemporaryDirectory();var repository=new MarketRepository(temporary.File("markets.db"));repository.InitializeCheap();var at=DateTimeOffset.UtcNow;
for(var id=1;id<=205;id++)await repository.SaveSnapshotAsync(new(Check.Station(id,id*.01),[Check.Quote(id,"water",10,20)],at,DataSource.Journal));
var book=repository.LoadPlanningRegion(Check.Request with{AsOf=at});Check.Equal(205,book.Stations.Count);Check.Equal(205,book.Quotes.Count);Check.That(!book.Truncated);
}
}
using System.Collections.Concurrent;
using XFEExtension.NetCore.XUnit.Attributes;
using XFEEDToolBox.Core.Data;using XFEEDToolBox.Core.Models;using XFEEDToolBox.Core.Planning;
namespace XFEEDToolBox.Test;
[TestFixture]public sealed class AllStationTradePlanningTests
{
private sealed class InlineProgress(Action<TradePlanningProgress> observe):IProgress<TradePlanningProgress>{public void Report(TradePlanningProgress value)=>observe(value);}
private static MarketBook LargeBook()
{
var stations=Enumerable.Range(1,501).Select(id=>Check.Station(id,id*.01)).ToArray();
var quotes=stations.Select(station=>Check.Quote(station.MarketId,"water",0,0,0,0)).ToList();
quotes.RemoveAll(quote=>quote.MarketId is 250 or 251);
quotes.Add(Check.Quote(250,"water",10,9));quotes.Add(Check.Quote(251,"water",0,20));
return new(stations,quotes);
}
[Test]public void Beyond180AllStationsParticipateAndARealAlternateOriginIncludesItsDeadhead()
{
var events=new ConcurrentQueue<TradePlanningProgress>();var request=Check.Request with{Loop=false,MaxLegs=1,SearchRadiusLy=500};
var result=new TradeRoutePlanner().CalculateCandidatesWithDiagnostics(LargeBook(),request,progress:new InlineProgress(events.Enqueue));
var plan=result.Candidates.First();Check.Equal(250L,plan.Request.StartMarketId);Check.Equal(501,plan.StationsConsidered);Check.Equal(2.5,plan.DeadheadDistanceLy);
Check.That(!result.Diagnostics.InputTruncated);Check.Equal(501,result.Diagnostics.CompatibleStations);
Check.That(events.Any(value=>value.Stage=="完成"&&value.Processed==value.Total));
Check.That(events.All(value=>value.Total is null||value.Processed<=value.Total));
}
[Test]public void SourceKindsDoNotExcludeCarrierDestinationsOrAnEmptyReturn()
{
var carrier=Check.Station(2,10) with{IsFleetCarrier=true,Type="FleetCarrier",MaxLandingPad=LandingPad.Unknown};
var book=new MarketBook([Check.Station(1),carrier],[Check.Quote(1,"water",10,9),Check.Quote(2,"water",0,20)]);
var request=Check.Request with{AllowedSourceKinds=StationSourceKinds.Starport,Loop=true,MaxLegs=2,IncludeCarriers=false};
var plan=new TradeRoutePlanner().CalculateCandidates(book,request).First();
Check.Equal(2L,plan.Legs[0].To.MarketId);Check.Equal(0,plan.Legs[1].Quantity);
Check.That(plan.Legs.Where(leg=>leg.Quantity>0).All(leg=>!leg.From.IsFleetCarrier));
}
[Test]public void UnknownHullAndUnknownPadNeverBecomeAnAssumedSmallDestination()
{
var planner=new TradeRoutePlanner();
Check.Throws<InvalidOperationException>(()=>planner.CalculateCandidates(Check.TwoStations,Check.Request with{ActualShipType="Unverified future ship",RequiredPad=LandingPad.Small}));
var request=Check.Request with{ActualShipType="Type9",RequiredPad=LandingPad.Small,AllowUnknownMetadata=true};
Check.That(!TradeRoutePlanner.IsStationCompatible(Check.Station(2) with{MaxLandingPad=LandingPad.Medium},request,false));
Check.That(!TradeRoutePlanner.IsStationCompatible(Check.Station(2) with{MaxLandingPad=LandingPad.Unknown},request,false));
}
[Test]public void TheSeventeenthAffordableDestinationIsNotDiscardedByAGrossPriceHeuristic()
{
var stations=Enumerable.Range(1,18).Select(id=>Check.Station(id,id*.01)).ToArray();var quotes=new List<CommodityQuote>();
for(var id=2;id<=17;id++){quotes.Add(Check.Quote(1,"expensive"+id,1_000_000_000,0));quotes.Add(Check.Quote(id,"expensive"+id,0,2_000_000_000));}
quotes.Add(Check.Quote(1,"water",100,90));quotes.Add(Check.Quote(18,"water",0,180));
var plans=new TradeRoutePlanner().CalculateCandidates(new(stations,quotes),Check.Request with{Loop=false,MaxLegs=1});
Check.Equal(18L,plans.First().Legs.Single().To.MarketId);
}
[Test]public void CancellationDuringParallelIndexingNeverReportsCompletion()
{
using var cancellation=new CancellationTokenSource();var events=new ConcurrentQueue<TradePlanningProgress>();
var progress=new InlineProgress(value=>{events.Enqueue(value);if(value.Stage=="建立商品与空间索引"&&value.Processed>0)cancellation.Cancel();});
Check.Throws<OperationCanceledException>(()=>new TradeRoutePlanner().CalculateCandidates(LargeBook(),Check.Request,token:cancellation.Token,progress:progress));
Check.That(!events.Any(value=>value.Stage=="完成"));
}
[Test]public async Task PlanningReadLoadsMoreThan180FreshCompatibleStationsWithoutDroppingRows()
{
using var temporary=new TemporaryDirectory();var repository=new MarketRepository(temporary.File("markets.db"));repository.InitializeCheap();var at=DateTimeOffset.UtcNow;
for(var id=1;id<=205;id++)await repository.SaveSnapshotAsync(new(Check.Station(id,id*.01),[Check.Quote(id,"water",10,20)],at,DataSource.Journal));
var book=repository.LoadPlanningRegion(Check.Request with{AsOf=at});Check.Equal(205,book.Stations.Count);Check.Equal(205,book.Quotes.Count);Check.That(!book.Truncated);
}
}