Report a simulator that crashes or closes right after the start

The exit code, read with its signal or NTSTATUS, and the last lines of the
error stream - or of log.txt when it stayed silent. The stream is captured
only while the starter stays to read it.
This commit is contained in:
maj00r
2026-10-04 13:38:29 +02:00
parent 04f6a4cc8d
commit 59798e41fa
12 changed files with 316 additions and 13 deletions

View File

@@ -0,0 +1,65 @@
using StarterNG.Application;
using StarterNG.Infrastructure.Adapters;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests;
public class SimulatorExitTests
{
private static readonly TimeSpan Session = TimeSpan.FromMinutes(20);
[Fact]
public void A_session_ended_cleanly_is_no_failure()
{
Assert.False(new SimulatorExit(0, Session, []).Failed);
}
[Fact]
public void An_error_code_is_a_failure()
{
Assert.True(new SimulatorExit(1, Session, []).Failed);
}
[Fact]
public void A_window_that_only_flashed_is_a_failure_even_with_a_clean_code()
{
var exit = new SimulatorExit(0, TimeSpan.FromSeconds(0.2), []);
Assert.True(exit.Failed);
Assert.True(exit.Quick);
}
[Theory]
[InlineData(139, "139 (SIGSEGV)")]
[InlineData(134, "134 (SIGABRT)")]
[InlineData(-1073741819, "0xC0000005 (access violation)")]
[InlineData(3, "3")]
public void The_code_says_what_killed_it(int code, string described)
{
Assert.Equal(described, new SimulatorExit(code, Session, []).DescribeCode());
}
[Fact]
public void Colour_codes_and_blank_lines_are_dropped_from_the_errors()
{
var exit = new SimulatorExit(1, Session, ["\u001b[31mBad file: texture\u001b[0m", "", " "]);
Assert.Equal(["Bad file: texture"], exit.CleanErrors);
}
[Fact]
public async Task A_real_process_leaves_its_exit_code_and_error_stream()
{
var launcher = new SystemProcessLauncher(new TestInstallation().Log);
var (shell, script) = OperatingSystem.IsWindows()
? ("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);
Assert.Null(error);
await process!.WaitForExitAsync();
Assert.Equal(3, process.ExitCode);
Assert.Equal("boom", Assert.Single(process.ErrorTail).Trim());
}
}

View File

@@ -54,6 +54,30 @@ public class StartSimulationTests
Assert.Equal(installation.Paths.Root, launcher.WorkingDirectory);
}
[Fact]
public void The_error_stream_is_kept_only_while_the_starter_stays_to_read_it()
{
var (start, launcher, installation) = Rig(files => files.WithExecutable(Build));
start.Execute(freeFly: true, saveSettings: false);
Assert.True(launcher.CapturedErrors);
// Closing right after the launch would leave the pipe with no reader.
installation.SettingsStore.Settings.AutoCloseStarter = true;
start.Execute(freeFly: true, saveSettings: false);
Assert.False(launcher.CapturedErrors);
}
[Fact]
public void The_end_of_the_simulator_log_is_read_for_a_silent_crash()
{
var (start, _, _) = Rig(files => files
.WithExecutable(Build)
.WithFile(TestInstallation.At("log.txt"), "first\r\nsecond\r\n\r\nthird\r\n"));
Assert.Equal(new[] { "second", "third" }, start.SimulatorLogTail(2));
}
[Fact]
public void A_file_that_cannot_be_run_is_not()
{
@@ -102,10 +126,13 @@ public class StartSimulationTests
public IReadOnlyList<string>? Arguments { get; private set; }
public bool? CapturedErrors { get; private set; }
public IProcessHandle? Start(string executablePath, IReadOnlyList<string> arguments, string? workingDirectory,
out string? error)
bool captureErrors, out string? error)
{
Executable = executablePath;
CapturedErrors = captureErrors;
Arguments = arguments;
WorkingDirectory = workingDirectory;
error = null;
@@ -121,6 +148,10 @@ public class StartSimulationTests
{
public bool HasExited => false;
public int? ExitCode => null;
public IReadOnlyList<string> ErrorTail => Array.Empty<string>();
public Task WaitForExitAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
}
}