From 9e1782c40170599a7cc554ce0305f98278e6700e Mon Sep 17 00:00:00 2001 From: Jaculabilis Date: Tue, 15 Mar 2022 17:24:16 -0700 Subject: [PATCH] Create fluent interface for building test cases --- MultiversalDiplomacyTests/TestCaseBuilder.cs | 422 ++++++++++++++++++ .../TestCaseBuilderTest.cs | 115 +++++ 2 files changed, 537 insertions(+) create mode 100644 MultiversalDiplomacyTests/TestCaseBuilder.cs create mode 100644 MultiversalDiplomacyTests/TestCaseBuilderTest.cs diff --git a/MultiversalDiplomacyTests/TestCaseBuilder.cs b/MultiversalDiplomacyTests/TestCaseBuilder.cs new file mode 100644 index 0000000..cd42f3d --- /dev/null +++ b/MultiversalDiplomacyTests/TestCaseBuilder.cs @@ -0,0 +1,422 @@ +using System.Collections.ObjectModel; + +using MultiversalDiplomacy.Model; +using MultiversalDiplomacy.Orders; + +namespace MultiversalDiplomacyTests; + +/// +/// A fluent interface for defining adjudication test cases. +/// +public class TestCaseBuilder +{ + /// + /// Context for defining orders given by a power. + /// + public interface IPowerContext + { + /// + /// Get the context for defining the orders for another power. + /// + public IPowerContext this[string powerName] { get; } + + /// + /// Define an order for an army in a province. + /// + public IUnitContext Army(string provinceName); + + /// + /// Define an order for a fleet in a province, optionally on a specific coast. + /// + public IUnitContext Fleet(string provinceName, string? coast = null); + } + + /// + /// Context for defining an order given to a unit. + /// + public interface IUnitContext + { + /// + /// Ensure the unit exists, but don't create an order for it. + /// + public IPowerContext Exists(); + + /// + /// Give the unit a hold order. + /// + public IPowerContext Holds(); + + /// + /// Give the unit a move order. + /// + public IPowerContext MovesTo(string provinceName, string? coast = null); + + /// + /// Give the unit a convoy order. + /// + public IConvoyContext Convoys { get; } + + /// + /// Give the unit a support order. + /// + public ISupportContext Supports { get; } + } + + /// + /// Context for defining a convoy order. + /// + public interface IConvoyContext + { + /// + /// Make the convoy order target an army. + /// + public IConvoyDestinationContext Army(string provinceName, string? powerName = null); + + /// + /// Make the convoy order target a fleet. + /// + public IConvoyDestinationContext Fleet( + string provinceName, + string? coast = null, + string? powerName = null); + } + + /// + /// Context for defining the destination of a convoy order. + /// + public interface IConvoyDestinationContext + { + /// + /// Define the destination of the convoy order. + /// + public IPowerContext To(string provinceName); + } + + /// + /// Context for defining a support order. + /// + public interface ISupportContext + { + /// + /// Make the support order target an army. + /// + public ISupportTypeContext Army(string provinceName, string? powerName = null); + + /// + /// Make the support order target a fleet. + /// + public ISupportTypeContext Fleet( + string provinceName, + string? coast = null, + string? powerName = null); + } + + /// + /// Context for defining the type of support order. + /// + public interface ISupportTypeContext + { + /// + /// Give the unit an order to support the target's hold order. + /// + public IPowerContext Hold(); + + /// + /// Give the unit an order to support the target's move order. + /// + public IPowerContext MoveTo(string provinceName, string? coast = null); + } + + public World World { get; private set; } + public ReadOnlyCollection Orders { get; } + private List OrderList; + private Season Season; + + /// + /// Create a test case builder that will operate on a world. + /// + public TestCaseBuilder(World world, Season? season = null) + { + this.World = world; + this.OrderList = new List(); + this.Orders = new(this.OrderList); + this.Season = season ?? this.World.Seasons.First(); + } + + /// + /// Get the context for defining the orders for a power. + /// + public IPowerContext this[string powerName] + { + get + { + Power power = this.World.GetPower(powerName); + return new PowerContext(this, power); + } + } + + /// + /// Get a unit matching a description. If no such unit exists, one is created and added to the + /// . + /// + /// + /// The unit type to create if the unit does not exist. + /// Per DATC 4.C.1-2, mismatching unit designations should not invalidate an order, which + /// effectively makes the designations superfluous. To support this, the test case builder + /// returns a unit that matches the power, location, and season even if the unit found is not + /// of this type. + /// + private Unit GetOrBuildUnit( + Power power, + Location location, + Season season, + UnitType type) + { + foreach (Unit unit in this.World.Units) + { + if (unit.Power == power + && unit.Location == location + && unit.Season == season) + { + return unit; + } + } + + // Not found + Unit newUnit = Unit.Build(location, season, power, type); + this.World = this.World.WithUnits(this.World.Units.Append(newUnit)); + return newUnit; + } + + private class PowerContext : IPowerContext + { + public TestCaseBuilder Builder; + public Power Power; + + public PowerContext(TestCaseBuilder Builder, Power Power) + { + this.Builder = Builder; + this.Power = Power; + } + + public IPowerContext this[string powerName] + => this.Builder[powerName]; + + public IUnitContext Army(string provinceName) + { + Location location = this.Builder.World.GetLand(provinceName); + Unit unit = this.Builder.GetOrBuildUnit( + this.Power, location, this.Builder.Season, UnitType.Army); + return new UnitContext(this, unit); + } + + public IUnitContext Fleet(string provinceName, string? coast = null) + { + Location location = this.Builder.World.GetWater(provinceName, coast); + Unit unit = this.Builder.GetOrBuildUnit( + this.Power, location, this.Builder.Season, UnitType.Fleet); + return new UnitContext(this, unit); + } + } + + private class UnitContext : IUnitContext + { + public TestCaseBuilder Builder; + public PowerContext PowerContext; + public Unit Unit; + + public UnitContext(PowerContext powerContext, Unit unit) + { + this.Builder = powerContext.Builder; + this.PowerContext = powerContext; + this.Unit = unit; + } + + /// + /// Declare that a unit exists without giving it an order. + /// + public IPowerContext Exists() + => this.PowerContext; + + /// + /// Order a unit to hold. + /// + public IPowerContext Holds() + { + HoldOrder order = new HoldOrder(this.PowerContext.Power, this.Unit); + this.Builder.OrderList.Add(order); + return this.PowerContext; + } + + /// + /// Order a unit to move to a destination. + /// + public IPowerContext MovesTo(string provinceName, string? coast = null) + { + Location destination = this.Unit.Type == UnitType.Army + ? this.Builder.World.GetLand(provinceName) + : this.Builder.World.GetWater(provinceName, coast); + MoveOrder moveOrder = new MoveOrder( + this.PowerContext.Power, + this.Unit, + this.Builder.Season, + destination); + this.Builder.OrderList.Add(moveOrder); + return this.PowerContext; + } + + public IConvoyContext Convoys + => new ConvoyContext(this); + + public ISupportContext Supports + => new SupportContext(this); + } + + private class ConvoyContext : IConvoyContext + { + public TestCaseBuilder Builder; + public PowerContext PowerContext; + public UnitContext UnitContext; + + public ConvoyContext(UnitContext unitContext) + { + this.Builder = unitContext.Builder; + this.PowerContext = unitContext.PowerContext; + this.UnitContext = unitContext; + } + + public IConvoyDestinationContext Army(string provinceName, string? powerName = null) + { + Power power = powerName == null + ? this.PowerContext.Power + : this.Builder.World.GetPower(powerName); + Location location = this.Builder.World.GetLand(provinceName); + Unit unit = this.Builder.GetOrBuildUnit( + power, location, this.Builder.Season, UnitType.Army); + return new ConvoyDestinationContext(this, unit); + } + + public IConvoyDestinationContext Fleet( + string provinceName, + string? coast = null, + string? powerName = null) + { + Power power = powerName == null + ? this.PowerContext.Power + : this.Builder.World.GetPower(powerName); + Location location = this.Builder.World.GetWater(provinceName, coast); + Unit unit = this.Builder.GetOrBuildUnit( + power, location, this.Builder.Season, UnitType.Fleet); + return new ConvoyDestinationContext(this, unit); + } + } + + private class ConvoyDestinationContext : IConvoyDestinationContext + { + public TestCaseBuilder Builder; + public PowerContext PowerContext; + public UnitContext UnitContext; + public Unit Target; + + public ConvoyDestinationContext(ConvoyContext convoyContext, Unit target) + { + this.Builder = convoyContext.Builder; + this.PowerContext = convoyContext.PowerContext; + this.UnitContext = convoyContext.UnitContext; + this.Target = target; + } + + public IPowerContext To(string provinceName) + { + Location location = this.Builder.World.GetLand(provinceName); + ConvoyOrder order = new ConvoyOrder( + this.PowerContext.Power, + this.UnitContext.Unit, + this.Target, + this.Builder.Season, + location); + this.Builder.OrderList.Add(order); + return this.PowerContext; + } + } + + private class SupportContext : ISupportContext + { + public TestCaseBuilder Builder; + public PowerContext PowerContext; + public UnitContext UnitContext; + + public SupportContext(UnitContext unitContext) + { + this.Builder = unitContext.Builder; + this.PowerContext = unitContext.PowerContext; + this.UnitContext = unitContext; + } + + public ISupportTypeContext Army(string provinceName, string? powerName = null) + { + Power power = powerName == null + ? this.PowerContext.Power + : this.Builder.World.GetPower(powerName); + Location location = this.Builder.World.GetLand(provinceName); + Unit unit = this.Builder.GetOrBuildUnit( + power, location, this.Builder.Season, UnitType.Army); + return new SupportTypeContext(this, unit); + } + + public ISupportTypeContext Fleet( + string provinceName, + string? coast = null, + string? powerName = null) + { + Power power = powerName == null + ? this.PowerContext.Power + : this.Builder.World.GetPower(powerName); + Location location = this.Builder.World.GetWater(provinceName, coast); + Unit unit = this.Builder.GetOrBuildUnit( + power, location, this.Builder.Season, UnitType.Fleet); + return new SupportTypeContext(this, unit); + } + } + + private class SupportTypeContext : ISupportTypeContext + { + public TestCaseBuilder Builder; + public PowerContext PowerContext; + public UnitContext UnitContext; + public Unit Target; + + public SupportTypeContext(SupportContext supportContext, Unit target) + { + this.Builder = supportContext.Builder; + this.PowerContext = supportContext.PowerContext; + this.UnitContext = supportContext.UnitContext; + this.Target = target; + } + + public IPowerContext Hold() + { + SupportHoldOrder order = new SupportHoldOrder( + this.PowerContext.Power, + this.UnitContext.Unit, + this.Target); + this.Builder.OrderList.Add(order); + return this.PowerContext; + } + + public IPowerContext MoveTo(string provinceName, string? coast = null) + { + Location destination = this.Target.Type == UnitType.Army + ? this.Builder.World.GetLand(provinceName) + : this.Builder.World.GetWater(provinceName, coast); + SupportMoveOrder order = new SupportMoveOrder( + this.PowerContext.Power, + this.UnitContext.Unit, + this.Target, + this.Builder.Season, + destination); + this.Builder.OrderList.Add(order); + return this.PowerContext; + } + } +} diff --git a/MultiversalDiplomacyTests/TestCaseBuilderTest.cs b/MultiversalDiplomacyTests/TestCaseBuilderTest.cs new file mode 100644 index 0000000..26be408 --- /dev/null +++ b/MultiversalDiplomacyTests/TestCaseBuilderTest.cs @@ -0,0 +1,115 @@ +using MultiversalDiplomacy.Model; +using MultiversalDiplomacy.Orders; + +using NUnit.Framework; + +namespace MultiversalDiplomacyTests; + +class TestCaseBuilderTest +{ + [Test] + public void BuilderCreatesUnits() + { + TestCaseBuilder setup = new(World.WithStandardMap().WithInitialSeason()); + + Assert.That(setup.World.Powers.Count(), Is.EqualTo(7), "Unexpected power count"); + Assert.That(setup.World.Units, Is.Empty, "Expected no units to be created yet"); + + setup + ["England"] + .Army("London").Exists() + .Fleet("Irish Sea").Exists() + ["Russia"] + .Fleet("Saint Petersburg", "west coast").Exists(); + + Assert.That(setup.Orders, Is.Empty, "Expected no orders to be created yet"); + Assert.That(setup.World.Units, Is.Not.Empty, "Expected units to be created"); + + Unit armyLON = setup.World.GetUnitAt("London") + ?? throw new AssertionException("Expected a unit in London"); + Assert.That(armyLON.Power.Name, Is.EqualTo("England"), "Unit created with wrong power"); + Assert.That(armyLON.Type, Is.EqualTo(UnitType.Army), "Unit created with wrong type"); + + Unit fleetIRI = setup.World.GetUnitAt("Irish Sea") + ?? throw new AssertionException("Expected a unit in Irish Sea"); + Assert.That(fleetIRI.Power.Name, Is.EqualTo("England"), "Unit created with wrong power"); + Assert.That(fleetIRI.Type, Is.EqualTo(UnitType.Fleet), "Unit created with wrong type"); + + Unit fleetSTP = setup.World.GetUnitAt("Saint Petersburg") + ?? throw new AssertionException("Expected a unit in Saint Petersburg"); + Assert.That(fleetSTP.Power.Name, Is.EqualTo("Russia"), "Unit created with wrong power"); + Assert.That(fleetSTP.Type, Is.EqualTo(UnitType.Fleet), "Unit created with wrong type"); + Assert.That( + fleetSTP.Location, + Is.EqualTo(setup.World.GetWater("STP", "wc")), + "Unit created on wrong coast"); + } + + [Test] + public void BuilderCreatesOrders() + { + TestCaseBuilder setup = new(World.WithStandardMap().WithInitialSeason()); + + Assert.That(setup.World.Powers.Count(), Is.EqualTo(7), "Unexpected power count"); + Assert.That(setup.World.Units, Is.Empty, "Expected no units to be created yet"); + Assert.That(setup.Orders, Is.Empty, "Expected no orders to be created yet"); + + setup + ["Germany"] + .Army("Berlin").MovesTo("Kiel") + .Army("Prussia").Holds() + ["England"] + .Fleet("North Sea").Convoys.Army("London").To("Holland") + ["France"] + .Army("Kiel").Supports.Army("London", powerName: "England").MoveTo("Holland") + .Army("Munich").Supports.Army("Kiel").Hold(); + + Assert.That(setup.Orders, Is.Not.Empty, "Expected orders to be created"); + Assert.That(setup.World.Units, Is.Not.Empty, "Expected units to be created"); + List orders = setup.Orders.OfType().ToList(); + + Func OrderForProvince(string name) + => order => order.Unit.Location.Province.Name == name; + + UnitOrder orderBer = orders.Single(OrderForProvince("Berlin")); + Assert.That(orderBer, Is.InstanceOf(), "Unexpected order type"); + Assert.That( + (orderBer as MoveOrder)?.Location, + Is.EqualTo(setup.World.GetLand("Kiel")), + "Unexpected move order destination"); + + UnitOrder orderPru = orders.Single(OrderForProvince("Prussia")); + Assert.That(orderPru, Is.InstanceOf(), "Unexpected order type"); + + UnitOrder orderNth = orders.Single(OrderForProvince("North Sea")); + Assert.That(orderNth, Is.InstanceOf(), "Unexpected order type"); + Assert.That( + (orderNth as ConvoyOrder)?.Target, + Is.EqualTo(setup.World.GetUnitAt("London")), + "Unexpected convoy order target"); + Assert.That( + (orderNth as ConvoyOrder)?.Location, + Is.EqualTo(setup.World.GetLand("Holland")), + "Unexpected convoy order destination"); + + UnitOrder orderKie = orders.Single(OrderForProvince("Kiel")); + Assert.That(orderKie, Is.InstanceOf(), "Unexpected order type"); + Assert.That( + (orderKie as SupportMoveOrder)?.Target, + Is.EqualTo(setup.World.GetUnitAt("London")), + "Unexpected convoy order target"); + Assert.That( + (orderKie as SupportMoveOrder)?.Location, + Is.EqualTo(setup.World.GetLand("Holland")), + "Unexpected convoy order destination"); + + UnitOrder orderMun = orders.Single(OrderForProvince("Munich")); + Assert.That(orderMun, Is.InstanceOf(), "Unexpected order type"); + Assert.That( + (orderMun as SupportHoldOrder)?.Target, + Is.EqualTo(setup.World.GetUnitAt("Kiel")), + "Unexpected convoy order target"); + + Assert.That(orders.Where(OrderForProvince("London")), Is.Empty, "Unexpected order"); + } +}