diff --git a/Terminal.Gui/ViewBase/Runnable/Runnable.cs b/Terminal.Gui/ViewBase/Runnable/Runnable.cs index 018cbf087..d51363354 100644 --- a/Terminal.Gui/ViewBase/Runnable/Runnable.cs +++ b/Terminal.Gui/ViewBase/Runnable/Runnable.cs @@ -170,16 +170,6 @@ public class Runnable : View, IRunnable /// public void RaiseIsModalChangedEvent (bool newIsModal) { - // CWP Phase 3: Post-notification (work already done by Application) - OnIsModalChanged (newIsModal); - - EventArgs args = new (newIsModal); - IsModalChanged?.Invoke (this, args); - - // Layout may need to change when modal state changes - SetNeedsLayout (); - SetNeedsDraw (); - if (newIsModal) { // Set focus to self if becoming modal @@ -194,6 +184,16 @@ public class Runnable : View, IRunnable App?.Driver?.UpdateCursor (); } } + + // CWP Phase 3: Post-notification (work already done by Application) + OnIsModalChanged (newIsModal); + + EventArgs args = new (newIsModal); + IsModalChanged?.Invoke (this, args); + + // Layout may need to change when modal state changes + SetNeedsLayout (); + SetNeedsDraw (); } /// diff --git a/Tests/StressTests/ApplicationStressTests.cs b/Tests/StressTests/ApplicationStressTests.cs index ee695d167..cb51ac940 100644 --- a/Tests/StressTests/ApplicationStressTests.cs +++ b/Tests/StressTests/ApplicationStressTests.cs @@ -1,41 +1,44 @@ +using System.Diagnostics; using Xunit.Abstractions; +// ReSharper disable AccessToDisposedClosure + namespace StressTests; -public class ApplicationStressTests +public class ApplicationStressTests (ITestOutputHelper output) { - public ApplicationStressTests (ITestOutputHelper output) - { - } + private const int NUM_INCREMENTS = 500; + + private const int NUM_PASSES = 50; + private const int POLL_MS_DEBUGGER = 500; + private const int POLL_MS_NORMAL = 100; private static volatile int _tbCounter; #pragma warning disable IDE1006 // Naming Styles private static readonly ManualResetEventSlim _wakeUp = new (false); #pragma warning restore IDE1006 // Naming Styles - private const int NUM_PASSES = 50; - private const int NUM_INCREMENTS = 500; - private const int POLL_MS = 100; /// - /// Stress test for Application.Invoke to verify that invocations from background threads - /// are not lost or delayed indefinitely. Tests 25,000 concurrent invocations (50 passes × 500 increments). + /// Stress test for Application.Invoke to verify that invocations from background threads + /// are not lost or delayed indefinitely. Tests 25,000 concurrent invocations (50 passes × 500 increments). /// /// - /// - /// This test automatically adapts its timeout when running under a debugger (500ms vs 100ms) - /// to account for slower iteration times caused by debugger overhead. - /// - /// - /// See InvokeLeakTest_Analysis.md for technical details about the timing improvements made - /// to TimedEvents (Stopwatch-based timing) and Application.Invoke (MainLoop wakeup). - /// + /// + /// This test automatically adapts its timeout when running under a debugger (500ms vs 100ms) + /// to account for slower iteration times caused by debugger overhead. + /// + /// + /// See InvokeLeakTest_Analysis.md for technical details about the timing improvements made + /// to TimedEvents (Stopwatch-based timing) and Application.Invoke (MainLoop wakeup). + /// /// [Fact] public async Task InvokeLeakTest () { + IApplication app = Application.Create (); + app.Init ("fake"); - Application.Init (driverName: "fake"); Random r = new (); TextField tf = new (); var top = new Window (); @@ -43,20 +46,21 @@ public class ApplicationStressTests _tbCounter = 0; - Task task = Task.Run (() => RunTest (r, tf, NUM_PASSES, NUM_INCREMENTS, POLL_MS)); + int pollMs = Debugger.IsAttached ? POLL_MS_DEBUGGER : POLL_MS_NORMAL; + Task task = Task.Run (() => RunTest (app, r, tf, NUM_PASSES, NUM_INCREMENTS, pollMs)); // blocks here until the RequestStop is processed at the end of the test - Application.Run (top); + app.Run (top); await task; // Propagate exception if any occurred Assert.Equal (NUM_INCREMENTS * NUM_PASSES, _tbCounter); top.Dispose (); - Application.Shutdown (); + app.Dispose (); return; - static void RunTest (Random r, TextField tf, int numPasses, int numIncrements, int pollMs) + void RunTest (IApplication application, Random random, TextField textField, int numPasses, int numIncrements, int pollMsValue) { for (var j = 0; j < numPasses; j++) { @@ -64,52 +68,70 @@ public class ApplicationStressTests for (var i = 0; i < numIncrements; i++) { - Launch (r, tf, (j + 1) * numIncrements); + Launch (application, random, textField, (j + 1) * numIncrements); } + int maxWaitMs = pollMsValue * 50; // Maximum total wait time (5s normal, 25s debugger) + var elapsedMs = 0; + while (_tbCounter != (j + 1) * numIncrements) // Wait for tbCounter to reach expected value { int tbNow = _tbCounter; // Wait for Application.TopRunnable to be running to ensure timed events can be processed - while (Application.TopRunnableView is null || Application.TopRunnableView is IRunnable { IsRunning: false }) + var topRunnableWaitMs = 0; + + while (application.TopRunnableView is null or IRunnable { IsRunning: false }) { Thread.Sleep (1); + topRunnableWaitMs++; + + if (topRunnableWaitMs > maxWaitMs) + { + application.Invoke (application.Dispose); + + throw new TimeoutException ( + $"Timeout: TopRunnableView never started running on pass {j + 1}" + ); + } } - _wakeUp.Wait (pollMs); + _wakeUp.Wait (pollMsValue); + elapsedMs += pollMsValue; if (_tbCounter != tbNow) { + elapsedMs = 0; // Reset elapsed time on progress + continue; } - // No change after wait: Idle handlers added via Application.Invoke have gone missing - Application.Invoke (() => Application.RequestStop ()); + if (elapsedMs > maxWaitMs) + { + // No change after maximum wait: Idle handlers added via Application.Invoke have gone missing + application.Invoke (application.Dispose); - throw new TimeoutException ( - $"Timeout: Increment lost. _tbCounter ({_tbCounter}) didn't " - + $"change after waiting {pollMs} ms. Failed to reach {(j + 1) * numIncrements} on pass {j + 1}" - ); + throw new TimeoutException ( + $"Timeout: Increment lost. _tbCounter ({_tbCounter}) didn't " + + $"change after waiting {maxWaitMs} ms (pollMs={pollMsValue}). " + + $"Failed to reach {(j + 1) * numIncrements} on pass {j + 1}" + ); + } } - - ; } - Application.Invoke (() => Application.RequestStop ()); + application.Invoke (application.Dispose); } - static void Launch (Random r, TextField tf, int target) + static void Launch (IApplication application, Random random, TextField textField, int target) { - Task.Run ( - () => + Task.Run (() => { - Thread.Sleep (r.Next (2, 4)); + Thread.Sleep (random.Next (2, 4)); - Application.Invoke ( - () => + application.Invoke (() => { - tf.Text = $"index{r.Next ()}"; + textField.Text = $"index{random.Next ()}"; Interlocked.Increment (ref _tbCounter); if (target == _tbCounter)