Преглед на файлове

Fixes #4325. ApplicationImpl.Invoke is sometimes running on UI thread when Application.Top is null (#4339)

BDisp преди 1 месец
родител
ревизия
1046b47be7

+ 1 - 1
Terminal.Gui/App/ApplicationImpl.cs

@@ -499,7 +499,7 @@ public class ApplicationImpl : IApplication
     public void Invoke (Action action)
     {
         // If we are already on the main UI thread
-        if (_mainThreadId == Thread.CurrentThread.ManagedThreadId)
+        if (Application.Top is { Running: true } && _mainThreadId == Thread.CurrentThread.ManagedThreadId)
         {
             action ();
             return;

+ 5 - 0
Terminal.Gui/Drivers/DotNetDriver/NetInput.cs

@@ -86,6 +86,11 @@ public class NetInput : ConsoleInput<ConsoleKeyInfo>, INetInput
     {
         base.Dispose ();
 
+        if (ConsoleDriver.RunningUnitTests)
+        {
+            return;
+        }
+
         // Disable mouse events first
         Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
 

+ 8 - 3
Tests/StressTests/ApplicationStressTests.cs

@@ -17,9 +17,7 @@ public class ApplicationStressTests : TestsAllViews
 
     private const int NUM_PASSES = 50;
     private const int NUM_INCREMENTS = 500;
-    
-    // Use longer timeout when running under debugger to account for slower iterations
-    private static readonly int POLL_MS = System.Diagnostics.Debugger.IsAttached ? 500 : 100;
+    private const int POLL_MS = 100;
 
     /// <summary>
     /// Stress test for Application.Invoke to verify that invocations from background threads
@@ -79,6 +77,13 @@ public class ApplicationStressTests : TestsAllViews
                 while (_tbCounter != (j + 1) * numIncrements) // Wait for tbCounter to reach expected value
                 {
                     int tbNow = _tbCounter;
+
+                    // Wait for Application.Top to be running to ensure timed events can be processed
+                    while (Application.Top is null || Application.Top is { Running: false })
+                    {
+                        Thread.Sleep (1);
+                    }
+
                     _wakeUp.Wait (pollMs);
 
                     if (_tbCounter != tbNow)

+ 1 - 2
Tests/UnitTests/Application/ApplicationTests.cs

@@ -586,11 +586,10 @@ public class ApplicationTests
     {
         var top = new Toplevel ();
         RunState rs = Application.Begin (top);
-        var firstIteration = false;
 
         var actionCalled = 0;
         Application.Invoke (() => { actionCalled++; });
-        Application.RunIteration (ref rs, firstIteration);
+        ApplicationImpl.Instance.TimedEvents!.RunTimers ();
         Assert.Equal (1, actionCalled);
         top.Dispose ();
         Application.Shutdown ();

+ 98 - 0
Tests/UnitTests/Application/MainLoopTests.cs

@@ -1,4 +1,5 @@
 using System.Diagnostics;
+using Xunit.Abstractions;
 
 // Alias Console to MockConsole so we don't accidentally use Console
 
@@ -7,6 +8,14 @@ namespace UnitTests.ApplicationTests;
 /// <summary>Tests MainLoop using the FakeMainLoop.</summary>
 public class MainLoopTests
 {
+    private readonly ITestOutputHelper _output;
+
+    public MainLoopTests (ITestOutputHelper output)
+    {
+        _output = output;
+        ConsoleDriver.RunningUnitTests = true;
+    }
+
     private static Button btn;
     private static string cancel;
     private static string clickMe;
@@ -708,6 +717,95 @@ public class MainLoopTests
         Assert.Equal (10, functionCalled);
     }
 
+    [Theory]
+    [InlineData ("fake")]
+    [InlineData ("windows")]
+    [InlineData ("dotnet")]
+    [InlineData ("unix")]
+    public void Application_Invoke_Run_TimedEvents (string driverName)
+    {
+        // Arrange
+        Application.Init (driverName: driverName);
+        var functionCalled = 0;
+        var stopwatch = new Stopwatch ();
+
+        // Act
+        Application.Invoke (() =>
+                            {
+                                // Stop the stopwatch *after* the function is called.
+                                functionCalled++;
+                                stopwatch.Stop ();
+                                Application.RequestStop ();
+                            });
+
+        // Start timing just before running the application loop.
+        stopwatch.Start ();
+        Application.Run ();
+
+        // Assert
+        Assert.NotNull (Application.Top);
+        Application.Top.Dispose ();
+        Application.Shutdown ();
+        Assert.Equal (1, functionCalled);
+
+        // Output the elapsed time for this test case.
+        // ReSharper disable once Xunit.XunitTestWithConsoleOutput
+        // ReSharper disable once LocalizableElement
+        Console.WriteLine ($"[{driverName}] Duration: {stopwatch.Elapsed.TotalMilliseconds:F2} ms");
+
+        // Output elapsed duration to xUnit's test output
+        _output.WriteLine ($"[{driverName}] Duration: {stopwatch.Elapsed.TotalMilliseconds:F2} ms");
+    }
+
+    [Theory]
+    [InlineData ("fake")]
+    [InlineData ("windows")]
+    [InlineData ("dotnet")]
+    [InlineData ("unix")]
+    public void Application_AddTimeout_Run_TimedEvents (string driverName)
+    {
+        // Arrange
+        Application.Init (driverName: driverName);
+        var functionCalled = 0;
+        var stopwatch = new Stopwatch ();
+
+        // Act
+        bool Function ()
+        {
+            functionCalled++;
+
+            if (functionCalled == 10 && Application.Top is { Running: true })
+            {
+                stopwatch.Stop ();
+                Application.RequestStop ();
+
+                return false;
+            }
+
+            return true;
+        }
+
+        Application.AddTimeout (TimeSpan.FromMilliseconds (1), Function);
+
+        // Start timing just before running the application loop.
+        stopwatch.Start ();
+        Application.Run ();
+
+        // Assert
+        Assert.NotNull (Application.Top);
+        Application.Top.Dispose ();
+        Application.Shutdown ();
+        Assert.Equal (10, functionCalled);
+
+        // Output the elapsed time for this test case.
+        // ReSharper disable once Xunit.XunitTestWithConsoleOutput
+        // ReSharper disable once LocalizableElement
+        Console.WriteLine ($"[{driverName}] Duration: {stopwatch.Elapsed.TotalMilliseconds:F2} ms");
+
+        // Output elapsed duration to xUnit's test output
+        _output.WriteLine ($"[{driverName}] Duration: {stopwatch.Elapsed.TotalMilliseconds:F2} ms");
+    }
+
     public static IEnumerable<object []> TestAddTimeout
     {
         get