Browse Source

Fixes #4434 - `InvokeLeakTest` (#4435)

* pre-alpha -> alpha

* don't build docs for v2_release

* Pulled from v2_release

* Refactor migration guide for Terminal.Gui v2

Restructured and expanded the migration guide to provide a comprehensive resource for transitioning from Terminal.Gui v1 to v2. Key updates include:

- Added a Table of Contents for easier navigation.
- Summarized major architectural changes in v2, including the instance-based application model, IRunnable architecture, and 24-bit TrueColor support.
- Updated examples to reflect new patterns, such as initializers replacing constructors and explicit disposal using `IDisposable`.
- Documented changes to the layout system, including the removal of `Absolute`/`Computed` styles and the introduction of `Viewport`.
- Standardized event patterns to use `object sender, EventArgs args`.
- Detailed updates to the Keyboard, Mouse, and Navigation APIs, including configurable key bindings and viewport-relative mouse coordinates.
- Replaced legacy components like `ScrollView` and `ContextMenu` with built-in scrolling and `PopoverMenu`.
- Clarified disposal rules and introduced best practices for resource management.
- Provided a complete migration example and a summary of breaking changes.

This update aims to simplify the migration process by addressing breaking changes, introducing new features, and aligning with modern .NET conventions.

* Updated runnable

* Refactor ApplicationStressTests for modularity and robustness

Refactored `ApplicationStressTests` to use `IApplication`
instances instead of static methods, enabling better
testability and alignment with dependency injection.

Enhanced timeout handling in `RunTest` with elapsed time
tracking and debugger-aware polling intervals. Improved
error handling by introducing exceptions for timeouts and
ensuring proper resource cleanup with `application.Dispose`.

Refactored `Launch` and `InvokeLeakTest` methods for
clarity and consistency. Removed redundant code and
improved overall readability and maintainability.
Tig 1 week ago
parent
commit
c9868e9901
2 changed files with 74 additions and 52 deletions
  1. 10 10
      Terminal.Gui/ViewBase/Runnable/Runnable.cs
  2. 64 42
      Tests/StressTests/ApplicationStressTests.cs

+ 10 - 10
Terminal.Gui/ViewBase/Runnable/Runnable.cs

@@ -170,16 +170,6 @@ public class Runnable : View, IRunnable
     /// <inheritdoc/>
     public void RaiseIsModalChangedEvent (bool newIsModal)
     {
-        // CWP Phase 3: Post-notification (work already done by Application)
-        OnIsModalChanged (newIsModal);
-
-        EventArgs<bool> 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<bool> args = new (newIsModal);
+        IsModalChanged?.Invoke (this, args);
+
+        // Layout may need to change when modal state changes
+        SetNeedsLayout ();
+        SetNeedsDraw ();
     }
 
     /// <inheritdoc/>

+ 64 - 42
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;
 
     /// <summary>
-    /// 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).
     /// </summary>
     /// <remarks>
-    /// <para>
-    /// This test automatically adapts its timeout when running under a debugger (500ms vs 100ms)
-    /// to account for slower iteration times caused by debugger overhead.
-    /// </para>
-    /// <para>
-    /// See InvokeLeakTest_Analysis.md for technical details about the timing improvements made
-    /// to TimedEvents (Stopwatch-based timing) and Application.Invoke (MainLoop wakeup).
-    /// </para>
+    ///     <para>
+    ///         This test automatically adapts its timeout when running under a debugger (500ms vs 100ms)
+    ///         to account for slower iteration times caused by debugger overhead.
+    ///     </para>
+    ///     <para>
+    ///         See InvokeLeakTest_Analysis.md for technical details about the timing improvements made
+    ///         to TimedEvents (Stopwatch-based timing) and Application.Invoke (MainLoop wakeup).
+    ///     </para>
     /// </remarks>
     [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 ());
-
-                    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}"
-                                               );
+                    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 {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)