using XFEExtension.NetCore.XUnit.Attributes;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Planning;
namespace XFEEDToolBox.Test;
[TestFixture]
public class CargoTests
{
[Test] public void MatchesIndependentExhaustiveSearch()
{
var random = new Random(71903);
for (var trial = 0; trial < 160; trial++)
{
var goods = Enumerable.Range(0, 4).Select(i => { var buy = random.Next(1, 40); return new CargoCandidate("g" + i, "g" + i, buy, buy + random.Next(1, 25), random.Next(1, 6)); }).ToArray();
var capacity = random.Next(1, 10); var cash = random.Next(1, 180); decimal optimum = 0;
for (var a = 0; a <= goods[0].Limit; a++) for (var b = 0; b <= goods[1].Limit; b++)
for (var c = 0; c <= goods[2].Limit; c++) for (var d = 0; d <= goods[3].Limit; d++)
{
var quantities = new[] { a, b, c, d };
if (quantities.Sum() <= capacity && quantities.Select((q, i) => (decimal)q * goods[i].BuyPrice).Sum() <= cash)
optimum = Math.Max(optimum, quantities.Select((q, i) => (decimal)q * goods[i].ProfitPerTon).Sum());
}
var result = CargoOptimizer.Optimize(goods, capacity, cash, 20000);
Check.Equal(optimum, result.Profit);
Check.That(result.ProvenOptimal, "Small problem should finish within budget");
Check.That(result.Orders.Sum(o => o.Cost) <= cash && result.Orders.Sum(o => o.Quantity) <= capacity);
}
}
[Test] public void MixesCheapAndExpensiveCargoUnderCashLimit()
{
var result = CargoOptimizer.Optimize([new("a", "A", 90, 190, 10), new("b", "B", 10, 80, 10)], 10, 180);
Check.Equal(730m, result.Profit);
Check.Equal(2, result.Orders.Count);
}
[Test] public void RespectsSupplyAndDemandLimit() => Check.Equal(2, CargoOptimizer.Optimize([new("a", "A", 1, 100, 2)], 100, 100).Orders.Single().Quantity);
[Test] public void UsesDecimalForLargeTransactionValues() => Check.Equal(1000000000000m, CargoOptimizer.Optimize([new("a", "A", 10000000000, 20000000000, 100)], 100, 1000000000000m).Profit);
[Test] public void MarksBudgetLimitedResult() => Check.That(!CargoOptimizer.Optimize([new("a", "A", 9, 15, 20), new("b", "B", 2, 5, 20)], 20, 90, 1).ProvenOptimal);
[Test] public void EmptyBudgetHasNoCargo() => Check.Equal(0, CargoOptimizer.Optimize([new("a", "A", 1, 2, 100)], 100, 0).Orders.Count);
[Test] public void CancellationIsObserved()
{
using var cancel = new CancellationTokenSource(); cancel.Cancel();
Check.Throws<OperationCanceledException>(() => CargoOptimizer.Optimize([new("a", "A", 1, 2, 10)], 10, 10, token: cancel.Token));
}
}
[TestFixture]
public class PlannerTests
{
private static RoutePlan Run(MarketBook? book = null, PlannerRequest? request = null) => new TradeRoutePlanner().Calculate(book ?? Check.TwoStations, request ?? Check.Request);
[Test] public void RollsProfitIntoNextStationCash()
{
var result = Run(request: Check.Request with { Loop = true });
Check.Equal(2, result.Legs.Count); Check.That(result.IsLoop);
Check.Equal(1600m, result.NetProfit); Check.Equal(11600m, result.Legs[^1].CashAfter);
}
[Test] public void ReserveIsNeverSpent()
{
var result = Run(request: Check.Request with { Credits = 1500, ReserveCredits = 1000 });
Check.That(result.Legs.Count > 0); Check.That(result.Legs[0].Orders.Sum(o => o.Cost) <= 500);
}
[Test] public void StaleQuotesDoNotBecomeProfitableRoutes()
{
var b = Check.TwoStations; b = b with { Quotes = b.Quotes.Select(q => q with { CollectedAt = DateTimeOffset.UtcNow.AddDays(-2) }).ToArray() };
Check.Equal(0, Run(b).Legs.Count);
}
[Test] public void FutureQuotesAreRejected()
{
var b = Check.TwoStations with { Quotes = Check.TwoStations.Quotes.Select(q => q with { CollectedAt = DateTimeOffset.UtcNow.AddHours(2) }).ToArray() };
Check.Equal(0, Run(b).Legs.Count);
}
[Test] public void IllegalGoodsAreExcluded()
{
var b = Check.TwoStations with { Quotes = Check.TwoStations.Quotes.Select(q => q with { Illegal = true }).ToArray() };
Check.Equal(0, Run(b).Legs.Count);
}
[Test] public void LargeShipsCannotUseMediumPads()
{
var b = Check.TwoStations with { Stations = [Check.Station(1), Check.Station(2, 10) with { MaxLandingPad = LandingPad.Medium }] };
Check.Equal(0, Run(b).Legs.Count);
}
[Test] public void ConservativeRiskExcludesAnarchy()
{
var b = Check.TwoStations with { Stations = [Check.Station(1), Check.Station(2, 10) with { Security = SecurityLevel.Anarchy }] };
Check.Equal(0, Run(b, Check.Request with { Risk = RiskTolerance.Conservative }).Legs.Count);
}
[Test] public void UnknownMetadataIsExcludedByDefault()
{
var b = Check.TwoStations with { Stations = [Check.Station(1), Check.Station(2, 10) with { DistanceToArrivalLs = null }] };
Check.Equal(0, Run(b).Legs.Count);
}
[Test] public void ExcludesPermitRestrictedStations()
{
var b = Check.TwoStations with { Stations = [Check.Station(1), Check.Station(2, 10) with { RequiresPermit = true }] };
Check.Equal(0, Run(b).Legs.Count);
}
[Test] public void CountsSupplyAcrossRepeatedLoopVisits()
{
var b = Check.TwoStations with { Quotes = Check.TwoStations.Quotes.Select(q => q with { Supply = 2, Demand = 3 }).ToArray() };
var result = Run(b, Check.Request with { Loop = true, MaxLegs = 6, Objective = RouteObjective.TotalProfit });
foreach (var group in result.Legs.SelectMany(l => l.Orders.Select(o => (l.From.MarketId, o))).GroupBy(x => (x.MarketId, x.o.Commodity))) Check.That(group.Sum(x => x.o.Quantity) <= 2);
}
[Test] public void DeductsOperatingCosts()
{
var result = Run(request: Check.Request with { MaxLegs = 1, CostPerJump = 100 });
Check.Equal(700m, result.NetProfit);
}
[Test] public void RejectsZeroJumpRange() => Check.Throws<ArgumentException>(() => Run(request: Check.Request with { LadenJumpRangeLy = 0 }));
[Test] public void RejectsAllCashReserved() => Check.Throws<ArgumentException>(() => Run(request: Check.Request with { ReserveCredits = 10000 }));
[Test] public void DemoLoopContainsMixedCargo()
{
var p = DemoData.Player;
var result = Run(DemoData.CreateBook(), Check.Request with { StartMarketId = p.MarketId!.Value, Credits = p.Credits!.Value, ReserveCredits = p.Rebuy, CargoCapacity = p.FreeCargo, Loop = true, MaxLegs = 4 });
Check.That(result.NetProfit > 0 && result.IsLoop); Check.That(result.Legs.Any(l => l.Orders.Count > 1));
}
[Test] public void ReportsInputTruncation() => Check.That(Run(Check.TwoStations with { Truncated = true }).Warnings.Any(w => w.Contains("截断")));
}
using XFEExtension.NetCore.XUnit.Attributes;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Planning;
namespace XFEEDToolBox.Test;
[TestFixture]
public class CargoTests
{
[Test] public void MatchesIndependentExhaustiveSearch()
{
var random = new Random(71903);
for (var trial = 0; trial < 160; trial++)
{
var goods = Enumerable.Range(0, 4).Select(i => { var buy = random.Next(1, 40); return new CargoCandidate("g" + i, "g" + i, buy, buy + random.Next(1, 25), random.Next(1, 6)); }).ToArray();
var capacity = random.Next(1, 10); var cash = random.Next(1, 180); decimal optimum = 0;
for (var a = 0; a <= goods[0].Limit; a++) for (var b = 0; b <= goods[1].Limit; b++)
for (var c = 0; c <= goods[2].Limit; c++) for (var d = 0; d <= goods[3].Limit; d++)
{
var quantities = new[] { a, b, c, d };
if (quantities.Sum() <= capacity && quantities.Select((q, i) => (decimal)q * goods[i].BuyPrice).Sum() <= cash)
optimum = Math.Max(optimum, quantities.Select((q, i) => (decimal)q * goods[i].ProfitPerTon).Sum());
}
var result = CargoOptimizer.Optimize(goods, capacity, cash, 20000);
Check.Equal(optimum, result.Profit);
Check.That(result.ProvenOptimal, "Small problem should finish within budget");
Check.That(result.Orders.Sum(o => o.Cost) <= cash && result.Orders.Sum(o => o.Quantity) <= capacity);
}
}
[Test] public void MixesCheapAndExpensiveCargoUnderCashLimit()
{
var result = CargoOptimizer.Optimize([new("a", "A", 90, 190, 10), new("b", "B", 10, 80, 10)], 10, 180);
Check.Equal(730m, result.Profit);
Check.Equal(2, result.Orders.Count);
}
[Test] public void RespectsSupplyAndDemandLimit() => Check.Equal(2, CargoOptimizer.Optimize([new("a", "A", 1, 100, 2)], 100, 100).Orders.Single().Quantity);
[Test] public void UsesDecimalForLargeTransactionValues() => Check.Equal(1000000000000m, CargoOptimizer.Optimize([new("a", "A", 10000000000, 20000000000, 100)], 100, 1000000000000m).Profit);
[Test] public void MarksBudgetLimitedResult() => Check.That(!CargoOptimizer.Optimize([new("a", "A", 9, 15, 20), new("b", "B", 2, 5, 20)], 20, 90, 1).ProvenOptimal);
[Test] public void EmptyBudgetHasNoCargo() => Check.Equal(0, CargoOptimizer.Optimize([new("a", "A", 1, 2, 100)], 100, 0).Orders.Count);
[Test] public void CancellationIsObserved()
{
using var cancel = new CancellationTokenSource(); cancel.Cancel();
Check.Throws<OperationCanceledException>(() => CargoOptimizer.Optimize([new("a", "A", 1, 2, 10)], 10, 10, token: cancel.Token));
}
}
[TestFixture]
public class PlannerTests
{
private static RoutePlan Run(MarketBook? book = null, PlannerRequest? request = null) => new TradeRoutePlanner().Calculate(book ?? Check.TwoStations, request ?? Check.Request);
[Test] public void RollsProfitIntoNextStationCash()
{
var result = Run(request: Check.Request with { Loop = true });
Check.Equal(2, result.Legs.Count); Check.That(result.IsLoop);
Check.Equal(1600m, result.NetProfit); Check.Equal(11600m, result.Legs[^1].CashAfter);
}
[Test] public void ReserveIsNeverSpent()
{
var result = Run(request: Check.Request with { Credits = 1500, ReserveCredits = 1000 });
Check.That(result.Legs.Count > 0); Check.That(result.Legs[0].Orders.Sum(o => o.Cost) <= 500);
}
[Test] public void StaleQuotesDoNotBecomeProfitableRoutes()
{
var b = Check.TwoStations; b = b with { Quotes = b.Quotes.Select(q => q with { CollectedAt = DateTimeOffset.UtcNow.AddDays(-2) }).ToArray() };
Check.Equal(0, Run(b).Legs.Count);
}
[Test] public void FutureQuotesAreRejected()
{
var b = Check.TwoStations with { Quotes = Check.TwoStations.Quotes.Select(q => q with { CollectedAt = DateTimeOffset.UtcNow.AddHours(2) }).ToArray() };
Check.Equal(0, Run(b).Legs.Count);
}
[Test] public void IllegalGoodsAreExcluded()
{
var b = Check.TwoStations with { Quotes = Check.TwoStations.Quotes.Select(q => q with { Illegal = true }).ToArray() };
Check.Equal(0, Run(b).Legs.Count);
}
[Test] public void LargeShipsCannotUseMediumPads()
{
var b = Check.TwoStations with { Stations = [Check.Station(1), Check.Station(2, 10) with { MaxLandingPad = LandingPad.Medium }] };
Check.Equal(0, Run(b).Legs.Count);
}
[Test] public void ConservativeRiskExcludesAnarchy()
{
var b = Check.TwoStations with { Stations = [Check.Station(1), Check.Station(2, 10) with { Security = SecurityLevel.Anarchy }] };
Check.Equal(0, Run(b, Check.Request with { Risk = RiskTolerance.Conservative }).Legs.Count);
}
[Test] public void UnknownMetadataIsExcludedByDefault()
{
var b = Check.TwoStations with { Stations = [Check.Station(1), Check.Station(2, 10) with { DistanceToArrivalLs = null }] };
Check.Equal(0, Run(b).Legs.Count);
}
[Test] public void ExcludesPermitRestrictedStations()
{
var b = Check.TwoStations with { Stations = [Check.Station(1), Check.Station(2, 10) with { RequiresPermit = true }] };
Check.Equal(0, Run(b).Legs.Count);
}
[Test] public void CountsSupplyAcrossRepeatedLoopVisits()
{
var b = Check.TwoStations with { Quotes = Check.TwoStations.Quotes.Select(q => q with { Supply = 2, Demand = 3 }).ToArray() };
var result = Run(b, Check.Request with { Loop = true, MaxLegs = 6, Objective = RouteObjective.TotalProfit });
foreach (var group in result.Legs.SelectMany(l => l.Orders.Select(o => (l.From.MarketId, o))).GroupBy(x => (x.MarketId, x.o.Commodity))) Check.That(group.Sum(x => x.o.Quantity) <= 2);
}
[Test] public void DeductsOperatingCosts()
{
var result = Run(request: Check.Request with { MaxLegs = 1, CostPerJump = 100 });
Check.Equal(700m, result.NetProfit);
}
[Test] public void RejectsZeroJumpRange() => Check.Throws<ArgumentException>(() => Run(request: Check.Request with { LadenJumpRangeLy = 0 }));
[Test] public void RejectsAllCashReserved() => Check.Throws<ArgumentException>(() => Run(request: Check.Request with { ReserveCredits = 10000 }));
[Test] public void DemoLoopContainsMixedCargo()
{
var p = DemoData.Player;
var result = Run(DemoData.CreateBook(), Check.Request with { StartMarketId = p.MarketId!.Value, Credits = p.Credits!.Value, ReserveCredits = p.Rebuy, CargoCapacity = p.FreeCargo, Loop = true, MaxLegs = 4 });
Check.That(result.NetProfit > 0 && result.IsLoop); Check.That(result.Legs.Any(l => l.Orders.Count > 1));
}
[Test] public void ReportsInputTruncation() => Check.That(Run(Check.TwoStations with { Truncated = true }).Warnings.Any(w => w.Contains("截断")));
}