Rework the depot, warehouse and vehicle base

A consist added to the warehouse gets a name there. The crew type stays through
loading a consist. The vehicle base window replaces the texture base. The depot
follows consists changed on the scenarios page, also with the warehouse tab
left open.
This commit is contained in:
maj00r
2026-11-01 16:06:16 +01:00
parent f152a020e3
commit 82429010d4
40 changed files with 3041 additions and 843 deletions

View File

@@ -0,0 +1,149 @@
using StarterNG.Classes;
using StarterNG.Domain;
using StarterNG.Domain.Vehicles;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests;
public class CargoTests
{
private readonly Consist _consist;
private readonly VehicleCatalog _catalog;
private readonly VehicleInfo _info;
public CargoTests()
{
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(TestInstallation.At("dynamic", "pkp", "en57", "textures.txt"), "!=b\nen57-a.mat=en57,EN57")
.WithFile(TestInstallation.At("dynamic", "pkp", "en57", "en57.fiz"),
"Load: LoadAccepted=Passengers,Pantstate MaxLoad=50")
.WithFile(TestInstallation.At("dynamic", "pkp", "tram", "textures.txt"), "!=b\ntram-a.mat=tram,TRAM")
.WithFile(TestInstallation.At("dynamic", "pkp", "tram", "tram.fiz"),
"Load: LoadAccepted=pantstate,Passengers MaxLoad=52")
.WithFile(TestInstallation.At("dynamic", "pkp", "s1", "textures.txt"), "!=b\ns1-a.mat=s1,S1")
.WithFile(TestInstallation.At("dynamic", "pkp", "s1", "s1.fiz"), "Load: LoadAccepted=Coal,Sand"));
installation.Vehicles.Load(installation.Library.Vehicles);
_catalog = installation.Library.Vehicles;
_info = new VehicleInfo(_catalog, installation.Physics);
_consist = new Consist(_catalog, _info);
}
private (Dynamic Car, List<ConsistItem> Consist) Wagon(string skin, string? type = null, int count = 0)
{
var car = _consist.MakeDynamic(_catalog.TextureForSkin(skin)!, null);
car.LoadType = type;
car.LoadCount = count;
return (car, new List<ConsistItem> { new() { Cars = new List<Dynamic> { car } } });
}
private Cargo Picking(int index) => new(_info, new Pick(index));
private sealed class Pick : Random
{
private readonly int _index;
public Pick(int index) => _index = index;
public override int Next(int maxValue) => _index;
public override int Next(int minValue, int maxValue) => maxValue - 1;
}
[Fact]
public void A_random_type_takes_no_more_than_it_holds()
{
var (car, consist) = Wagon("en57-a", "Passengers", 50);
Picking(1).RandomTypes(consist);
Assert.Equal("Pantstate", car.LoadType);
Assert.Equal(Dynamic.PantStateMax, car.LoadCount);
Assert.Contains(" 3 Pantstate ", car.ToTrainsetNode());
}
[Fact]
public void A_random_type_leaves_a_count_it_can_hold()
{
var (car, consist) = Wagon("en57-a", "Pantstate", 2);
Picking(0).RandomTypes(consist);
Assert.Equal("Passengers", car.LoadType);
Assert.Equal(2, car.LoadCount);
}
[Fact]
public void A_random_type_with_no_limit_declared_keeps_the_count()
{
// MaxLoad unset: nothing to hold it to.
var (car, consist) = Wagon("s1-a", "Coal", 40);
Picking(1).RandomTypes(consist);
Assert.Equal("Sand", car.LoadType);
Assert.Equal(40, car.LoadCount);
}
[Fact]
public void Filling_an_empty_car_goes_by_the_type_it_is_given()
{
// Its first accepted type is pantographs: the 52 of the .fiz is not theirs.
var (car, _) = Wagon("tram-a");
Picking(0).FillToMax(car);
Assert.Equal("pantstate", car.LoadType);
Assert.Equal(Dynamic.PantStateMax, car.LoadCount);
}
[Fact]
public void A_random_amount_for_an_empty_car_goes_by_the_type_it_is_given()
{
var (car, consist) = Wagon("tram-a");
Picking(0).RandomAmounts(consist);
Assert.Equal("pantstate", car.LoadType);
Assert.InRange(car.LoadCount, 1, Dynamic.PantStateMax);
}
[Fact]
public void A_count_passed_on_keeps_where_the_next_car_declares_no_limit()
{
// MaxLoad unset: nothing to hold it to, as when taken from the car before.
var (_, consist) = Wagon("s1-a", "Coal", 40);
var (next, following) = Wagon("s1-a");
consist.AddRange(following);
Picking(0).CopyToFollowing(consist, consist[0]);
Assert.Equal("Coal", next.LoadType);
Assert.Equal(40, next.LoadCount);
}
[Fact]
public void A_count_passed_on_is_held_to_what_the_next_car_declares()
{
var (_, consist) = Wagon("tram-a", "Passengers", 52);
var (next, following) = Wagon("en57-a");
consist.AddRange(following);
Picking(0).CopyToFollowing(consist, consist[0]);
Assert.Equal("Passengers", next.LoadType);
Assert.Equal(50, next.LoadCount);
}
[Fact]
public void A_car_with_nothing_declared_stays_untouched()
{
var (car, _) = Wagon("en57-a");
car.MaxLoad = 0;
Picking(0).FillToMax(car);
Assert.Null(car.LoadType);
Assert.Equal(0, car.LoadCount);
}
}

View File

@@ -9,6 +9,7 @@ public class ConsistCrewTests
{
private readonly Consist _consist;
private readonly VehicleCatalog _catalog;
private readonly VehicleInfo _info;
public ConsistCrewTests()
{
@@ -20,7 +21,129 @@ public class ConsistCrewTests
installation.Vehicles.Load(installation.Library.Vehicles);
_catalog = installation.Library.Vehicles;
_consist = new Consist(_catalog, new VehicleInfo(_catalog, installation.Physics));
_info = new VehicleInfo(_catalog, installation.Physics);
_consist = new Consist(_catalog, _info);
}
private List<Dynamic> Cars(params string[] skins) =>
skins.Select(skin => _consist.MakeDynamic(_catalog.TextureForSkin(skin)!, null)).ToList();
[Theory]
[InlineData(eDriverType.Reardriver)]
[InlineData(eDriverType.Passenger)]
public void A_loaded_consist_keeps_the_kind_of_crew(eDriverType crew)
{
var before = Cars("ep07-001", "b111-01");
before[0].DriverType = crew;
var loaded = Cars("ep07-002", "b111-01");
loaded[0].DriverType = eDriverType.Headdriver;
var cars = Consist.LoadedOver(before, loaded, _catalog, _info);
Assert.Equal(crew, cars[0].DriverType);
Assert.Equal(eDriverType.Nobody, cars[1].DriverType);
}
[Fact]
public void A_consist_with_nobody_in_it_stays_so_after_loading()
{
var loaded = Cars("ep07-002");
loaded[0].DriverType = eDriverType.Headdriver;
var cars = Consist.LoadedOver(Cars("ep07-001"), loaded, _catalog, _info);
Assert.Equal(eDriverType.Nobody, cars[0].DriverType);
}
[Fact]
public void A_driver_does_not_go_into_loaded_wagons()
{
var before = Cars("ep07-001");
before[0].DriverType = eDriverType.Headdriver;
var cars = Consist.LoadedOver(before, Cars("b111-01"), _catalog, _info);
Assert.Equal(eDriverType.Nobody, cars[0].DriverType);
}
[Fact]
public void Loaded_over_an_empty_consist_the_vehicles_keep_their_own_crew()
{
var loaded = Cars("ep07-002");
loaded[0].DriverType = eDriverType.Reardriver;
Assert.Equal(eDriverType.Reardriver,
Consist.LoadedOver(new List<Dynamic>(), loaded, _catalog, _info)[0].DriverType);
}
[Fact]
public void Vehicles_joining_a_consist_come_without_their_own_crew()
{
var before = Cars("ep07-001");
before[0].DriverType = eDriverType.Headdriver;
var added = Cars("ep07-002");
added[0].DriverType = eDriverType.Headdriver;
var cars = Consist.Joined(before, added);
Assert.Equal(eDriverType.Headdriver, cars[0].DriverType);
Assert.Equal(eDriverType.Nobody, cars[1].DriverType);
}
[Fact]
public void A_locomotive_moved_to_the_head_takes_the_driver_in_the_same_cab()
{
var first = Add("ep07-001", 0);
var second = Add("ep07-002", 1);
first.Driver = eDriverType.Reardriver;
first.Cars[0].DriverType = eDriverType.Reardriver;
_consist.Move(1, 0);
Assert.Equal(eDriverType.Reardriver, second.Cars[0].DriverType);
Assert.Equal(eDriverType.Nobody, first.Cars[0].DriverType);
}
[Fact]
public void Vehicles_joining_an_empty_consist_keep_their_crew()
{
var added = Cars("ep07-002");
added[0].DriverType = eDriverType.Headdriver;
Assert.Equal(eDriverType.Headdriver, Consist.Joined(new List<Dynamic>(), added)[0].DriverType);
}
[Fact]
public void Selecting_a_wagon_keeps_the_locomotive_as_the_starting_vehicle()
{
var loco = Add("ep07-001", 0);
var wagon = Add("b111-01", 1);
loco.Cars[0].Name = "loco";
wagon.Cars[0].Name = "wagon";
Assert.Equal("loco", Consist.StartingVehicle(_consist, wagon, "loco"));
}
[Fact]
public void A_wagon_selected_after_a_stale_pick_falls_back_to_the_driver()
{
var loco = Add("ep07-001", 0);
var wagon = Add("b111-01", 1);
loco.Cars[0].Name = "loco";
Assert.Equal("loco", Consist.StartingVehicle(_consist, wagon, "gone"));
}
[Fact]
public void Selecting_a_crewed_unit_makes_it_the_starting_vehicle()
{
var first = Add("ep07-001", 0);
var wagon = Add("b111-01", 1);
first.Cars[0].Name = "loco";
wagon.Cars[0].Name = "wagon";
wagon.Cars[0].DriverType = eDriverType.Passenger;
Assert.Equal("wagon", Consist.StartingVehicle(_consist, wagon, "loco"));
}
[Fact]

View File

@@ -0,0 +1,98 @@
using StarterNG.Classes;
using StarterNG.Domain;
namespace StarterNG.Tests;
public class ConsistHistoryTests
{
private static Dynamic Car(string name) => new()
{
Name = name, DataFolder = "pkp", SkinFile = name, MmdFile = name, PathName = ""
};
private static Trainset With(params string[] names) =>
new("trainset none none 0 0\nendtrainset") { Vehicles = names.Select(Car).ToList() };
private static string[] Names(IEnumerable<Dynamic> vehicles) => vehicles.Select(v => v.Name).ToArray();
[Fact]
public void A_redraw_without_a_change_leaves_nothing_to_undo()
{
var history = new ConsistHistory();
var trainset = With("a", "b");
history.Record(trainset, 0);
history.Record(trainset, 1);
Assert.False(history.CanUndo(trainset));
}
[Fact]
public void Undo_goes_back_one_change_and_redo_forward_again()
{
var history = new ConsistHistory();
var trainset = With("a");
history.Record(trainset, 0);
trainset.Vehicles.Add(Car("b"));
history.Record(trainset, 1);
var back = history.Undo(trainset, 1);
Assert.Equal(new[] { "a" }, Names(back!.Vehicles));
Assert.True(history.CanRedo(trainset));
trainset.Vehicles = back.Vehicles.ToList();
history.Record(trainset, 0, rebase: true);
var forward = history.Redo(trainset, 0);
Assert.Equal(new[] { "a", "b" }, Names(forward!.Vehicles));
Assert.Equal(1, forward.Selected);
}
[Fact]
public void An_edit_made_in_place_is_still_undone()
{
// The vehicles are shared with the editor, so a change to one of them must
// not reach the state kept for undo.
var history = new ConsistHistory();
var trainset = With("a");
history.Record(trainset, 0);
trainset.Vehicles[0].DriverType = eDriverType.Passenger;
var back = history.Undo(trainset, 0);
Assert.NotEqual(eDriverType.Passenger, back!.Vehicles[0].DriverType);
}
[Fact]
public void A_new_change_drops_what_could_be_redone()
{
var history = new ConsistHistory();
var trainset = With("a");
history.Record(trainset, 0);
trainset.Vehicles.Add(Car("b"));
history.Record(trainset, 1);
trainset.Vehicles = history.Undo(trainset, 1)!.Vehicles.ToList();
history.Record(trainset, 0, rebase: true);
trainset.Vehicles.Add(Car("c"));
history.Record(trainset, 1);
Assert.False(history.CanRedo(trainset));
}
[Fact]
public void Each_trainset_has_its_own_history()
{
var history = new ConsistHistory();
var first = With("a");
var second = With("x");
history.Record(first, 0);
first.Vehicles.Add(Car("b"));
history.Record(first, 1);
history.Record(second, 0);
Assert.True(history.CanUndo(first));
Assert.False(history.CanUndo(second));
}
}

View File

@@ -1,3 +1,4 @@
using StarterNG.Application;
using StarterNG.Classes;
using StarterNG.Domain;
using StarterNG.Domain.Vehicles;
@@ -87,6 +88,25 @@ public class ConsistOrientationTests
Assert.All(consist, card => Assert.Equal(Consist.IsTurned(card.Cars[0]), card.Flipped));
}
[Fact]
public void A_turned_unit_comes_back_as_it_was_from_every_load()
{
AppServices.Initialize(_installation.Paths, _installation.Files, _installation.Clock, _installation.Environment);
var consist = NewConsist(out var unit);
consist.Flip(unit);
string[] turned = Describe(consist);
var trainset = new Trainset("trainset a tor 0 0\nendtrainset");
for (int load = 0; load < 3; load++)
{
trainset.Vehicles = consist.Flatten();
consist.LoadFrom(trainset);
Assert.True(Assert.Single(consist).Flipped);
Assert.Equal(turned, Describe(consist));
}
}
private Consist NewConsist(out ConsistItem unit)
{
var consist = new Consist(_catalog, new VehicleInfo(_catalog, _installation.Physics));

View File

@@ -21,6 +21,21 @@ public class ConsistTailTests
_consist = new Consist(_catalog, new VehicleInfo(_catalog, installation.Physics));
}
[Fact]
public void A_unit_can_join_the_one_it_is_held_to_from_either_side()
{
var first = Append("b111-01");
var second = Append("b111-02");
Assert.False(_consist.CanJoin(first, 0));
Assert.False(_consist.CanJoin(second, 1));
first.Cars[0].Coupling.Set(Coupling.WorkshopLock, true);
Assert.True(_consist.CanJoin(first, 0));
Assert.True(_consist.CanJoin(second, 1));
}
[Fact]
public void The_vehicle_count_counts_cars_not_units()
{

View File

@@ -74,6 +74,70 @@ public class PhysicsAndAssetsTests
Assert.NotNull(installation.Physics.For("pkp/ep07/", "ep07"));
}
[Theory]
[InlineData("pkp/21xm_v1/")]
[InlineData("pkp/21xm_v1")]
[InlineData("PKP\\21XM_V1")]
[InlineData("dynamic/pkp/21xm_v1/")]
public void Two_fiz_files_of_one_name_are_told_apart_by_their_folder(string folder)
{
// As in the game: dynamic/**/21xm_v1/214m.fiz and sa134_v1/214m.fiz.
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(Fiz("pkp", "21xm_v1", "214m"), "Param: M=51000\nDimensions: L=24.38")
.WithFile(Fiz("pkp", "sa134_v1", "214m"), "Param: M=48000\nDimensions: L=24.5"));
var physics = installation.Physics.For(folder, "214m.e3d")!;
Assert.Equal(51000, physics.Mass);
Assert.Equal(24.38, physics.Length);
Assert.Equal(48000, installation.Physics.For("pkp/sa134_v1/", "214m")!.Mass);
// Asked again, the first answer has not stuck to the name.
Assert.Equal(51000, installation.Physics.For(folder, "214m")!.Mass);
}
[Fact]
public void A_folder_without_the_fiz_falls_back_to_the_name_alone()
{
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(Fiz("pkp", "ep07", "ep07"), "Param: M=80000"));
Assert.Equal(80000, installation.Physics.For("pkp/other/", "ep07")!.Mass);
Assert.Equal(80000, installation.Physics.For(null, "ep07")!.Mass);
}
[Fact]
public void The_fallback_to_a_name_two_folders_share_is_the_same_on_every_machine()
{
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(Fiz("zz", "b", "214m"), "Param: M=2")
.WithFile(Fiz("aa", "a", "214m"), "Param: M=1"));
Assert.Equal(1, installation.Physics.For("elsewhere/", "214m")!.Mass);
}
[Fact]
public void A_dumb_variant_in_the_vehicles_own_folder_goes_before_a_namesake_elsewhere()
{
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(Fiz("pkp", "ep07", "ep07dumb"), "Param: M=1000")
.WithFile(Fiz("pkp", "other", "ep07"), "Param: M=2000"));
Assert.Equal(1000, installation.Physics.For("pkp/ep07/", "ep07")!.Mass);
}
[Fact]
public void Two_folders_sharing_a_name_count_once_among_what_is_indexed()
{
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(Fiz("pkp", "a", "214m"), "Param: M=1")
.WithFile(Fiz("pkp", "b", "214m"), "Param: M=2")
.WithFile(Fiz("pkp", "c", "ep07"), "Param: M=3"));
installation.Physics.Preload();
Assert.Equal(2, installation.Physics.IndexedCount);
}
[Fact]
public void An_unknown_model_has_no_physics_and_an_empty_installation_indexes_nothing()
{

View File

@@ -0,0 +1,47 @@
using StarterNG.Domain.Vehicles;
namespace StarterNG.Tests;
public class RevisionSpanTests
{
[Theory]
[InlineData("14.01.2005", "2005-01-14", "2005-01-14")]
[InlineData("06.12.2016", "2016-12-06", "2016-12-06")]
[InlineData("xx.06.2008", "2008-06-01", "2008-06-30")]
[InlineData("?.06.2008", "2008-06-01", "2008-06-30")]
[InlineData("xx.xx.2011", "2011-01-01", "2011-12-31")]
[InlineData("?.?.2011", "2011-01-01", "2011-12-31")]
[InlineData("02.2016", "2016-02-01", "2016-02-29")]
public void A_credit_line_date_is_as_exact_as_it_says(string text, string from, string to)
{
var span = RevisionSpan.Parse(text);
Assert.Equal(DateTime.Parse(from), span?.From);
Assert.Equal(DateTime.Parse(to), span?.To);
}
[Theory]
[InlineData("2016", "2016-01-01", "2016-12-31")]
[InlineData("2016-12", "2016-12-01", "2016-12-31")]
[InlineData("2016-12-06", "2016-12-06", "2016-12-06")]
[InlineData(" 2016-12- ", "2016-12-01", "2016-12-31")]
public void What_is_typed_to_filter_by_reads_the_same_way(string text, string from, string to)
{
var span = RevisionSpan.Parse(text);
Assert.Equal(DateTime.Parse(from), span?.From);
Assert.Equal(DateTime.Parse(to), span?.To);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("?")]
[InlineData("xx.xx.xxxx")]
[InlineData("31.02.2015")]
[InlineData("01.13.2015")]
[InlineData("VRZ-99_Kolowag_Septemwri")]
[InlineData("201")]
public void Anything_else_is_no_date(string? text) =>
Assert.Null(RevisionSpan.Parse(text));
}

View File

@@ -0,0 +1,197 @@
using StarterNG.Classes;
using StarterNG.Domain;
using StarterNG.Domain.Vehicles;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests;
public class TexRandomizerTests
{
private readonly Consist _consist;
private readonly VehicleCatalog _catalog;
public TexRandomizerTests()
{
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(TestInstallation.At("dynamic", "pkp", "ep07", "textures.txt"),
"!=e\nep07-001.mat=ep07,EP07\nep07-002.mat=ep07,EP07")
.WithFile(TestInstallation.At("dynamic", "pkp", "en57", "textures.txt"),
"!=z\n^2\nen57-a.mat=en57a,EN57\nen57-b.mat=en57b,EN57\nen57-c.mat=en57a,EN57\nen57-d.mat=en57b,EN57")
.WithFile(TestInstallation.At("dynamic", "pkp", "b111", "textures.txt"),
"!=b\nb111-01.mat=b111,B111\nb111-02.mat=b111,B111"));
installation.Vehicles.Load(installation.Library.Vehicles);
_catalog = installation.Library.Vehicles;
_consist = new Consist(_catalog, new VehicleInfo(_catalog, installation.Physics));
}
private Dynamic Car(string skin) => _consist.MakeDynamic(_catalog.TextureForSkin(skin)!, null);
private ConsistItem Put(params Dynamic[] cars)
{
var item = new ConsistItem
{
Cars = cars.ToList(),
Grouped = cars.Length > 1,
Driver = Consist.UnitDriver(cars)
};
_consist.Insert(_consist.Count, item);
return item;
}
/// <summary>The randomizer with the next pick in line always the other livery of two.</summary>
private int Randomize() => new TexRandomizer(_catalog, new SecondPick()).Apply(_consist, t => _consist.MakeDynamic(t, null));
private sealed class SecondPick : Random
{
public override int Next(int maxValue) => 1;
}
[Theory]
[InlineData(eDriverType.Headdriver)]
[InlineData(eDriverType.Reardriver)]
[InlineData(eDriverType.Passenger)]
public void The_crew_stays_on_the_vehicle_that_takes_over(eDriverType crew)
{
var loco = Car("ep07-001");
loco.DriverType = crew;
Put(loco);
Assert.Equal(1, Randomize());
var car = Assert.Single(_consist[0].Cars);
Assert.Equal("ep07-002", car.SkinFile);
Assert.Equal(crew, car.DriverType);
Assert.Equal(crew, _consist[0].Driver);
}
[Fact]
public void A_driver_kept_is_what_the_export_writes()
{
var loco = Car("ep07-001");
loco.DriverType = eDriverType.Headdriver;
Put(loco);
Randomize();
Assert.Contains(" headdriver ", _consist[0].Cars[0].ToTrainsetNode());
}
[Fact]
public void An_empty_locomotive_stays_empty()
{
// Consist.Replace would staff it; a driver of its own is not this one's to give.
Put(Car("ep07-001"));
Randomize();
Assert.Equal(eDriverType.Nobody, _consist[0].Cars[0].DriverType);
Assert.Equal(eDriverType.Nobody, _consist[0].Driver);
}
[Fact]
public void A_vehicle_keeps_facing_the_way_it_did()
{
var loco = Car("ep07-001");
loco.Offset = -1f;
var item = Put(loco);
item.Flipped = true;
Randomize();
Assert.True(_consist[0].Flipped);
Assert.True(Consist.IsTurned(_consist[0].Cars[0]));
}
[Fact]
public void A_vehicle_keeps_its_load()
{
var wagon = Car("b111-01");
wagon.LoadType = "Passengers";
wagon.LoadCount = 40;
wagon.MaxLoad = 60;
Put(wagon);
Assert.Equal(1, Randomize());
var car = _consist[0].Cars[0];
Assert.Equal("b111-02", car.SkinFile);
Assert.Equal("Passengers", car.LoadType);
Assert.Equal(40, car.LoadCount);
Assert.Equal(60, car.MaxLoad);
}
[Fact]
public void A_unit_of_the_same_length_keeps_all_of_it_car_by_car()
{
var first = Car("en57-a");
var second = Car("en57-b");
second.DriverType = eDriverType.Reardriver;
first.LoadType = "Passengers";
first.LoadCount = 10;
second.LoadType = "Passengers";
second.LoadCount = 20;
first.Offset = -1f;
second.Offset = -1f;
var item = Put(first, second);
item.Flipped = true;
Assert.Equal(1, Randomize());
var unit = _consist[0];
Assert.Equal(new[] { "en57-c", "en57-d" }, unit.Cars.Select(c => c.SkinFile));
Assert.True(unit.Grouped);
Assert.True(unit.Flipped);
Assert.Equal(new[] { eDriverType.Nobody, eDriverType.Reardriver }, unit.Cars.Select(c => c.DriverType));
Assert.Equal(eDriverType.Reardriver, unit.Driver);
Assert.Equal(new[] { 10, 20 }, unit.Cars.Select(c => c.LoadCount));
Assert.All(unit.Cars, c => Assert.True(Consist.IsTurned(c)));
}
[Theory]
[InlineData(eDriverType.Headdriver, 0)]
[InlineData(eDriverType.Reardriver, 1)]
public void A_crew_moving_into_a_longer_unit_sits_in_the_car_with_its_cab(eDriverType crew, int car)
{
var loco = Car("en57-c");
loco.DriverType = crew;
Put(loco);
Assert.Equal(1, new TexRandomizer(_catalog, new FirstPick()).Apply(_consist, t => _consist.MakeDynamic(t, null)));
var unit = _consist[0];
Assert.Equal(2, unit.Cars.Count);
Assert.Equal(crew, unit.Cars[car].DriverType);
Assert.Equal(eDriverType.Nobody, unit.Cars[1 - car].DriverType);
Assert.Equal(crew, unit.Driver);
}
private sealed class FirstPick : Random
{
public override int Next(int maxValue) => 0;
}
[Fact]
public void A_selected_vehicle_stays_selected_as_the_one_that_took_over()
{
var item = Put(Car("ep07-001"));
_consist.Selected = item;
Randomize();
Assert.Same(_consist[0], _consist.Selected);
}
[Fact]
public void A_vehicle_not_selected_does_not_take_the_selection()
{
// ep07-001's turn picks ep07-002; ep07-002's turn picks itself and passes.
Put(Car("ep07-001"));
var selected = Put(Car("ep07-002"));
_consist.Selected = selected;
Assert.Equal(1, Randomize());
Assert.Same(selected, _consist.Selected);
}
}

View File

@@ -0,0 +1,77 @@
using StarterNG.Classes;
using StarterNG.Domain;
using StarterNG.Views;
namespace StarterNG.Tests;
public class TrainsetDisplayTests
{
private static Trainset Consist(string driver = "headdriver", string? description = null) =>
new(string.Concat(
description is null ? "" : $"//$o {description}\n",
$"trainset a tor 0 0\nnode -1 0 v1 dynamic d s m 0 {driver} 3 0 enddynamic\nendtrainset"));
[Theory]
[InlineData("-ai", true)]
[InlineData(" - ai", true)]
[InlineData("a - b", false)]
[InlineData("rozkladowy", false)]
public void A_dash_opening_the_description_says_the_consist_is_ai(string description, bool ai) =>
Assert.Equal(ai, TrainsetDisplay.IsAi(Consist(description: description)));
[Fact]
public void No_description_is_no_ai() =>
Assert.False(TrainsetDisplay.IsAi(Consist()));
[Fact]
public void An_ai_consist_is_listed_only_when_they_are_shown()
{
var ai = Consist(description: "-ai");
Assert.False(TrainsetDisplay.ListedIn(ai, showAi: false, drivableOnly: false, current: null));
Assert.True(TrainsetDisplay.ListedIn(ai, showAi: true, drivableOnly: false, current: null));
}
[Fact]
public void Drivable_only_leaves_out_a_consist_nobody_sits_in()
{
var empty = Consist(driver: "nobody");
Assert.False(TrainsetDisplay.ListedIn(empty, showAi: true, drivableOnly: true, current: null));
Assert.True(TrainsetDisplay.ListedIn(empty, showAi: true, drivableOnly: false, current: null));
Assert.True(TrainsetDisplay.ListedIn(Consist(), showAi: true, drivableOnly: true, current: null));
}
[Fact]
public void Drivable_only_keeps_the_one_picked_whatever_its_crew()
{
var picked = Consist(driver: "nobody");
Assert.True(TrainsetDisplay.ListedIn(picked, showAi: true, drivableOnly: true, current: picked));
}
[Fact]
public void Being_picked_does_not_show_an_ai_consist_the_filter_hides()
{
var picked = Consist(description: "-ai");
Assert.False(TrainsetDisplay.ListedIn(picked, showAi: false, drivableOnly: true, current: picked));
}
[Theory]
[InlineData(eDriverType.Headdriver, 0)]
[InlineData(eDriverType.Reardriver, 1)]
[InlineData(eDriverType.Passenger, 2)]
[InlineData(eDriverType.Nobody, 3)]
public void A_crew_keeps_its_place_in_the_list(eDriverType crew, int place)
{
Assert.Equal(place, CrewChoices.IndexOf(crew));
Assert.Equal(crew, CrewChoices.At(place));
}
[Theory]
[InlineData(-1)]
[InlineData(4)]
public void A_list_with_nothing_selected_means_nobody(int place) =>
Assert.Equal(eDriverType.Nobody, CrewChoices.At(place));
}

View File

@@ -0,0 +1,162 @@
using StarterNG.Domain.Vehicles;
using StarterNG.Tests.Fakes;
using StarterNG.Views;
namespace StarterNG.Tests;
public class VehicleBaseQueryTests
{
private static VehicleBaseRow Row(string skin, string? operatorName = null, string? revision = null,
string? author = null, string? depot = null, string? photo = null,
string directory = "dynamic/pkp/", string? model = "model")
{
return new VehicleBaseRow(new VehicleTexture
{
Directory = directory,
Skinfile = skin,
Model = model,
Meta = new VehicleMeta
{
Operator = operatorName,
RevisionDate = revision,
TextureAuthor = author,
Depot = depot,
PhotoAuthor = photo
}
});
}
private static IReadOnlyList<VehicleTexture> Catalogued(params string[] textures)
{
var files = new InMemoryFileSystem();
foreach (string text in textures)
files.WithFile(TestInstallation.At("dynamic", "pkp", text[..text.IndexOf('=')], "textures.txt"),
text[(text.IndexOf('=') + 1)..]);
var installation = new TestInstallation(files);
installation.Vehicles.Load(installation.Library.Vehicles);
return installation.Library.Vehicles.Textures;
}
private static string[] Skins(IEnumerable<VehicleBaseRow> rows) => rows.Select(r => r.Skin).ToArray();
private static string[] Selected(VehicleBaseFilters filters, params VehicleBaseRow[] rows) =>
Skins(VehicleBaseQuery.Select(rows, filters, r => r.Skin, descending: false));
[Fact]
public void A_question_mark_in_the_credit_line_is_what_its_author_did_not_know()
{
var row = Row("a", operatorName: "?", revision: "?", author: "Jan");
Assert.Equal("", row.Operator);
Assert.Equal("", row.Revision);
Assert.Null(row.Date);
Assert.Equal("", row.DateKey);
Assert.Equal("Jan", row.Author);
}
[Fact]
public void The_date_key_sorts_as_the_dates_do() =>
Assert.Equal("20081201", Row("a", revision: "xx.12.2008").DateKey);
[Fact]
public void Each_filter_looks_in_its_own_field_ignoring_case_and_spaces_around()
{
var a = Row("alfa", operatorName: "PKP", author: "Jan", depot: "Lublin", photo: "Ewa", directory: "dynamic/one/");
var b = Row("beta", operatorName: "CD", author: "Olaf", depot: "Praha", photo: "Karel", directory: "dynamic/two/");
Assert.Equal(new[] { "alfa" }, Selected(new VehicleBaseFilters { Name = " ALF " }, a, b));
Assert.Equal(new[] { "beta" }, Selected(new VehicleBaseFilters { Operator = "cd" }, a, b));
Assert.Equal(new[] { "alfa" }, Selected(new VehicleBaseFilters { Author = "jan" }, a, b));
Assert.Equal(new[] { "beta" }, Selected(new VehicleBaseFilters { Station = "prah" }, a, b));
Assert.Equal(new[] { "alfa" }, Selected(new VehicleBaseFilters { Photo = "EW" }, a, b));
Assert.Equal(new[] { "beta" }, Selected(new VehicleBaseFilters { Model = "TWO/" }, a, b));
Assert.Empty(Selected(new VehicleBaseFilters { Name = "alf", Operator = "cd" }, a, b));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void A_filter_with_nothing_typed_lets_everything_through(string? typed) =>
Assert.Equal(new[] { "a", "b" }, Selected(new VehicleBaseFilters { Name = typed }, Row("a"), Row("b")));
[Theory]
[InlineData("2016", null, true)]
[InlineData("2016-12", null, true)]
[InlineData("2017", null, false)]
[InlineData(null, "2016", true)]
[InlineData(null, "2015", false)]
[InlineData("2010", "2020", true)]
public void A_revision_counts_while_its_span_overlaps_what_is_asked(string? from, string? to, bool shown)
{
var row = Row("a", revision: "xx.xx.2016");
var found = Selected(new VehicleBaseFilters { RevisionFrom = from, RevisionTo = to }, row);
Assert.Equal(shown, found.Length == 1);
}
[Fact]
public void A_month_only_date_overlaps_at_both_of_its_ends()
{
var row = Row("a", revision: "xx.06.2008");
Assert.Single(Selected(new VehicleBaseFilters { RevisionFrom = "30.06.2008" }, row));
Assert.Single(Selected(new VehicleBaseFilters { RevisionTo = "01.06.2008" }, row));
Assert.Empty(Selected(new VehicleBaseFilters { RevisionFrom = "01.07.2008" }, row));
Assert.Empty(Selected(new VehicleBaseFilters { RevisionTo = "31.05.2008" }, row));
}
[Fact]
public void A_revision_asked_for_leaves_out_what_has_no_date_and_what_cannot_be_read_asks_nothing()
{
var dated = Row("dated", revision: "2016");
var undated = Row("undated");
Assert.Equal(new[] { "dated" }, Selected(new VehicleBaseFilters { RevisionFrom = "2000" }, dated, undated));
Assert.Equal(new[] { "dated", "undated" },
Selected(new VehicleBaseFilters { RevisionFrom = "soon" }, dated, undated));
}
[Theory]
[InlineData(false, new[] { "b", "c", "a", "d" })]
[InlineData(true, new[] { "c", "b", "a", "d" })]
public void Blanks_stay_at_the_bottom_whichever_way_a_column_is_sorted_and_the_name_settles_a_tie(
bool descending, string[] order)
{
var rows = new[] { Row("d"), Row("c", operatorName: "Z"), Row("a"), Row("b", operatorName: "M") };
Assert.Equal(order, Skins(VehicleBaseQuery.Select(rows, new VehicleBaseFilters(), r => r.Operator, descending)));
}
[Fact]
public void The_class_list_counts_a_class_under_one_name_whatever_its_case_in_order_of_the_names()
{
var textures = Catalogued("b111=!=b\nb111-01.mat=b111,B111\nb111-02.mat=b222,b111",
"ep07=!=e\nep07-01.mat=ep07,AP07");
var counts = VehicleBaseQuery.ClassCounts(textures.Select(t => new VehicleBaseRow(t)));
Assert.Equal(new[] { ("AP07", 1), ("B111", 2) },
counts.Select(c => (c.Name.ToUpperInvariant(), c.Count)));
}
[Fact]
public void A_row_with_no_class_is_not_counted() =>
Assert.Empty(VehicleBaseQuery.ClassCounts(new[] { Row("a") }));
[Fact]
public void The_class_picked_has_to_match_whole_while_the_typed_one_only_has_to_appear()
{
var textures = Catalogued("b111=!=b\nb111-01.mat=b111,B111");
var row = new VehicleBaseRow(textures[0]);
Assert.NotEqual("", row.Class);
Assert.Single(VehicleBaseQuery.Select(new[] { row }, new VehicleBaseFilters { PickedClass = row.Class.ToUpperInvariant() },
r => r.Skin, descending: false));
Assert.Empty(VehicleBaseQuery.Select(new[] { row }, new VehicleBaseFilters { PickedClass = row.Class[..1] + "?" },
r => r.Skin, descending: false));
Assert.Single(VehicleBaseQuery.Select(new[] { row }, new VehicleBaseFilters { Class = row.Class[..1] },
r => r.Skin, descending: false));
}
}

View File

@@ -0,0 +1,139 @@
using StarterNG.Application;
using StarterNG.Classes;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests;
// PresetStore keeps the disk of the first test that reaches it, a fake one as every
// composition root in the tests is built on fakes. So each test has a name to itself.
public class WarehouseEditTests
{
private const string Text =
"trainset a tor 0 0\nnode -1 0 v1 dynamic d s m 0 headdriver 3 0 enddynamic\nendtrainset\n";
private static readonly string Entry = ConsistText.Serialize(new Trainset(Text));
private readonly WarehouseEdit _edit = new();
public WarehouseEditTests()
{
var installation = new TestInstallation(new InMemoryFileSystem());
AppServices.Initialize(installation.Paths, installation.Files, installation.Clock, installation.Environment);
}
private static TrainsetPreset Preset(string name, string? entry = null) => new() { Name = name, Entry = entry ?? Entry };
private static TrainsetPreset? Stored(string name) => PresetStore.All().FirstOrDefault(p => p.Name == name);
[Fact]
public void Opening_says_what_is_edited_and_closing_that_nothing_is()
{
var trainset = _edit.Open(Preset("edit-open"));
Assert.Same(trainset, _edit.Trainset);
Assert.Equal("edit-open", _edit.Name);
_edit.Close();
Assert.Null(_edit.Trainset);
Assert.Null(_edit.Name);
}
[Fact]
public void A_consist_opened_again_is_the_same_trainset_so_its_undo_history_outlives_a_switch()
{
var first = _edit.Open(Preset("edit-switch"));
_edit.Open(Preset("edit-switch-other"));
Assert.Same(first, _edit.Open(Preset("edit-switch")));
}
[Fact]
public void An_entry_written_over_from_elsewhere_is_read_afresh()
{
var first = _edit.Open(Preset("edit-over"));
var second = _edit.Open(Preset("edit-over", ConsistText.Serialize(new Trainset(Text.Replace(" v1 ", " v9 ")))));
Assert.NotSame(first, second);
Assert.Equal("v9", second.Vehicles.Single().Name);
}
[Fact]
public void The_name_of_a_consist_goes_with_any_letter_case()
{
var first = _edit.Open(Preset("edit-case"));
Assert.Same(first, _edit.Open(Preset("EDIT-CASE")));
}
[Fact]
public void Nothing_is_written_while_the_consist_stands_as_it_was_read()
{
PresetStore.Save("edit-unchanged", Entry);
_edit.Open(Stored("edit-unchanged")!);
PresetStore.Delete("edit-unchanged");
_edit.Save();
Assert.Null(Stored("edit-unchanged"));
}
[Fact]
public void A_change_is_written_once()
{
PresetStore.Save("edit-changed", Entry);
var trainset = _edit.Open(Stored("edit-changed")!);
trainset.Vehicles.Add(trainset.Vehicles[0].Clone());
_edit.Save();
Assert.Equal(ConsistText.Serialize(trainset), Stored("edit-changed")!.Entry);
PresetStore.Delete("edit-changed");
_edit.Save();
Assert.Null(Stored("edit-changed"));
}
[Fact]
public void Nothing_is_written_with_nothing_open()
{
var trainset = _edit.Open(Preset("edit-closed"));
trainset.Vehicles.Add(trainset.Vehicles[0].Clone());
_edit.Close();
_edit.Save();
Assert.Null(Stored("edit-closed"));
}
[Fact]
public void A_rename_keeps_the_trainset_and_saves_under_the_new_name()
{
var trainset = _edit.Open(Preset("edit-old"));
_edit.Renamed("edit-old", "edit-new");
Assert.Equal("edit-new", _edit.Name);
trainset.Vehicles.Add(trainset.Vehicles[0].Clone());
_edit.Save();
Assert.Null(Stored("edit-old"));
Assert.Equal(ConsistText.Serialize(trainset), Stored("edit-new")!.Entry);
Assert.Same(trainset, _edit.Open(Preset("edit-new", Stored("edit-new")!.Entry)));
}
[Fact]
public void Renaming_another_consist_leaves_the_one_open_alone()
{
var other = _edit.Open(Preset("edit-other"));
var open = _edit.Open(Preset("edit-mine"));
_edit.Renamed("edit-other", "edit-other-new");
Assert.Same(open, _edit.Trainset);
Assert.Equal("edit-mine", _edit.Name);
Assert.Same(other, _edit.Open(Preset("edit-other-new")));
}
}