diff --git a/Examples/UICatalog/UICatalog.cs b/Examples/UICatalog/UICatalog.cs index 4460572592..8b8eeca71f 100644 --- a/Examples/UICatalog/UICatalog.cs +++ b/Examples/UICatalog/UICatalog.cs @@ -55,7 +55,9 @@ namespace UICatalog; /// 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 @@ private static int Main (string [] args) UICatalogMain (Options); + Debug.Assert (Application.ForceDriver == string.Empty); + return 0; } @@ -255,7 +259,9 @@ private static Scenario RunUICatalogTopLevel () Application.Init (driverName: _forceDriver); - var top = Application.Run (); + _uiCatalogDriver = Application.Driver!.GetName (); + + Toplevel top = Application.Run (); top.Dispose (); Application.Shutdown (); VerifyObjectsWereDisposed (); @@ -421,6 +427,8 @@ private static void UICatalogMain (UICatalogCommandLineOptions options) Application.InitializedChanged += ApplicationOnInitializedChanged; #endif + Application.ForceDriver = _forceDriver; + scenario.Main (); scenario.Dispose (); @@ -439,6 +447,8 @@ void ApplicationOnInitializedChanged (object? sender, EventArgs e) if (e.Value) { sw.Start (); + _scenarioDriver = Application.Driver!.GetName (); + Debug.Assert (_scenarioDriver == _uiCatalogDriver); } else { diff --git a/Terminal.Gui/App/Application.Driver.cs b/Terminal.Gui/App/Application.Driver.cs index 741134d390..635ff854b9 100644 --- a/Terminal.Gui/App/Application.Driver.cs +++ b/Terminal.Gui/App/Application.Driver.cs @@ -28,7 +28,21 @@ public static bool Force16Colors 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; + } } /// diff --git a/Terminal.Gui/App/ApplicationImpl.Driver.cs b/Terminal.Gui/App/ApplicationImpl.Driver.cs index 837024b83c..edb6adcbd2 100644 --- a/Terminal.Gui/App/ApplicationImpl.Driver.cs +++ b/Terminal.Gui/App/ApplicationImpl.Driver.cs @@ -34,7 +34,7 @@ private void CreateDriver (string? driverName) bool factoryIsFake = _componentFactory is IComponentFactory; // 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; diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs index 73465bf8ac..09f06588d7 100644 --- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs +++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs @@ -33,8 +33,8 @@ public void Init (string? driverName = null) _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 void Init (string? driverName = null) _keyboard.PrevTabGroupKey = existingPrevTabGroupKey; } - CreateDriver (driverName ?? _driverName); + CreateDriver (_driverName); Screen = Driver!.Screen; Initialized = true; @@ -145,9 +145,13 @@ private static void AssertNoEventSubscribers (string eventName, Delegate? eventD } #endif + private bool _isResetingState; + /// 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 void ResetState (bool ignoreDisposed = false) // === 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 void ResetState (bool ignoreDisposed = false) // (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; } /// diff --git a/Terminal.Gui/App/Mouse/MouseImpl.cs b/Terminal.Gui/App/Mouse/MouseImpl.cs index 29116db0ad..23a3aed611 100644 --- a/Terminal.Gui/App/Mouse/MouseImpl.cs +++ b/Terminal.Gui/App/Mouse/MouseImpl.cs @@ -254,6 +254,7 @@ public void ResetState () // 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 diff --git a/Tests/UnitTests/FakeDriverBase.cs b/Tests/UnitTests/FakeDriverBase.cs index f692cd914d..657c5a5880 100644 --- a/Tests/UnitTests/FakeDriverBase.cs +++ b/Tests/UnitTests/FakeDriverBase.cs @@ -4,7 +4,7 @@ namespace UnitTests; /// Enables tests to create a FakeDriver for testing purposes. /// [Collection ("Global Test Setup")] -public abstract class FakeDriverBase +public abstract class FakeDriverBase : IDisposable { /// /// Creates a new FakeDriver instance with the specified buffer size. @@ -19,14 +19,20 @@ protected static IDriver CreateFakeDriver (int width = 80, int height = 25) 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; } + + /// + public void Dispose () + { + Application.ResetState (true); + } } diff --git a/Tests/UnitTests/TestsAllViews.cs b/Tests/UnitTests/TestsAllViews.cs index 619334087c..7428e26c08 100644 --- a/Tests/UnitTests/TestsAllViews.cs +++ b/Tests/UnitTests/TestsAllViews.cs @@ -1,6 +1,5 @@ #nullable enable using System.Reflection; -using System.Drawing; namespace UnitTests; diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationForceDriverTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationForceDriverTests.cs new file mode 100644 index 0000000000..a3136cc112 --- /dev/null +++ b/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 (() => Application.ForceDriver = "dotnet"); + + Application.ForceDriver = "fake"; + Assert.Equal ("fake", Application.ForceDriver); + } +} diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs index 914d676eec..3ea1d796e2 100644 --- a/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs +++ b/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using Moq; using Terminal.Gui.App; diff --git a/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs b/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs index e17c28cdf7..c8b55d191d 100644 --- a/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs @@ -40,10 +40,13 @@ public void ToAnsi_Simple_Text () 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 void ToAnsi_With_Colors () 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;;;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 void ToAnsi_With_Unicode_Characters () 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 void ToAnsi_Attribute_Changes_Within_Line () 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 void ToAnsi_Force16Colors () 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] diff --git a/Tests/UnitTestsParallelizable/TestSetup.cs b/Tests/UnitTestsParallelizable/TestSetup.cs index a8d37578ee..178a0d4de8 100644 --- a/Tests/UnitTestsParallelizable/TestSetup.cs +++ b/Tests/UnitTestsParallelizable/TestSetup.cs @@ -24,8 +24,8 @@ public void Dispose () // 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 @@ private void CheckDefaultState () 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); diff --git a/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs b/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs index 17ac27ab57..da433ffc01 100644 --- a/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs +++ b/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 { diff --git a/Tests/UnitTestsParallelizable/Text/TextFormatterJustificationTests.cs b/Tests/UnitTestsParallelizable/Text/TextFormatterJustificationTests.cs index 8990e1698b..e01f995ae9 100644 --- a/Tests/UnitTestsParallelizable/Text/TextFormatterJustificationTests.cs +++ b/Tests/UnitTestsParallelizable/Text/TextFormatterJustificationTests.cs @@ -4,7 +4,7 @@ // Alias Console to MockConsole so we don't accidentally use Console -namespace UnitTests.TextTests; +namespace UnitTests_Parallelizable.TextTests; public class TextFormatterJustificationTests (ITestOutputHelper output) : FakeDriverBase { diff --git a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs b/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs index 61aadaea74..c5a571c10b 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs +++ b/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 { diff --git a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.cs b/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.cs index 4c1da89b56..3c785ae36c 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.cs +++ b/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; diff --git a/Tests/UnitTestsParallelizable/View/Layout/LayoutTests.cs b/Tests/UnitTestsParallelizable/View/Layout/LayoutTests.cs index 1c45576f21..eccd03179f 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/LayoutTests.cs +++ b/Tests/UnitTestsParallelizable/View/Layout/LayoutTests.cs @@ -2,7 +2,7 @@ namespace UnitTests_Parallelizable.LayoutTests; -public class LayoutTests : GlobalTestSetup +public class LayoutTests { #region Constructor Tests diff --git a/Tests/UnitTestsParallelizable/View/Layout/ViewLayoutEventTests.cs b/Tests/UnitTestsParallelizable/View/Layout/ViewLayoutEventTests.cs index bc368764fb..c606abbaab 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/ViewLayoutEventTests.cs +++ b/Tests/UnitTestsParallelizable/View/Layout/ViewLayoutEventTests.cs @@ -3,7 +3,7 @@ namespace UnitTests_Parallelizable.ViewLayoutEventTests; -public class ViewLayoutEventTests : GlobalTestSetup +public class ViewLayoutEventTests { [Fact] public void View_WidthChanging_Event_Fires () diff --git a/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs b/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs index fc740c63a7..7ca25dbadf 100644 --- a/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs @@ -1,4 +1,5 @@ using JetBrains.Annotations; +using UnitTests.Parallelizable; namespace UnitTests_Parallelizable.ViewsTests;