using System.Reflection; using System.Reflection.Emit; using AcDream.App.Rendering; using AcDream.App.Tests.Architecture; using AcDream.Runtime.Session; using Silk.NET.Windowing; namespace AcDream.App.Tests.Rendering; /// /// Fix #406: shutdown must not report a graceful exit while an exception from /// the native frame loop is still unwinding. These checks inspect compiled /// latch and status edges because constructing would /// require a real GPU/window. /// public sealed class GameWindowCrashStatusTests { [Fact] public void Run_LatchesRunFailureBeforeRethrowingFromTheFrameLoopCatch() { MethodInfo run = RequiredMethod(nameof(GameWindow.Run)); IReadOnlyList calls = CompiledCallGraph.Read(run); CompiledCall frameLoop = Assert.Single( calls, call => call.Target.Name == nameof(IWindow.Run) && call.Target.DeclaringType?.Namespace == "Silk.NET.Windowing"); CompiledCall retain = Assert.Single( calls, call => call.Target.DeclaringType == typeof(ResourceConstructionCleanupLedger) && call.Target.Name == nameof(ResourceConstructionCleanupLedger.RetainFrom)); CompiledFieldReference latch = Assert.Single( CompiledCallGraph.ReadFieldReferences(run), field => field.Field.Name == "_runFailure" && field.OpCode == OpCodes.Stfld); CompiledInstruction rethrow = Assert.Single( CompiledCallGraph.ReadInstructions(run), instruction => instruction.OpCode == OpCodes.Rethrow); Assert.True(frameLoop.Offset < retain.Offset); Assert.True(retain.Offset < latch.Offset); Assert.True(latch.Offset < rethrow.Offset); } [Fact] public void ReportExited_ChecksRunFailureBeforeEitherGracefulOrShutdownIncompletePaths() { MethodInfo reportExited = RequiredMethod("ReportExited"); IReadOnlyList calls = CompiledCallGraph.Read(reportExited); CompiledFieldReference failureRead = Assert.Single( CompiledCallGraph.ReadFieldReferences(reportExited), field => field.Field.Name == "_runFailure" && field.OpCode == OpCodes.Ldfld); CompiledCall[] exited = calls .Where(call => call.Target.DeclaringType == typeof(SessionStatusWriter) && call.Target.Name == nameof(SessionStatusWriter.Exited)) .ToArray(); Assert.Equal(3, exited.Length); CompiledCall status = Assert.Single( calls, call => call.Target.DeclaringType == typeof(GameWindowLifetimeReport) && call.Target.Name == "get_Status"); Assert.True(failureRead.Offset < exited[0].Offset); Assert.True(exited[0].Offset < status.Offset); Assert.True(status.Offset < exited[1].Offset); Assert.True(exited[1].Offset < exited[2].Offset); Assert.Equal( ["app", "crashed", "graceful", "shutdown-incomplete"], CompiledCallGraph.ReadStringLiterals(reportExited)); MethodInfo completeShutdown = RequiredMethod("CompleteShutdown"); Assert.Equal( 2, CompiledCallGraph.Read(completeShutdown).Count(call => call.Target.DeclaringType == typeof(GameWindow) && call.Target.Name == "ReportExited")); Assert.Equal( exited.Length, CompiledCallGraph.ReadDeclared(typeof(GameWindow)).Count(call => call.Target.DeclaringType == typeof(SessionStatusWriter) && call.Target.Name == nameof(SessionStatusWriter.Exited))); } [Fact] public void RunFailureFieldExistsAndDefaultsToNull() { FieldInfo field = typeof(GameWindow).GetField( "_runFailure", BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new MissingFieldException(typeof(GameWindow).FullName, "_runFailure"); Assert.Equal(typeof(Exception), field.FieldType); ConstructorInfo[] constructors = typeof(GameWindow).GetConstructors( BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); Assert.NotEmpty(constructors); Assert.DoesNotContain( constructors.SelectMany(CompiledCallGraph.ReadFieldReferences), reference => reference.Field == field && reference.OpCode == OpCodes.Stfld); } private static MethodInfo RequiredMethod(string name) => typeof(GameWindow).GetMethod( name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new MissingMethodException(typeof(GameWindow).FullName, name); }