Add Hasler tape viewer for the simulator's physics logs

Reads physicslog/*.dat and draws each trip as a Hasler tape or, for vehicles
with an electronic recorder, as an evaluation screen. Tapes are grouped by
launch, and a launch still running can be followed live. Includes whole-tape
PNG export and a summary of times and averages. With the log off, the page
offers to turn it on.
This commit is contained in:
maj00r
2026-11-01 16:06:16 +01:00
parent 82429010d4
commit 6e45f5de59
12 changed files with 2707 additions and 0 deletions

View File

@@ -37,6 +37,7 @@ public sealed class TestInstallation
Executables = new ExecutableLocator(Files, Paths, Environment, Log);
SettingsStore = new SettingsStore(Files, Clock, Log, SettingsPaths, new SettingsSerializer(), Executables);
Tapes = new HaslerTapes(Files, Paths, Log, Clock, new TimetableLocator(Files), () => SimulatorRunning);
}
public InMemoryFileSystem Files { get; }
@@ -57,6 +58,8 @@ public sealed class TestInstallation
public SettingsPaths SettingsPaths { get; }
public ExecutableLocator Executables { get; }
public SettingsStore SettingsStore { get; }
public HaslerTapes Tapes { get; }
public bool SimulatorRunning { get; set; }
/// <summary>Path of a file inside the fake installation.</summary>
public static string At(params string[] segments) => Path.Combine(new[] { Root }.Concat(segments).ToArray());

View File

@@ -0,0 +1,168 @@
using System.Globalization;
using StarterNG.Domain.Tapes;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests;
public class HaslerTapeTests
{
private const string Header =
"Time[s] Velocity[m/s] Acceleration[m/ss] Coupler.Dist[m] Coupler.Force[N] TractionForce[kN] FrictionForce[kN] " +
"BrakeForce[kN] BrakePress[MPa] PipePress[MPa] MotorCurrent[A] MCP SCP BCP LBP Direction Command CVal1 CVal2 " +
"Security Wheelslip EngineTemp[Deg] OilTemp[Deg] WaterTemp[Deg] WaterAuxTemp[Deg]\r\n";
private static string Line(double t, double v, double brake, double current, string command = "none",
int security = 0) =>
string.Create(CultureInfo.InvariantCulture,
$"{t:0.0000} {v:0.0000} 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 {brake:0.0000} 0.5000 {current:0.0000} ") +
$"3 0 0.0000 0.0000 1 {command} 0.0000 0.0000 {security} 0 0.0000 0.0000 0.0000 0.0000\r\n";
[Fact]
public void A_physics_log_reads_as_speed_distance_and_the_two_hasler_marks()
{
var tape = HaslerTape.Parse(Header
+ Line(10, 0, 0.45, 0)
+ Line(20, 36, 0, 400)
+ Line(30, 72, 0, 0, security: 1));
Assert.Equal(3, tape.Samples.Count);
Assert.Equal(72, tape.MaxSpeedKmh, 6);
Assert.Equal(20, tape.Duration, 6);
Assert.Equal(50 + 150, tape.DistanceM, 6); // trapezoids: 0->36 km/h and 36->72 km/h over 10 s each
Assert.True(tape.Samples[0].Braking);
Assert.True(tape.Samples[1].Current);
Assert.True(tape.Samples[2].SecurityAlarm);
}
[Fact]
public void Standing_and_running_time_split_the_recording_like_the_old_analyser()
{
var tape = HaslerTape.Parse(Header + Line(0, 0, 0.45, 0) + Line(60, 0, 0.45, 0) + Line(120, 36, 0, 400)
+ Line(180, 36, 0, 0));
Assert.Equal(60, tape.StandingTime, 6);
Assert.Equal(120, tape.RunningTime, 6);
Assert.Equal(900 / 180.0 * 3.6, tape.AverageWithStopsKmh, 6); // 300 m ramping up to 36 km/h, 600 m at it
Assert.Equal(900 / 120.0 * 3.6, tape.AverageSpeedKmh, 6);
}
[Fact]
public void A_line_from_the_simulator_reads_in_kmh_and_bar_whatever_its_header_says()
{
var tape = HaslerTape.Parse(Header +
"107.5512 38.6099 -0.0201 -0.0002 917.2355 -0.0000 436.4420 0.0000 0.0021 4.9981 0.0000 0 0 0.0000 " +
"0.0000 1 none 0.0000 0.0000 0 0 50.0000 45.0000 50.0000 40.0000\r\n");
var s = Assert.Single(tape.Samples);
Assert.Equal(38.6099, s.SpeedKmh, 6);
Assert.Equal(4.9981, s.PipePressBar, 6);
Assert.False(s.Braking);
}
[Fact]
public void A_line_still_being_written_or_a_command_with_spaces_does_not_break_the_tape()
{
var tape = HaslerTape.Parse(Header + Line(0, 18, 0, 0, command: "Set Velocity") + Line(1, 18, 0, 0) + "2.0500 5.0");
Assert.Equal(2, tape.Samples.Count);
Assert.Equal(5, tape.DistanceM, 6);
}
[Fact]
public void The_sample_under_a_point_of_the_tape_is_found_by_time_and_by_distance()
{
var tape = HaslerTape.Parse(Header + Line(0, 36, 0, 0) + Line(1, 36, 0, 0) + Line(2, 36, 0, 0));
Assert.Equal(1, tape.IndexAt(1.5, byDistance: false));
Assert.Equal(2, tape.IndexAt(25, byDistance: true));
Assert.Equal(0, tape.IndexAt(-3, byDistance: false));
}
[Theory]
[InlineData(0, 90)]
[InlineData(90, 90)]
[InlineData(90.5, 100)]
[InlineData(120, 120)]
[InlineData(151, 180)]
[InlineData(180, 180)]
[InlineData(181, 200)]
[InlineData(205, 220)]
public void The_scale_tops_out_at_the_speedometers_range_then_at_the_next_twenty(double max, double top) =>
Assert.Equal(top, TapeFigures.TopSpeed(max));
[Theory]
[InlineData(1, 10)]
[InlineData(0.8, 10)]
[InlineData(0.79, 20)]
[InlineData(0.4, 20)]
[InlineData(0.16, 50)]
[InlineData(0.08, 100)]
[InlineData(0.04, 200)]
[InlineData(0.039, 500)]
public void Speed_lines_take_the_first_step_at_least_8_pixels_apart(double pxPerKmh, int step) =>
Assert.Equal(step, TapeFigures.SpeedStep(pxPerKmh));
[Theory]
[InlineData(60, 1)]
[InlineData(59, 2)]
[InlineData(30, 2)]
[InlineData(12, 5)]
[InlineData(6, 10)]
[InlineData(4, 15)]
[InlineData(2, 30)]
[InlineData(1, 60)]
[InlineData(0.99, 120)]
public void Clock_lines_take_the_first_step_at_least_60_pixels_apart(double pxPerMinute, double step) =>
Assert.Equal(step, TapeFigures.MinuteStep(pxPerMinute));
[Theory]
[InlineData(600, 0.1)]
[InlineData(500, 0.1)]
[InlineData(499, 0.2)]
[InlineData(100, 0.5)]
[InlineData(50, 1)]
[InlineData(25, 2)]
[InlineData(10, 5)]
[InlineData(5, 10)]
[InlineData(2.5, 20)]
[InlineData(2, 50)]
public void Distance_marks_take_the_first_step_at_least_50_pixels_apart(double pxPerKm, double step) =>
Assert.Equal(step, TapeFigures.KmStep(pxPerKm));
[Theory]
[InlineData(-5, "0:00")]
[InlineData(0, "0:00")]
[InlineData(65, "1:05")]
[InlineData(3599, "59:59")]
[InlineData(3600, "1:00:00")]
[InlineData(3723, "1:02:03")]
[InlineData(90000, "25:00:00")]
public void A_length_of_time_prints_hours_only_when_it_has_them(double seconds, string text) =>
Assert.Equal(text, TapeFigures.Clock(seconds));
[Theory]
[InlineData("7:52", 7, 52, 0)]
[InlineData("07:52", 7, 52, 0)]
[InlineData(" 23:05 ", 23, 5, 0)]
[InlineData("7:52:30", 7, 52, 30)]
[InlineData("24:00", 0, 0, 0)]
[InlineData("noon", 0, 0, 0)]
[InlineData("", 0, 0, 0)]
[InlineData(null, 0, 0, 0)]
public void A_clock_typed_in_is_a_time_of_the_day_or_nothing(string? text, int hours, int minutes, int seconds) =>
Assert.Equal(new TimeSpan(hours, minutes, seconds), TapeFigures.ParseClock(text));
[Fact]
public void Tapes_are_listed_from_physicslog_newest_first()
{
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(TestInstallation.At("physicslog", "EU07-424.dat"), Header + Line(0, 1, 0, 0)));
var tapes = installation.Tapes;
var list = tapes.List();
Assert.Single(list);
Assert.Equal("EU07-424", list[0].Vehicle);
Assert.Single(tapes.Load(list[0])!.Samples);
}
}

View File

@@ -0,0 +1,485 @@
using StarterNG.Application;
using StarterNG.Domain.Tapes;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests;
public class TapeLaunchTests
{
private static readonly DateTime Launched = new(2025, 12, 31, 23, 0, 0, DateTimeKind.Utc);
[Fact]
public void A_launch_is_written_and_read_back_with_names_that_hold_spaces()
{
var launch = new TapeLaunch(Launched, "td.scn", new TimeSpan(7, 52, 0), new[]
{
new TapeVehicle("EP07-424-HIST", "pkp/ep07_v1", "ep07-424"),
new TapeVehicle("wagon 1", "pkp/111a_v2", "111a")
});
var back = TapeLaunch.Parse(launch.Format())!;
Assert.Equal(Launched, back.LaunchedUtc);
Assert.Equal("td.scn", back.Scenery);
Assert.Equal(new TimeSpan(7, 52, 0), back.StartClock);
Assert.Equal("pkp/ep07_v1", back.Find("ep07-424-hist")!.DataFolder);
Assert.Equal("111a", back.Find("wagon 1")!.TypeName);
}
[Fact]
public void The_driven_vehicle_and_the_consist_are_written_and_read_back()
{
var launch = new TapeLaunch(Launched, "td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>(),
"EP07 424", new[] { "EP07 424", "wagon 1" });
var back = TapeLaunch.Parse(launch.Format())!;
Assert.Equal(TapeRole.Driven, back.RoleOf("ep07 424"));
Assert.Equal(TapeRole.OwnConsist, back.RoleOf("WAGON 1"));
Assert.Equal(TapeRole.Other, back.RoleOf("et22-100"));
}
[Fact]
public void A_note_without_the_driven_vehicle_tells_nothing_of_it()
{
var back = TapeLaunch.Parse("launched\t2025-12-31T23:00:00.0000000Z\nscenery\ttd.scn\nclock\t10:30\n")!;
Assert.Null(back.Driven);
Assert.Equal(TapeRole.Other, back.RoleOf("ep07-424"));
Assert.Null(back.SceneryName);
Assert.Null(back.Train);
}
[Fact]
public void The_scenario_and_the_train_are_written_and_read_back()
{
var launch = new TapeLaunch(Launched, "td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>(),
SceneryName: "Tor doświadczalny", Train: "EIE 1531");
var back = TapeLaunch.Parse(launch.Format())!;
Assert.Equal("Tor doświadczalny", back.SceneryName);
Assert.Equal("EIE 1531", back.Train);
}
[Fact]
public void A_tape_of_the_last_launch_gets_its_scenario_and_train()
{
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(TestInstallation.At("physicslog", "EP07-424.dat"), "t"));
installation.Clock.Now = Launched;
var tapes = installation.Tapes;
tapes.RecordLaunch("td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>(), "EP07-424", new[] { "EP07-424" },
"Tor doświadczalny", "EIE1531");
var context = tapes.ContextOf(tapes.List().Single());
Assert.Equal("Tor doświadczalny", context.Scenery);
Assert.Equal("EIE1531", context.Train);
}
[Fact]
public void A_tape_of_someone_elses_vehicle_does_not_get_the_train_of_the_player()
{
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(TestInstallation.At("physicslog", "ET22-100.dat"), "t"));
installation.Clock.Now = Launched;
var tapes = installation.Tapes;
tapes.RecordLaunch("td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>(), "EP07-424", new[] { "EP07-424" },
"Tor doświadczalny", "EIE1531");
var context = tapes.ContextOf(tapes.List().Single());
Assert.Equal("Tor doświadczalny", context.Scenery);
Assert.Null(context.Train);
}
[Fact]
public void A_scenario_without_a_name_goes_by_its_file()
{
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(TestInstallation.At("physicslog", "EP07-424.dat"), "t"));
installation.Clock.Now = Launched;
var tapes = installation.Tapes;
tapes.RecordLaunch("td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>());
Assert.Equal("td.scn", tapes.ContextOf(tapes.List().Single()).Scenery);
}
[Theory]
[InlineData("[ Rodzaj i numer pociagu | EIE1531 \t ] \r\n[ Kategoria | IC ]\r\n", "EIE1531")]
[InlineData(" ____\r\n[ Rodzaj i numer pociągu | LPE 782 ]\r\n", "LPE 782")]
[InlineData("[ Kategoria | IC ]\r\n", null)]
[InlineData("", null)]
public void The_train_number_comes_from_the_heading_of_its_timetable(string timetable, string? number)
{
var files = new InMemoryFileSystem().WithFile(TestInstallation.At("scenery", "baltyk", "eie1531.txt"), timetable);
Assert.Equal(number, new StarterNG.Infrastructure.Sceneries.TimetableLocator(files)
.TrainNumber(new StarterNG.Domain.Sceneries.Scenery(TestInstallation.At("scenery", "td.scn"), ""),
"baltyk/eie1531"));
}
[Theory]
[InlineData("none")]
[InlineData("NONE")]
[InlineData("")]
[InlineData(null)]
public void A_trainset_without_a_timetable_has_no_train_number(string? name) =>
Assert.Null(new StarterNG.Infrastructure.Sceneries.TimetableLocator(new InMemoryFileSystem())
.TrainNumber(new StarterNG.Domain.Sceneries.Scenery(TestInstallation.At("scenery", "td.scn"), ""), name));
[Fact]
public void A_timetable_that_is_not_there_gives_no_train_number() =>
Assert.Null(new StarterNG.Infrastructure.Sceneries.TimetableLocator(new InMemoryFileSystem())
.TrainNumber(new StarterNG.Domain.Sceneries.Scenery(TestInstallation.At("scenery", "td.scn"), ""),
@"baltyk\mod\eie1531"));
[Fact]
public void The_driven_tape_and_the_consist_are_listed_ahead_of_the_rest()
{
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(TestInstallation.At("physicslog", "a-other.dat"), "t")
.WithFile(TestInstallation.At("physicslog", "b-wagon.dat"), "t")
.WithFile(TestInstallation.At("physicslog", "c-lok.dat"), "t"));
installation.Clock.Now = Launched;
var tapes = installation.Tapes;
tapes.RecordLaunch("td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>(), "c-lok", new[] { "c-lok", "b-wagon" });
Assert.Equal("c-lok", tapes.DrivenVehicle);
Assert.Equal(new[] { "c-lok", "b-wagon", "a-other" }, tapes.List().Select(tape => tape.Vehicle));
Assert.Equal(new[] { TapeRole.StartedIn, TapeRole.OwnConsist, TapeRole.Other }, tapes.List().Select(tape => tape.Role));
}
[Fact]
public void With_no_log_the_vehicle_of_the_launch_is_only_the_one_it_started_in()
{
var launch = new TapeLaunch(Launched, "td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>(), "ra", new[] { "ra", "rb" });
Assert.Equal(TapeRole.StartedIn, launch.RoleOf("ra", PlayerVehicles.Unknown));
Assert.Equal(TapeRole.OwnConsist, launch.RoleOf("rb", PlayerVehicles.Unknown));
}
[Fact]
public void Where_the_player_has_been_is_written_and_read_back()
{
var launch = new TapeLaunch(Launched, "td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>(),
Player: PlayerVehicles.None.After("a received command: [x]\nb received command: [x]\n"));
var back = TapeLaunch.Parse(launch.Format())!;
Assert.Equal("b", back.Player!.Current);
Assert.True(back.Player.Used("a"));
}
[Fact]
public void A_note_of_a_single_launch_still_reads()
{
var all = TapeLaunch.ParseAll("launched\t2025-12-31T23:00:00.0000000Z\nscenery\ttd.scn\nclock\t10:30\n");
Assert.Equal("td.scn", all.Single().Scenery);
Assert.Null(all.Single().Player);
}
[Fact]
public void Tapes_part_between_the_launches_they_were_written_after()
{
var files = new InMemoryFileSystem()
.WithFile(TestInstallation.At("physicslog", "ra.dat"), "t")
.WithFile(TestInstallation.At("physicslog", "rb.dat"), "t");
var installation = new TestInstallation(files);
var tapes = installation.Tapes;
installation.Clock.Now = Launched;
tapes.RecordLaunch("a.scn", new TimeSpan(7, 0, 0), Array.Empty<TapeVehicle>(), "ra", new[] { "ra" }, "A", "T1");
installation.Clock.Now = Launched.AddHours(1);
tapes.RecordLaunch("b.scn", new TimeSpan(9, 0, 0), Array.Empty<TapeVehicle>(), "rb", new[] { "rb" }, "B", "T2");
files.WrittenAt(TestInstallation.At("physicslog", "ra.dat"), Launched.AddMinutes(30))
.WrittenAt(TestInstallation.At("physicslog", "rb.dat"), Launched.AddMinutes(90));
var sessions = tapes.Sessions();
Assert.Equal(new[] { "B", "A" }, sessions.Select(session => session.Launch!.SceneryName));
Assert.Equal("rb", sessions[0].Tapes.Single().Vehicle);
Assert.Equal("ra", sessions[1].Tapes.Single().Vehicle);
Assert.Equal(new TimeSpan(7, 0, 0), tapes.ContextOf(sessions[1].Tapes[0]).StartClock);
Assert.Equal("T1", tapes.ContextOf(sessions[1].Tapes[0]).Train);
}
[Fact]
public void Tapes_with_no_launch_noted_go_together_by_when_they_were_written()
{
var early = new DateTime(2026, 3, 1, 12, 0, 0, DateTimeKind.Utc);
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(TestInstallation.At("physicslog", "a.dat"), "t")
.WithFile(TestInstallation.At("physicslog", "b.dat"), "t")
.WithFile(TestInstallation.At("physicslog", "c.dat"), "t")
.WrittenAt(TestInstallation.At("physicslog", "a.dat"), early)
.WrittenAt(TestInstallation.At("physicslog", "b.dat"), early.AddSeconds(30))
.WrittenAt(TestInstallation.At("physicslog", "c.dat"), early.AddHours(1)));
var sessions = installation.Tapes.Sessions();
Assert.Equal(new[] { 1, 2 }, sessions.Select(session => session.Tapes.Count));
Assert.All(sessions, session => Assert.Null(session.Launch));
}
[Fact]
public void A_running_session_is_listed_with_no_tapes_yet_and_goes_with_the_simulator()
{
var installation = new TestInstallation(new InMemoryFileSystem());
installation.Clock.Now = Launched;
var tapes = installation.Tapes;
tapes.RecordLaunch("td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>(), "ra");
installation.SimulatorRunning = true;
var running = tapes.Sessions().Single();
Assert.True(running.Running);
Assert.Empty(running.Tapes);
installation.SimulatorRunning = false;
Assert.Empty(tapes.Sessions());
}
[Fact]
public void A_log_of_its_own_tells_of_the_vehicles_when_the_common_one_is_stale()
{
string kept = TestInstallation.At("logs", "log (td.scn) 2026.txt");
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(TestInstallation.At("log.txt"), "ra received command: [x]\n")
.WithFile(kept, "rb received command: [x]\n")
.WrittenAt(TestInstallation.At("log.txt"), Launched.AddHours(-2))
.WrittenAt(kept, Launched.AddMinutes(10)));
installation.Clock.Now = Launched;
var tapes = installation.Tapes;
tapes.RecordLaunch("td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>(), "ra", new[] { "ra", "rb" });
Assert.Equal("rb", tapes.DrivenVehicle);
}
[Fact]
public void The_vehicles_of_a_run_stay_with_it_when_the_next_begins()
{
var (tapes, installation) = TapesOfTrain("ra received command: [x]\nrb received command: [x]\n");
var written = Launched.AddMinutes(30);
installation.Files.WrittenAt(TestInstallation.At("log.txt"), written)
.WrittenAt(TestInstallation.At("physicslog", "ra.dat"), written)
.WrittenAt(TestInstallation.At("physicslog", "rb.dat"), written)
.WrittenAt(TestInstallation.At("physicslog", "obcy.dat"), written);
installation.Clock.Now = Launched.AddHours(1);
tapes.RecordLaunch("td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>(), "inny", new[] { "inny" });
var run = tapes.Sessions().Single();
Assert.Equal("Test", run.Launch!.SceneryName);
Assert.Equal(TapeRole.Driven, run.Tapes.Single(tape => tape.Vehicle == "rb").Role);
Assert.Equal(TapeRole.DrivenEarlier, run.Tapes.Single(tape => tape.Vehicle == "ra").Role);
}
[Fact]
public void Only_the_last_launches_are_remembered()
{
var installation = new TestInstallation(new InMemoryFileSystem());
var tapes = installation.Tapes;
for (int i = 0; i < 35; i++)
{
installation.Clock.Now = Launched.AddMinutes(i);
tapes.RecordLaunch($"s{i}.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>());
}
var noted = TapeLaunch.ParseAll(installation.Files.ReadAllText(
TestInstallation.At("physicslog", "starter-launch.txt"), System.Text.Encoding.UTF8));
Assert.Equal(30, noted.Count);
Assert.Equal("s5.scn", noted[0].Scenery);
Assert.Equal("s34.scn", noted[^1].Scenery);
}
[Fact]
public void The_vehicle_the_commands_go_to_is_the_one_the_player_is_in()
{
var player = PlayerVehicles.None.After(
"[ 21.764 ]\t\tTrying to enter player train, \"lok-a\"\r\n" +
"[ 25.609 ]\t\tlok-a received command: [aidriverdisable]\r\n" +
"[ 255.539 ]\t\tKey pressed: [F5]\r\n" +
"[ 255.569 ]\t\tlok b received command: [aidriverdisable]\r\n" +
"lok-c received command: [cabchangebackward]\n");
Assert.Equal("lok-c", player.Current);
Assert.True(player.Used("LOK-A"));
Assert.True(player.Used("lok b"));
Assert.False(player.Used("lok-d"));
}
[Fact]
public void Going_back_to_a_vehicle_makes_it_the_current_one_again()
{
var player = PlayerVehicles.None
.After("a received command: [x]\nb received command: [x]\n")
.After("A received command: [x]\n");
Assert.Equal("A", player.Current);
Assert.True(player.Used("b"));
}
[Fact]
public void After_a_change_of_vehicle_the_new_one_is_driven_and_the_first_was_driven_earlier()
{
var launch = new TapeLaunch(Launched, "td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>(),
"ra", new[] { "ra", "s", "rb" });
var player = PlayerVehicles.None.After("ra received command: [x]\nrb received command: [x]\n");
Assert.Equal(TapeRole.Driven, launch.RoleOf("RB", player));
Assert.Equal(TapeRole.DrivenEarlier, launch.RoleOf("ra", player));
Assert.Equal(TapeRole.OwnConsist, launch.RoleOf("s", player));
Assert.Equal(TapeRole.Other, launch.RoleOf("obcy", player));
}
[Fact]
public void Before_the_first_command_the_vehicle_of_the_launch_is_driven()
{
var launch = new TapeLaunch(Launched, "td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>(), "ra", new[] { "ra", "rb" });
Assert.Equal(TapeRole.Driven, launch.RoleOf("ra", PlayerVehicles.None));
Assert.Equal(TapeRole.OwnConsist, launch.RoleOf("rb", PlayerVehicles.None));
}
private static (HaslerTapes Tapes, TestInstallation Installation) TapesOfTrain(string log)
{
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(TestInstallation.At("physicslog", "ra.dat"), "t")
.WithFile(TestInstallation.At("physicslog", "rb.dat"), "t")
.WithFile(TestInstallation.At("physicslog", "obcy.dat"), "t")
.WithFile(TestInstallation.At("log.txt"), log));
installation.Clock.Now = Launched;
var tapes = installation.Tapes;
tapes.RecordLaunch("td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>(), "ra", new[] { "ra", "rb" }, "Test", "MPJ1");
return (tapes, installation);
}
[Fact]
public void The_tape_of_the_vehicle_changed_to_comes_first_and_keeps_the_train()
{
var (tapes, _) = TapesOfTrain("ra received command: [x]\nrb received command: [x]\n");
Assert.Equal(new[] { "rb", "ra", "obcy" }, tapes.List().Select(tape => tape.Vehicle));
Assert.Equal(new[] { TapeRole.Driven, TapeRole.DrivenEarlier, TapeRole.Other }, tapes.List().Select(tape => tape.Role));
Assert.Equal("rb", tapes.DrivenVehicle);
Assert.Equal("MPJ1", tapes.ContextOf(tapes.List().First()).Train);
}
[Fact]
public void A_vehicle_of_another_train_changed_to_is_driven_without_the_number_of_the_first()
{
var (tapes, _) = TapesOfTrain("obcy received command: [x]\n");
var driven = tapes.List().First();
Assert.Equal("obcy", driven.Vehicle);
Assert.Null(tapes.ContextOf(driven).Train);
}
[Fact]
public void A_change_of_vehicle_during_the_run_is_seen_at_the_next_look()
{
const string start = "ra received command: [x]\n";
var (tapes, installation) = TapesOfTrain(start);
Assert.Equal("ra", tapes.DrivenVehicle);
installation.Files.WithFile(TestInstallation.At("log.txt"), start + "rb received com");
Assert.Equal("ra", tapes.DrivenVehicle);
installation.Files.WithFile(TestInstallation.At("log.txt"), start + "rb received command: [x]\n");
Assert.Equal("rb", tapes.DrivenVehicle);
}
[Fact]
public void A_log_begun_anew_forgets_the_vehicles_of_the_old_one()
{
var (tapes, installation) = TapesOfTrain("ra received command: [x]\nrb received command: [x]\n");
Assert.Equal("rb", tapes.DrivenVehicle);
installation.Files.WithFile(TestInstallation.At("log.txt"), "obcy received command: [x]\n");
Assert.Equal("obcy", tapes.DrivenVehicle);
Assert.Equal(TapeRole.OwnConsist, tapes.List().Single(tape => tape.Vehicle == "rb").Role);
}
[Fact]
public void A_log_older_than_the_launch_says_nothing_of_it()
{
var (tapes, installation) = TapesOfTrain("rb received command: [x]\n");
installation.Clock.Now = installation.Files.LastWriteTimeUtc.AddMinutes(10);
tapes.RecordLaunch("td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>(), "ra", new[] { "ra", "rb" });
Assert.Equal("ra", tapes.DrivenVehicle);
}
[Fact]
public void A_tape_older_than_the_last_launch_has_no_part_in_it()
{
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(TestInstallation.At("physicslog", "c-lok.dat"), "t"));
installation.Clock.Now = installation.Files.LastWriteTimeUtc.AddDays(1);
var tapes = installation.Tapes;
tapes.RecordLaunch("td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>(), "c-lok");
Assert.Equal(TapeRole.Other, tapes.List().Single().Role);
}
[Fact]
public void A_log_written_before_the_launch_is_not_taken_for_one_of_it()
{
var launch = new TapeLaunch(Launched, "td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>());
Assert.True(launch.Covers(Launched.AddMinutes(3)));
Assert.False(launch.Covers(Launched.AddHours(-1)));
}
[Theory]
[InlineData("pkp/ep07_v1", "ep07-424", RecorderKind.Hasler)]
[InlineData("pkp/en57al_v1", "en57al-1234", RecorderKind.Electronic)]
[InlineData("pkp/x4e_v1", "x4e-659", RecorderKind.Electronic)]
[InlineData("pkp/et22_v3", "et22-1100", RecorderKind.Hasler)]
public void The_recorder_follows_the_vehicle_behind_the_name(string folder, string type, RecorderKind kind) =>
Assert.Equal(kind, Recorders.Guess(new TapeVehicle("poc1_lok", folder, type), "poc1_lok"));
[Fact]
public void Without_a_launch_the_tape_name_is_all_there_is_to_go_by()
{
Assert.Equal(RecorderKind.Electronic, Recorders.Guess(null, "ED250-001"));
Assert.Equal(RecorderKind.Hasler, Recorders.Guess(null, "eu07-193"));
}
[Fact]
public void A_tape_of_the_last_launch_gets_its_clock_and_recorder()
{
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(TestInstallation.At("physicslog", "ED250-001.dat"), "t"));
installation.Clock.Now = Launched;
var tapes = installation.Tapes;
tapes.RecordLaunch("lk4.scn", new TimeSpan(14, 5, 0),
new[] { new TapeVehicle("ED250-001", "pkp/ed250_v1", "ed250-001") });
var context = tapes.ContextOf(tapes.List().Single());
Assert.Equal(new TimeSpan(14, 5, 0), context.StartClock);
Assert.Equal(RecorderKind.Electronic, context.Recorder);
}
[Fact]
public void A_tape_older_than_the_last_launch_keeps_its_hour_unknown()
{
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(TestInstallation.At("physicslog", "EU07-424.dat"), "t"));
installation.Clock.Now = installation.Files.LastWriteTimeUtc.AddDays(1);
var tapes = installation.Tapes;
tapes.RecordLaunch("td.scn", new TimeSpan(8, 0, 0), Array.Empty<TapeVehicle>());
Assert.Null(tapes.ContextOf(tapes.List().Single()).StartClock);
}
}