瀏覽代碼

Fixes #4391. Weird situation where ForceDriver with args doesn't persists on open scenario (#4395)

* Fixes #4391. Weird situation where ForceDriver with args doesn't persists on open scenario

* Prevents change ForceDriver if app it's already initialized and allowing also initialize with driver instead of only by driver name.

* Add dispose into FakeDriverBase and reset ForceDriver

* Moving test to Application folder

* Fix unit test

* Remove unnecessary GlobalTestSetup

* Add GC.SuppressFinalize

* Revert "Add GC.SuppressFinalize"

This reverts commit 2bd7cd7791be49449d84ea1f8011c341f295d8a8.

* Reset MouseGrabView

* Avoid CI warnings

* Add GlobalTestSetup in all test that use Application

* Trying to fix unit test

* Reverting scope changes

* Remove UICatalog testing Run

* Force re-run CI test

* Fix merge errors

* Fix ansi for the red background color code

* Fix more ANSI color code unit tests

---------

Co-authored-by: Tig <[email protected]>
BDisp 3 周之前
父節點
當前提交
1bd5e3761a

+ 12 - 2
Examples/UICatalog/UICatalog.cs

@@ -55,7 +55,9 @@ namespace UICatalog;
 /// </remarks>
 public class UICatalog
 {
-    private static string? _forceDriver = null;
+    private static string? _forceDriver;
+    private static string? _uiCatalogDriver;
+    private static string? _scenarioDriver;
 
     public static string LogFilePath { get; set; } = string.Empty;
     public static LoggingLevelSwitch LogLevelSwitch { get; } = new ();
@@ -194,6 +196,8 @@ public class UICatalog
 
         UICatalogMain (Options);
 
+        Debug.Assert (Application.ForceDriver == string.Empty);
+
         return 0;
     }
 
@@ -255,7 +259,9 @@ public class UICatalog
 
         Application.Init (driverName: _forceDriver);
 
-        var top = Application.Run<UICatalogTop> ();
+        _uiCatalogDriver = Application.Driver!.GetName ();
+
+        Toplevel top = Application.Run<UICatalogTop> ();
         top.Dispose ();
         Application.Shutdown ();
         VerifyObjectsWereDisposed ();
@@ -421,6 +427,8 @@ public class UICatalog
             Application.InitializedChanged += ApplicationOnInitializedChanged;
 #endif
 
+            Application.ForceDriver = _forceDriver;
+
             scenario.Main ();
             scenario.Dispose ();
 
@@ -439,6 +447,8 @@ public class UICatalog
                 if (e.Value)
                 {
                     sw.Start ();
+                    _scenarioDriver = Application.Driver!.GetName ();
+                    Debug.Assert (_scenarioDriver == _uiCatalogDriver);
                 }
                 else
                 {

+ 15 - 1
Terminal.Gui/App/Application.Driver.cs

@@ -28,7 +28,21 @@ public static partial class Application // Driver abstractions
     public static string ForceDriver
     {
         get => ApplicationImpl.Instance.ForceDriver;
-        set => ApplicationImpl.Instance.ForceDriver = value;
+        set
+        {
+            if (!string.IsNullOrEmpty (ApplicationImpl.Instance.ForceDriver) && value != Driver?.GetName ())
+            {
+                // ForceDriver cannot be changed if it has a valid value
+                return;
+            }
+
+            if (ApplicationImpl.Instance.Initialized && value != Driver?.GetName ())
+            {
+                throw new InvalidOperationException ($"The {nameof (ForceDriver)} can only be set before initialized.");
+            }
+
+            ApplicationImpl.Instance.ForceDriver = value;
+        }
     }
 
     /// <inheritdoc cref="IApplication.Sixel"/>

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

@@ -34,7 +34,7 @@ public partial class ApplicationImpl
         bool factoryIsFake = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
 
         // Then check driverName
-        bool nameIsWindows = driverName?.Contains ("win", StringComparison.OrdinalIgnoreCase) ?? false;
+        bool nameIsWindows = driverName?.Contains ("windows", StringComparison.OrdinalIgnoreCase) ?? false;
         bool nameIsDotNet = driverName?.Contains ("dotnet", StringComparison.OrdinalIgnoreCase) ?? false;
         bool nameIsUnix = driverName?.Contains ("unix", StringComparison.OrdinalIgnoreCase) ?? false;
         bool nameIsFake = driverName?.Contains ("fake", StringComparison.OrdinalIgnoreCase) ?? false;

+ 16 - 6
Terminal.Gui/App/ApplicationImpl.Lifecycle.cs

@@ -33,8 +33,8 @@ public partial class ApplicationImpl
             _driverName = ForceDriver;
         }
 
-       // Debug.Assert (Navigation is null);
-       // Navigation = new ();
+        // Debug.Assert (Navigation is null);
+        // Navigation = new ();
 
         //Debug.Assert (Popover is null);
         //Popover = new ();
@@ -62,7 +62,7 @@ public partial class ApplicationImpl
             _keyboard.PrevTabGroupKey = existingPrevTabGroupKey;
         }
 
-        CreateDriver (driverName ?? _driverName);
+        CreateDriver (_driverName);
         Screen = Driver!.Screen;
         Initialized = true;
 
@@ -145,9 +145,13 @@ public partial class ApplicationImpl
     }
 #endif
 
+    private bool _isResetingState;
+
     /// <inheritdoc/>
     public void ResetState (bool ignoreDisposed = false)
     {
+        _isResetingState = true;
+
         // Shutdown is the bookend for Init. As such it needs to clean up all resources
         // Init created. Apps that do any threading will need to code defensively for this.
         // e.g. see Issue #537
@@ -231,7 +235,14 @@ public partial class ApplicationImpl
         // === 9. Clear graphics ===
         Sixel.Clear ();
 
-        // === 10. Reset synchronization context ===
+        // === 10. Reset ForceDriver ===
+        // Note: ForceDriver and Force16Colors are reset
+        // If they need to persist across Init/Shutdown cycles
+        // then the user of the library should manage that state
+        Force16Colors = false;
+        ForceDriver = string.Empty;
+
+        // === 11. Reset synchronization context ===
         // IMPORTANT: Always reset sync context, even if not initialized
         // This ensures cleanup works correctly even if Shutdown is called without Init
         // Reset synchronization context to allow the user to run async/await,
@@ -240,8 +251,7 @@ public partial class ApplicationImpl
         // (https://github.com/gui-cs/Terminal.Gui/issues/1084).
         SynchronizationContext.SetSynchronizationContext (null);
 
-        // Note: ForceDriver and Force16Colors are NOT reset; 
-        // they need to persist across Init/Shutdown cycles
+        _isResetingState = false;
     }
 
     /// <summary>

+ 1 - 0
Terminal.Gui/App/Mouse/MouseImpl.cs

@@ -254,6 +254,7 @@ internal class MouseImpl : IMouse
         // Do not clear LastMousePosition; Popover's require it to stay set with last mouse pos.
         CachedViewsUnderMouse.Clear ();
         MouseEvent = null;
+        MouseGrabView = null;
     }
 
     // Mouse grab functionality merged from MouseGrabHandler

+ 12 - 6
Tests/UnitTests/FakeDriverBase.cs

@@ -4,7 +4,7 @@ namespace UnitTests;
 ///     Enables tests to create a FakeDriver for testing purposes.
 /// </summary>
 [Collection ("Global Test Setup")]
-public abstract class FakeDriverBase
+public abstract class FakeDriverBase : IDisposable
 {
     /// <summary>
     ///     Creates a new FakeDriver instance with the specified buffer size.
@@ -19,14 +19,20 @@ public abstract class FakeDriverBase
         var output = new FakeOutput ();
 
         DriverImpl driver = new (
-                                                          new FakeInputProcessor (null),
-                                                          new OutputBufferImpl (),
-                                                          output,
-                                                          new AnsiRequestScheduler (new AnsiResponseParser ()),
-                                                          new SizeMonitorImpl (output));
+                                 new FakeInputProcessor (null),
+                                 new OutputBufferImpl (),
+                                 output,
+                                 new AnsiRequestScheduler (new AnsiResponseParser ()),
+                                 new SizeMonitorImpl (output));
 
         driver.SetScreenSize (width, height);
 
         return driver;
     }
+
+    /// <inheritdoc />
+    public void Dispose ()
+    {
+        Application.ResetState (true);
+    }
 }

+ 0 - 1
Tests/UnitTests/TestsAllViews.cs

@@ -1,6 +1,5 @@
 #nullable enable
 using System.Reflection;
-using System.Drawing;
 
 namespace UnitTests;
 

+ 41 - 0
Tests/UnitTestsParallelizable/Application/ApplicationForceDriverTests.cs

@@ -0,0 +1,41 @@
+using UnitTests;
+
+namespace UnitTests_Parallelizable.ApplicationTests;
+
+public class ApplicationForceDriverTests : FakeDriverBase
+{
+    [Fact]
+    public void ForceDriver_Does_Not_Changes_If_It_Has_Valid_Value ()
+    {
+        Assert.False (Application.Initialized);
+        Assert.Null (Application.Driver);
+        Assert.Equal (string.Empty, Application.ForceDriver);
+
+        Application.ForceDriver = "fake";
+        Assert.Equal ("fake", Application.ForceDriver);
+
+        Application.ForceDriver = "dotnet";
+        Assert.Equal ("fake", Application.ForceDriver);
+    }
+
+    [Fact]
+    public void ForceDriver_Throws_If_Initialized_Changed_To_Another_Value ()
+    {
+        IDriver driver = CreateFakeDriver ();
+
+        Assert.False (Application.Initialized);
+        Assert.Null (Application.Driver);
+        Assert.Equal (string.Empty, Application.ForceDriver);
+
+        Application.Init (driverName: "fake");
+        Assert.True (Application.Initialized);
+        Assert.NotNull (Application.Driver);
+        Assert.Equal ("fake", Application.Driver.GetName ());
+        Assert.Equal (string.Empty, Application.ForceDriver);
+
+        Assert.Throws<InvalidOperationException> (() => Application.ForceDriver = "dotnet");
+
+        Application.ForceDriver = "fake";
+        Assert.Equal ("fake", Application.ForceDriver);
+    }
+}

+ 1 - 1
Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs

@@ -1,4 +1,4 @@
-#nullable enable
+#nullable enable
 using Moq;
 using Terminal.Gui.App;
 

+ 67 - 32
Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs

@@ -40,10 +40,13 @@ public class ToAnsiTests : FakeDriverBase
         Assert.Equal (3, lines.Length);
     }
 
-    [Fact]
-    public void ToAnsi_With_Colors ()
+    [Theory]
+    [InlineData (true, "\u001b[31m", "\u001b[34m")]
+    [InlineData (false, "\u001b[38;2;255;0;0m", "\u001b[38;2;0;0;255")]
+    public void ToAnsi_With_Colors (bool force16Colors, string expectedRed, string expectedBue)
     {
         IDriver driver = CreateFakeDriver (10, 2);
+        driver.Force16Colors = force16Colors;
 
         // Set red foreground
         driver.CurrentAttribute = new Attribute (Color.Red, Color.Black);
@@ -56,26 +59,42 @@ public class ToAnsiTests : FakeDriverBase
 
         string ansi = driver.ToAnsi ();
 
+        Assert.True (driver.Force16Colors == force16Colors);
         // Should contain ANSI color codes
-        Assert.Contains ("\u001b[31m", ansi); // Red foreground
-        Assert.Contains ("\u001b[34m", ansi); // Blue foreground
+        Assert.Contains (expectedRed, ansi); // Red foreground
+        Assert.Contains (expectedBue, ansi); // Blue foreground
         Assert.Contains ("Red", ansi);
         Assert.Contains ("Blue", ansi);
     }
 
-    [Fact]
-    public void ToAnsi_With_Background_Colors ()
+    [Theory]
+    [InlineData (false, "\u001b[48;2;")]
+    [InlineData (true, "\u001b[41m")]
+    public void ToAnsi_With_Background_Colors (bool force16Colors, string expected)
     {
         IDriver driver = CreateFakeDriver (10, 2);
+        Application.Force16Colors = force16Colors;
 
         // Set background color
-        driver.CurrentAttribute = new Attribute (Color.White, Color.Red);
+        driver.CurrentAttribute = new (Color.White, Color.Red);
         driver.AddStr ("WhiteOnRed");
 
         string ansi = driver.ToAnsi ();
 
+        /*
+         The ANSI escape sequence for red background (8-color) is ESC[41m — where ESC is \x1b (or \u001b).
+           Examples:
+           •	C# string: "\u001b[41m" or "\x1b[41m"
+           •	Reset (clear attributes): "\u001b[0m"
+           Notes:
+           •	Bright/red background (16-color bright variant) uses ESC[101m ("\u001b[101m").
+           •	For 24-bit RGB background use ESC[48;2;<r>;<g>;<b>m, e.g. "\u001b[48;2;255;0;0m" for pure red.
+         */
+
+        Assert.True (driver.Force16Colors == force16Colors);
+
         // Should contain ANSI background color code
-        Assert.Contains ("\u001b[41m", ansi); // Red background
+        Assert.Contains (expected, ansi); // Red background
         Assert.Contains ("WhiteOnRed", ansi);
     }
 
@@ -138,10 +157,13 @@ public class ToAnsiTests : FakeDriverBase
         Assert.Contains ("???", ansi);
     }
 
-    [Fact]
-    public void ToAnsi_Attribute_Changes_Within_Line ()
+    [Theory]
+    [InlineData (true, "\u001b[31m", "\u001b[34m")]
+    [InlineData (false, "\u001b[38;2;", "\u001b[48;2;")]
+    public void ToAnsi_Attribute_Changes_Within_Line (bool force16Colors, string expectedRed, string expectedBlue)
     {
         IDriver driver = CreateFakeDriver (20, 1);
+        driver.Force16Colors = force16Colors;
 
         driver.AddStr ("Normal");
         driver.CurrentAttribute = new Attribute (Color.Red, Color.Black);
@@ -151,10 +173,11 @@ public class ToAnsiTests : FakeDriverBase
 
         string ansi = driver.ToAnsi ();
 
+        Assert.True (driver.Force16Colors == force16Colors);
         // Should contain color changes within the line
         Assert.Contains ("Normal", ansi);
-        Assert.Contains ("\u001b[31m", ansi); // Red
-        Assert.Contains ("\u001b[34m", ansi); // Blue
+        Assert.Contains (expectedRed, ansi); // Red
+        Assert.Contains (expectedBlue, ansi); // Blue
     }
 
     [Fact]
@@ -223,40 +246,52 @@ public class ToAnsiTests : FakeDriverBase
         Assert.DoesNotContain ("\u001b[38;2;", ansi); // No RGB codes
     }
 
-    [Fact]
-    public void ToAnsi_Multiple_Attributes_Per_Line ()
+    [Theory]
+    [InlineData (true, "\u001b[31m", "\u001b[32m", "\u001b[34m", "\u001b[33m", "\u001b[35m", "\u001b[36m")]
+    [InlineData (false, "\u001b[38;2;255;0;0m", "\u001b[38;2;0;128;0m", "\u001b[38;2;0;0;255", "\u001b[38;2;255;255;0m", "\u001b[38;2;255;0;255m", "\u001b[38;2;0;255;255m")]
+    public void ToAnsi_Multiple_Attributes_Per_Line (
+        bool force16Colors,
+        string expectedRed,
+        string expectedGreen,
+        string expectedBlue,
+        string expectedYellow,
+        string expectedMagenta,
+        string expectedCyan
+    )
     {
         IDriver driver = CreateFakeDriver (50, 1);
+        driver.Force16Colors = force16Colors;
 
         // Create a line with many attribute changes
-        string[] colors = { "Red", "Green", "Blue", "Yellow", "Magenta", "Cyan" };
+        string [] colors = { "Red", "Green", "Blue", "Yellow", "Magenta", "Cyan" };
 
         foreach (string colorName in colors)
         {
             Color fg = colorName switch
-            {
-                "Red" => Color.Red,
-                "Green" => Color.Green,
-                "Blue" => Color.Blue,
-                "Yellow" => Color.Yellow,
-                "Magenta" => Color.Magenta,
-                "Cyan" => Color.Cyan,
-                _ => Color.White
-            };
-
-            driver.CurrentAttribute = new Attribute (fg, Color.Black);
+                       {
+                           "Red" => Color.Red,
+                           "Green" => Color.Green,
+                           "Blue" => Color.Blue,
+                           "Yellow" => Color.Yellow,
+                           "Magenta" => Color.Magenta,
+                           "Cyan" => Color.Cyan,
+                           _ => Color.White
+                       };
+
+            driver.CurrentAttribute = new (fg, Color.Black);
             driver.AddStr (colorName);
         }
 
         string ansi = driver.ToAnsi ();
 
+        Assert.True (driver.Force16Colors == force16Colors);
         // Should contain multiple color codes
-        Assert.Contains ("\u001b[31m", ansi); // Red
-        Assert.Contains ("\u001b[32m", ansi); // Green
-        Assert.Contains ("\u001b[34m", ansi); // Blue
-        Assert.Contains ("\u001b[33m", ansi); // Yellow
-        Assert.Contains ("\u001b[35m", ansi); // Magenta
-        Assert.Contains ("\u001b[36m", ansi); // Cyan
+        Assert.Contains (expectedRed, ansi); // Red
+        Assert.Contains (expectedGreen, ansi); // Green
+        Assert.Contains (expectedBlue, ansi); // Blue
+        Assert.Contains (expectedYellow, ansi); // Yellow
+        Assert.Contains (expectedMagenta, ansi); // Magenta
+        Assert.Contains (expectedCyan, ansi); // Cyan
     }
 
     [Fact]

+ 2 - 2
Tests/UnitTestsParallelizable/TestSetup.cs

@@ -24,8 +24,8 @@ public class GlobalTestSetup : IDisposable
         // Reset application state just in case a test changed something.
         // TODO: Add an Assert to ensure none of the state of Application changed.
         // TODO: Add an Assert to ensure none of the state of ConfigurationManager changed.
-        CheckDefaultState ();
         Application.ResetState (true);
+        CheckDefaultState ();
     }
 
     // IMPORTANT: Ensure this matches the code in Init_ResetState_Resets_Properties
@@ -43,7 +43,7 @@ public class GlobalTestSetup : IDisposable
         Assert.Null (Application.Mouse.MouseGrabView);
 
         // Don't check Application.ForceDriver
-        // Assert.Empty (Application.ForceDriver);
+        Assert.Empty (Application.ForceDriver);
         // Don't check Application.Force16Colors
         //Assert.False (Application.Force16Colors);
         Assert.Null (Application.Driver);

+ 2 - 1
Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs

@@ -1,11 +1,12 @@
 #nullable enable
 using System.Text;
 using UICatalog;
+using UnitTests;
 using Xunit.Abstractions;
 
 // Alias Console to MockConsole so we don't accidentally use Console
 
-namespace UnitTests.TextTests;
+namespace UnitTests_Parallelizable.TextTests;
 
 public class TextFormatterDrawTests (ITestOutputHelper output) : FakeDriverBase
 {

+ 1 - 1
Tests/UnitTestsParallelizable/Text/TextFormatterJustificationTests.cs

@@ -4,7 +4,7 @@ using Xunit.Abstractions;
 
 // Alias Console to MockConsole so we don't accidentally use Console
 
-namespace UnitTests.TextTests;
+namespace UnitTests_Parallelizable.TextTests;
 
 public class TextFormatterJustificationTests (ITestOutputHelper output) : FakeDriverBase
 {

+ 3 - 1
Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs

@@ -1,4 +1,6 @@
-namespace UnitTests_Parallelizable.LayoutTests;
+using UnitTests.Parallelizable;
+
+namespace UnitTests_Parallelizable.LayoutTests;
 
 public partial class DimAutoTests
 {

+ 0 - 1
Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.cs

@@ -1,5 +1,4 @@
 using System.Text;
-using UnitTests;
 using Xunit.Abstractions;
 using static Terminal.Gui.ViewBase.Dim;
 

+ 1 - 1
Tests/UnitTestsParallelizable/View/Layout/LayoutTests.cs

@@ -2,7 +2,7 @@
 
 namespace UnitTests_Parallelizable.LayoutTests;
 
-public class LayoutTests : GlobalTestSetup
+public class LayoutTests
 {
     #region Constructor Tests
 

+ 1 - 1
Tests/UnitTestsParallelizable/View/Layout/ViewLayoutEventTests.cs

@@ -3,7 +3,7 @@ using UnitTests.Parallelizable;
 
 namespace UnitTests_Parallelizable.ViewLayoutEventTests;
 
-public class ViewLayoutEventTests : GlobalTestSetup
+public class ViewLayoutEventTests
 {
     [Fact]
     public void View_WidthChanging_Event_Fires ()

+ 1 - 0
Tests/UnitTestsParallelizable/Views/ShortcutTests.cs

@@ -1,4 +1,5 @@
 using JetBrains.Annotations;
+using UnitTests.Parallelizable;
 
 namespace UnitTests_Parallelizable.ViewsTests;