namespace AcDream.App.Tests.Rendering; /// /// Fix #406: before this change, GameWindow.CompleteShutdown wrote /// a hardcoded exited{code:0,reason:"graceful"} status event /// whenever the resource-shutdown transaction converged — even when /// Dispose() (and therefore CompleteShutdown) ran mid-unwind /// of an exception that escaped Run()'s Silk.NET frame loop and was /// about to crash the process via the CLR's unhandled-exception path. /// GameWindow cannot be constructed without a live GPU/window (see /// the established pattern in GameWindowSlice8BoundaryTests), so /// this pins the fix as a source-shape test exactly like that file does /// for the surrounding shutdown machinery. /// public sealed class GameWindowCrashStatusTests { [Fact] public void Run_LatchesRunFailureBeforeRethrowingFromTheFrameLoopCatch() { string body = MethodBody( "public void Run()", "void IGameWindowPlatformPublication.PublishGraphics("); string tryBlock = Slice(body, "try\n {\n _window.Run();", "}\n }"); AssertAppearsInOrder( tryBlock, "_window.Run();", "catch (Exception failure)", "_constructionCleanup.RetainFrom(failure);", // The latch MUST happen before the rethrow: Dispose() (and // therefore CompleteShutdown/ReportExited) can run mid-unwind // of this exact exception, via Program.cs's // `using var window = ...`. "_runFailure = failure;", "throw;"); } [Fact] public void ReportExited_ChecksRunFailureBeforeEitherGracefulOrShutdownIncompletePaths() { string source = GameWindowSource(); string reportExited = Slice( source, "private void ReportExited(GameWindowLifetimeReport report)", "\n }\n"); Assert.Contains( "string sessionId = _options.SessionId ?? \"app\";", reportExited, StringComparison.Ordinal); AssertAppearsInOrder( reportExited, "if (_runFailure is not null)", "_statusWriter.Exited(sessionId, 1, \"crashed\");", "return;", "if (report.Status == GameWindowLifetimeStatus.Complete)", "_statusWriter.Exited(sessionId, 0, \"graceful\");", "_statusWriter.Exited(sessionId, 1, \"shutdown-incomplete\");"); // Every terminal-status write in CompleteShutdown funnels through // this ONE method — a second, uncoordinated call site would be // exactly how the pre-fix bug reappears. Assert.Equal( 2, CountOccurrences(source, "ReportExited(report)")); Assert.DoesNotContain( "_statusWriter.Exited(_options.SessionId ?? \"app\", 0, \"graceful\")", source, StringComparison.Ordinal); } [Fact] public void RunFailureFieldExistsAndDefaultsToNull() { string source = GameWindowSource(); Assert.Contains( "private Exception? _runFailure;", source, StringComparison.Ordinal); } private static string MethodBody(string start, string end) => Slice(GameWindowSource(), start, end); private static string Slice(string source, string start, string end) { int first = source.IndexOf(start, StringComparison.Ordinal); int last = source.IndexOf(end, first + 1, StringComparison.Ordinal); Assert.True(first >= 0, $"Missing source boundary: {start}"); Assert.True(last > first, $"Missing source boundary: {end}"); return source[first..last]; } private static int CountOccurrences(string source, string value) { int count = 0; int cursor = 0; while ((cursor = source.IndexOf(value, cursor, StringComparison.Ordinal)) >= 0) { count++; cursor += value.Length; } return count; } private static void AssertAppearsInOrder(string source, params string[] fragments) { int cursor = -1; foreach (string fragment in fragments) { int next = source.IndexOf(fragment, cursor + 1, StringComparison.Ordinal); Assert.True(next >= 0, $"Missing expected source fragment: {fragment}"); Assert.True(next > cursor, $"Out-of-order source fragment: {fragment}"); cursor = next; } } private static string GameWindowSource() => File.ReadAllText(Path.Combine( FindRepoRoot(), "src", "AcDream.App", "Rendering", "GameWindow.cs")).Replace("\r\n", "\n", StringComparison.Ordinal); private static string FindRepoRoot() { DirectoryInfo? directory = new(AppContext.BaseDirectory); while (directory is not null) { if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) return directory.FullName; directory = directory.Parent; } throw new DirectoryNotFoundException("Could not find AcDream.slnx."); } }