Start the scenery editor from its own button and note launches

With no scenery selected the button starts a new one in the editor. Each launch
notes the scenery, the timetable number and the vehicles for the tapes. Tools
work on the selected scenery, and an emptied consist stays listed.
This commit is contained in:
maj00r
2026-11-01 16:06:16 +01:00
parent 6e45f5de59
commit 2ebff91316
9 changed files with 670 additions and 124 deletions

View File

@@ -32,8 +32,39 @@ public class SimulatorAftermathTests
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(TestInstallation.At("log.txt"), "first\r\nsecond\r\n\r\nthird\r\n"));
var tail = new SimulatorAftermath(installation.Files, installation.Paths, installation.Log).SimulatorLogTail(2);
var started = installation.Files.LastWriteTimeUtc.AddMinutes(-1);
var tail = new SimulatorAftermath(installation.Files, installation.Paths, installation.Log)
.SimulatorLogTail(2, started);
Assert.Equal(new[] { "second", "third" }, tail);
}
[Fact]
public void A_log_not_written_since_the_start_says_nothing_of_this_run()
{
// Dead before it opened its log: what is there is the session before.
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(TestInstallation.At("log.txt"), "yesterday's error\r\n"));
var started = installation.Files.LastWriteTimeUtc.AddMinutes(10);
var tail = new SimulatorAftermath(installation.Files, installation.Paths, installation.Log)
.SimulatorLogTail(2, started);
Assert.Empty(tail);
}
[Fact]
public void A_log_written_a_moment_before_the_start_was_taken_counts()
{
// The start is taken just after the launch, as for the crash dumps.
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(TestInstallation.At("log.txt"), "opening\r\n"));
var started = installation.Files.LastWriteTimeUtc.AddSeconds(3);
var tail = new SimulatorAftermath(installation.Files, installation.Paths, installation.Log)
.SimulatorLogTail(2, started);
Assert.Equal(new[] { "opening" }, tail);
}
}

View File

@@ -55,11 +55,28 @@ public class SimulatorExitTests
? ("cmd", new[] { "/c", "echo boom 1>&2 & exit 3" })
: ("/bin/sh", new[] { "-c", "echo boom >&2; exit 3" });
var process = launcher.Start(shell, script, null, captureErrors: true, out string? error);
var process = launcher.Start(shell, script, null, new Dictionary<string, string>(), captureErrors: true,
out string? error);
Assert.Null(error);
await process!.WaitForExitAsync();
Assert.Equal(3, process.ExitCode);
Assert.Equal("boom", Assert.Single(process.ErrorTail).Trim());
}
[Fact]
public async Task A_real_process_gets_the_environment_it_was_given()
{
var launcher = new SystemProcessLauncher(new TestInstallation().Log);
var (shell, script) = OperatingSystem.IsWindows()
? ("cmd", new[] { "/c", "echo %__NV_PRIME_RENDER_OFFLOAD% 1>&2" })
: ("/bin/sh", new[] { "-c", "echo $__NV_PRIME_RENDER_OFFLOAD >&2" });
var process = launcher.Start(shell, script, null,
new Dictionary<string, string> { ["__NV_PRIME_RENDER_OFFLOAD"] = "1" },
captureErrors: true, out _);
await process!.WaitForExitAsync();
Assert.Equal("1", Assert.Single(process.ErrorTail).Trim());
}
}

View File

@@ -1,5 +1,7 @@
using StarterNG.Application;
using StarterNG.Application.Abstractions;
using StarterNG.Classes;
using StarterNG.Infrastructure;
using StarterNG.Infrastructure.Sceneries;
using StarterNG.Tests.Fakes;
@@ -23,6 +25,58 @@ public class StartSimulationTests
Assert.Equal(installation.Paths.Root, launcher.WorkingDirectory);
}
[Fact]
public void With_the_physics_log_on_the_launch_is_noted_for_the_tapes()
{
var (start, launcher, installation) = Rig(files => files.WithExecutable(Build), sceneryClock: "07:52");
installation.SettingsStore.Settings.PhysicsLog = true;
start.Execute(freeFly: false, saveSettings: false);
// Under the name the simulator is given, which is the name it logs under.
string driven = launcher.Arguments![launcher.Arguments.ToList().IndexOf("-v") + 1];
string noted = installation.Files.ReadAllText(TestInstallation.At("physicslog", "starter-launch.txt"));
Assert.Contains("clock\t07:52", noted);
Assert.Contains($"vehicle\t{driven}\tPKP/EP07\tep07.mmd", noted);
}
[Fact]
public void Without_the_physics_log_nothing_is_noted()
{
var (start, _, installation) = Rig(files => files.WithExecutable(Build));
start.Execute(freeFly: false, saveSettings: false);
Assert.False(installation.Files.FileExists(TestInstallation.At("physicslog", "starter-launch.txt")));
}
[Theory]
[InlineData("v2")]
[InlineData("V2")]
[InlineData("gone")]
[InlineData(null)]
[InlineData("")]
public void A_crew_is_there_to_start_with_when_the_pick_has_one_or_names_nothing(string? preferred) =>
Assert.True(StartSimulation.HasCrewToStart(WagonAndLocomotive, preferred));
[Fact]
public void A_pick_with_nobody_in_it_says_no_crew() =>
Assert.False(StartSimulation.HasCrewToStart(WagonAndLocomotive, "v1"));
[Fact]
public void A_consist_nobody_sits_in_has_no_crew_to_start()
{
var empty = new Trainset("trainset a tor 0 0\nnode -1 0 v1 dynamic d s m 0 nobody 3 0 enddynamic\nendtrainset");
Assert.False(StartSimulation.HasCrewToStart(empty, null));
}
private static Trainset WagonAndLocomotive => new(
"trainset a tor 0 0\n" +
"node -1 0 v1 dynamic d s m 0 nobody 3 0 enddynamic\n" +
"node -1 0 v2 dynamic d s m 0 headdriver 3 0 enddynamic\n" +
"endtrainset");
[Fact]
public void A_build_for_the_other_system_is_still_launched()
{
@@ -54,6 +108,49 @@ public class StartSimulationTests
Assert.Equal(installation.Paths.Root, launcher.WorkingDirectory);
}
[Fact]
public void Wine_is_pointed_at_the_settings_the_starter_saves()
{
string wine = Path.Combine("/usr/bin", "wine");
string exe = Path.Combine(Path.GetTempPath(), "maszyna", "eu07.exe");
const string roaming = "/home/kolejarz/.wine/drive_c/users/kolejarz/AppData/Roaming";
var (start, _, installation) = Rig(files => files
.WithExecutable(wine)
.WithFile(exe, "MZ\u0090\u0000")
.WithDirectory(roaming));
installation.Environment.With("PATH", "/usr/bin").With("HOME", "/home/kolejarz").With("USER", "kolejarz");
installation.SettingsStore.Settings.ExecutablePath = exe;
start.Execute(freeFly: true, saveSettings: false);
Assert.Equal(Path.GetFullPath(Path.Combine("/home/kolejarz", ".config", "MaSzyna")),
installation.Files.LinkTarget(Path.Combine(roaming, "MaSzyna")));
}
[Fact]
public void A_native_build_leaves_the_wine_prefix_alone()
{
const string roaming = "/home/kolejarz/.wine/drive_c/users/kolejarz/AppData/Roaming";
var (start, _, installation) = Rig(files => files.WithExecutable(Build).WithDirectory(roaming));
installation.Environment.With("HOME", "/home/kolejarz").With("USER", "kolejarz");
start.Execute(freeFly: true, saveSettings: false);
Assert.Null(installation.Files.LinkTarget(roaming + "/MaSzyna"));
}
[Theory]
[InlineData(0)]
[InlineData(3)]
public void Any_renderer_is_left_for_the_simulator_to_take(int renderer)
{
var (start, launcher, installation) = Rig(files => files.WithExecutable(Build, "\u007fELF binary"));
installation.SettingsStore.Settings.RenderEngine = renderer;
Assert.Equal(SimulationStartOutcome.Started, start.Execute(freeFly: true, saveSettings: false).Outcome);
Assert.NotNull(launcher.Executable);
}
[Fact]
public void The_error_stream_is_kept_only_while_the_starter_stays_to_read_it()
{
@@ -80,6 +177,28 @@ public class StartSimulationTests
Assert.False(installation.Files.FileExists(TestInstallation.At("scenery", "$td.scn")));
}
[Fact]
public void A_new_scenery_opens_at_the_wizard_even_with_a_scenery_selected()
{
var (start, launcher, _) = Rig(files => files.WithExecutable(Build));
var result = start.ExecuteNewSceneryEditor();
Assert.Equal(SimulationStartOutcome.Started, result.Outcome);
Assert.Equal(new[] { "-edit" }, launcher.Arguments);
}
[Fact]
public void With_no_scenery_selected_the_editor_opens_at_its_wizard()
{
var (start, launcher, _) = Rig(files => files.WithExecutable(Build), state => state.CurrentScenery = null);
var result = start.ExecuteEditor();
Assert.Equal(SimulationStartOutcome.Started, result.Outcome);
Assert.Equal(new[] { "-edit" }, launcher.Arguments);
}
[Fact]
public void A_file_that_cannot_be_run_is_not()
{
@@ -92,8 +211,134 @@ public class StartSimulationTests
Assert.Null(launcher.Executable);
}
[Fact]
public void A_scenery_with_no_consist_picked_starts_with_the_free_camera()
{
var (start, launcher, _) = Rig(files => files.WithExecutable(Build), state => state.CurrentTrainset = null);
var result = start.Execute(freeFly: false, saveSettings: false);
Assert.Equal(SimulationStartOutcome.Started, result.Outcome);
Assert.Equal(new[] { "-s", "$td.scn" }, launcher.Arguments);
}
[Fact]
public void The_copy_for_a_start_is_named_in_lower_case_as_the_simulator_looks_for_it()
{
var (start, launcher, _) = Rig(files => files.WithExecutable(Build), sceneryFile: "Glinojeck.scn");
start.Execute(freeFly: true, saveSettings: false);
Assert.Equal(new[] { "-s", "$glinojeck.scn" }, launcher.Arguments);
}
[Fact]
public void A_free_camera_start_without_a_consist_notes_no_driven_vehicle_for_the_tapes()
{
var (start, launcher, installation) = Rig(files => files.WithExecutable(Build),
state => state.CurrentTrainset = null);
installation.SettingsStore.Settings.PhysicsLog = true;
start.Execute(freeFly: true, saveSettings: false);
Assert.NotNull(launcher.Executable);
var note = StarterNG.Domain.Tapes.TapeLaunch.Parse(
installation.Files.ReadAllText(TestInstallation.At("physicslog", "starter-launch.txt")))!;
Assert.Null(note.Driven);
Assert.Empty(note.Consist!);
}
[Fact]
public void A_start_in_a_vehicle_notes_it_and_its_consist_for_the_tapes()
{
var (start, launcher, installation) = Rig(files => files.WithExecutable(Build));
installation.SettingsStore.Settings.PhysicsLog = true;
start.Execute(freeFly: false, saveSettings: false);
// Under the name the simulator is given, which is the name it logs under.
string given = launcher.Arguments![launcher.Arguments.ToList().IndexOf("-v") + 1];
var note = StarterNG.Domain.Tapes.TapeLaunch.Parse(
installation.Files.ReadAllText(TestInstallation.At("physicslog", "starter-launch.txt")))!;
Assert.Equal(given, note.Driven);
Assert.Equal(new[] { given }, note.Consist);
}
[Fact]
public void A_start_notes_the_scenario_and_the_train_of_the_timetable_for_the_tapes()
{
var (start, _, installation) = Rig(files =>
{
files.WithExecutable(Build);
files.WithFile(TestInstallation.At("scenery", "pociag.txt"),
"[ Rodzaj i numer pociagu | MPJ15120 ]\r\n");
});
installation.SettingsStore.Settings.PhysicsLog = true;
start.Execute(freeFly: false, saveSettings: false);
var note = StarterNG.Domain.Tapes.TapeLaunch.Parse(
installation.Files.ReadAllText(TestInstallation.At("physicslog", "starter-launch.txt")))!;
Assert.Equal("Test", note.SceneryName);
Assert.Equal("MPJ15120", note.Train);
}
[Fact]
public void A_free_camera_start_notes_no_train_for_the_tapes()
{
var (start, _, installation) = Rig(files => files.WithExecutable(Build));
installation.SettingsStore.Settings.PhysicsLog = true;
start.Execute(freeFly: true, saveSettings: false);
var note = StarterNG.Domain.Tapes.TapeLaunch.Parse(
installation.Files.ReadAllText(TestInstallation.At("physicslog", "starter-launch.txt")))!;
Assert.Null(note.Train);
}
private static void HybridLaptop(InMemoryFileSystem files)
{
files.WithFile("/sys/class/drm/card0/device/uevent",
"DRIVER=nvidia\nPCI_ID=10DE:2D19\nPCI_SLOT_NAME=0000:01:00.0\n")
.WithFile("/sys/class/drm/card0/device/boot_vga", "0\n")
.WithFile("/sys/class/drm/card1/device/uevent",
"DRIVER=amdgpu\nPCI_ID=1002:1900\nPCI_SLOT_NAME=0000:65:00.0\n")
.WithFile("/sys/class/drm/card1/device/boot_vga", "1\n")
.WithFile("/sys/class/drm/card1-eDP-1/status", "connected\n");
}
[Fact]
public void On_a_laptop_with_two_cards_the_simulator_goes_to_the_discrete_one()
{
var (start, launcher, _) = Rig(files =>
{
files.WithExecutable(Build);
HybridLaptop(files);
});
start.Execute(freeFly: true, saveSettings: false);
Assert.Equal("1", launcher.Environment!["__NV_PRIME_RENDER_OFFLOAD"]);
}
[Fact]
public void Left_to_the_system_the_simulator_gets_no_card_of_the_starters_choosing()
{
var (start, launcher, installation) = Rig(files =>
{
files.WithExecutable(Build);
HybridLaptop(files);
});
installation.SettingsStore.Settings.Gpu = StarterNG.Domain.Settings.GpuPreference.System;
start.Execute(freeFly: true, saveSettings: false);
Assert.Empty(launcher.Environment!);
}
private static (StartSimulation Start, RecordingLauncher Launcher, TestInstallation Installation) Rig(
Action<InMemoryFileSystem> addSimulator)
Action<InMemoryFileSystem> addSimulator, Action<AppState>? arrange = null, string sceneryFile = "td.scn",
string sceneryClock = "10:30")
{
var installation = new TestInstallation();
addSimulator(installation.Files);
@@ -103,7 +348,7 @@ public class StartSimulationTests
settings.ExecutablePath = Build;
var scenery = new SceneryParser(installation.Clock, installation.Random).Parse(
TestInstallation.At("scenery", "td.scn"),
TestInstallation.At("scenery", sceneryFile),
"""
//$n Test
trainset pociag tor_1 10 0
@@ -111,12 +356,20 @@ public class StartSimulationTests
endtrainset
FirstInit
""");
scenery.Weather.ScenarioTimeOverride = sceneryClock;
var state = new AppState { CurrentScenery = scenery, CurrentTrainset = scenery.Trainsets[0] };
arrange?.Invoke(state);
var launcher = new RecordingLauncher();
var start = new StartSimulation(state, installation.SettingsStore, installation.Library.Vehicles,
installation.Executables, installation.Files, installation.Paths, launcher,
installation.Random, installation.Log);
installation.Executables,
installation.Files, installation.Paths, launcher,
installation.Random, installation.Log,
new LinuxGpus(installation.Files, installation.Environment),
installation.Environment,
new WineConfigLink(installation.Files, installation.Environment,
installation.SettingsPaths, installation.Log),
installation.Tapes);
return (start, launcher, installation);
}
@@ -130,9 +383,13 @@ public class StartSimulationTests
public bool? CapturedErrors { get; private set; }
public IReadOnlyDictionary<string, string>? Environment { get; private set; }
public IProcessHandle? Start(string executablePath, IReadOnlyList<string> arguments, string? workingDirectory,
bool captureErrors, out string? error)
IReadOnlyDictionary<string, string> environment, bool captureErrors,
out string? error)
{
Environment = environment;
Executable = executablePath;
CapturedErrors = captureErrors;
Arguments = arguments;
@@ -144,6 +401,8 @@ public class StartSimulationTests
public bool OpenInShell(string pathOrUrl) => false;
public IProcessHandle? FindRunning(string processName) => null;
public bool IsRunning(string processName) => false;
}
private sealed class RunningProcess : IProcessHandle