From de3c02e9bfa458d6968be00f48b7ddf1af9d1d43 Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Sat, 5 Nov 2022 14:36:41 -0600 Subject: [PATCH] refactored internal Init() (now called InternnalInit()) to be more clear; updated docs and unit tests --- Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 18 ++-- Terminal.Gui/Core/Application.cs | 100 +++++++++++------- .../Scenarios/BackgroundWorkerCollection.cs | 4 - UICatalog/UICatalog.cs | 1 + UnitTests/ApplicationTests.cs | 69 ++++++++++-- 5 files changed, 132 insertions(+), 60 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index 9e77a775f..66eeed296 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -1451,16 +1451,20 @@ namespace Terminal.Gui { { TerminalResized = terminalResized; - var winSize = WinConsole.GetConsoleOutputWindow (out Point pos); - cols = winSize.Width; - rows = winSize.Height; + try { + var winSize = WinConsole.GetConsoleOutputWindow (out Point pos); + cols = winSize.Width; + rows = winSize.Height; - WindowsConsole.SmallRect.MakeEmpty (ref damageRegion); + WindowsConsole.SmallRect.MakeEmpty (ref damageRegion); - ResizeScreen (); - UpdateOffScreen (); + ResizeScreen (); + UpdateOffScreen (); - CreateColors (); + CreateColors (); + } catch (Win32Exception e) { + throw new InvalidOperationException ("The Windows Console output window is not available.", e); + } } public override void ResizeScreen () diff --git a/Terminal.Gui/Core/Application.cs b/Terminal.Gui/Core/Application.cs index f9954d0bf..1b6cb2489 100644 --- a/Terminal.Gui/Core/Application.cs +++ b/Terminal.Gui/Core/Application.cs @@ -329,29 +329,28 @@ namespace Terminal.Gui { /// into a single call. An applciation cam use /// without explicitly calling . /// - /// The to use. If not specified the default driver for the + /// + /// The to use. If not specified the default driver for the /// platform will be used (see , , and ). - /// Specifies the to use. - public static void Init (ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) => Init (() => Toplevel.Create (), driver, mainLoopDriver, resetState: true); + /// + /// Specifies the to use. + /// Must not be if is not . + /// + public static void Init (ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) => InternalInit (() => Toplevel.Create (), driver, mainLoopDriver); internal static bool _initialized = false; internal static int _mainThreadId = -1; - /// - /// Internal function for initializing a Terminal.Gui application with a factory object, - /// a , and . - /// - /// This is a low-level function; most applications will use as it is simpler. - /// - /// Specifies the factory funtion./> - /// The to use. If not specified the default driver for the - /// platform will be used (see , , and ). - /// Specifies the to use. - /// If (default) all state will be reset. - /// Set to to not reset the state (for when this function is called via - /// when - /// has not already been called. f - internal static void Init (Func topLevelFactory, ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null, bool resetState = true) + // INTERNAL function for initializing an app with a Toplevel factory object, driver, and mainloop. + // + // Called from: + // + // Init() - When the user wants to use the default Toplevel. calledViaRunT will be false, causing all state to be reset. + // Run() - When the user wants to use a custom Toplevel. calledViaRunT will be true, enabling Run() to be called without calling Init first. + // Unit Tests - To initialize the app with a custom Toplevel, using the FakeDriver. calledViaRunT will be false, causing all state to be reset. + // + // calledViaRunT: If false (default) all state will be reset. If true the state will not be reset. + internal static void InternalInit (Func topLevelFactory, ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null, bool calledViaRunT = false) { if (_initialized && driver == null) return; @@ -359,6 +358,7 @@ namespace Terminal.Gui { throw new InvalidOperationException ("Init has already been called and must be bracketed by Shutdown."); } + // Note in this case, we don't verify the type of the Toplevel created by new T(). // Used only for start debugging on Unix. //#if DEBUG // while (!System.Diagnostics.Debugger.IsAttached) { @@ -367,15 +367,18 @@ namespace Terminal.Gui { // System.Diagnostics.Debugger.Break (); //#endif - // Reset all class variables (Application is a singleton). - if (resetState) { + if (!calledViaRunT) { + // Reset all class variables (Application is a singleton). ResetState (); } - // This supports Unit Tests and the passing of a mock driver/loopdriver + // FakeDriver (for UnitTests) if (driver != null) { if (mainLoopDriver == null) { - throw new ArgumentNullException ("mainLoopDriver cannot be null if driver is provided."); + throw new ArgumentNullException ("InternalInit mainLoopDriver cannot be null if driver is provided."); + } + if (!(driver is FakeDriver)) { + throw new InvalidOperationException ("InternalInit can only be called with FakeDriver."); } Driver = driver; } @@ -395,7 +398,16 @@ namespace Terminal.Gui { } MainLoop = new MainLoop (mainLoopDriver); - Driver.Init (TerminalResized); + try { + Driver.Init (TerminalResized); + } catch (InvalidOperationException ex) { + // This is a case where the driver is unable to initialize the console. + // This can happen if the console is already in use by another process or + // if running in unit tests. + // In this case, we want to throw a more specific exception. + throw new InvalidOperationException ("Unable to initialize the console. This can happen if the console is already in use by another process or in unit tests.", ex); + } + SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop)); Top = topLevelFactory (); @@ -1250,8 +1262,7 @@ namespace Terminal.Gui { /// Runs the application by calling /// with a new instance of the specified -derived class. /// - /// If has not arleady been called, this function will - /// call . + /// Calling first is not needed as this function will initialze the /// /// /// must be called when the application is closing (typically after Run> has @@ -1263,27 +1274,36 @@ namespace Terminal.Gui { /// /// /// The to use. If not specified the default driver for the - /// platform will be used (see , , and ). + /// platform will be used (, , or ). + /// This parameteter must be if has already been called. + /// /// Specifies the to use. public static void Run (Func errorHandler = null, ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) where T : Toplevel, new() { - if (_initialized && Driver != null) { - var top = new T (); - var type = top.GetType ().BaseType; - while (type != typeof (Toplevel) && type != typeof (object)) { - type = type.BaseType; + if (_initialized) { + if (Driver != null) { + // Init() has been called and we have a driver, so just run the app. + var top = new T (); + var type = top.GetType ().BaseType; + while (type != typeof (Toplevel) && type != typeof (object)) { + type = type.BaseType; + } + if (type != typeof (Toplevel)) { + throw new ArgumentException ($"{top.GetType ().Name} must be derived from TopLevel"); + } + Run (top, errorHandler); + } else { + // This codepath should be impossible because Init(null, null) will select the platform default driver + throw new InvalidOperationException ("Init() completed without a driver being set (this should be impossible); Run() cannot be called."); } - if (type != typeof (Toplevel)) { - throw new ArgumentException ($"{top.GetType ().Name} must be derived from TopLevel"); - } - // Run() will eventually cause Application.Top to be set, via Begin() and SetCurrentAsTop() - Run (top, errorHandler); } else { - if (!_initialized && driver == null) { - throw new ArgumentException ("Init has not been called; a valid driver and mainloop must be provided"); + // Init() has NOT been called. + if (driver != null) { + // Caller has provided a driver so call Init with it (but set calledViaRunT to true so we don't reset Application state). + InternalInit (() => new T (), driver, mainLoopDriver, calledViaRunT: true); + } else { + throw new ArgumentException ("A Driver must be specified when calling Run() when Init() has not been called."); } - // Note in this case, we don't verify the type of the Toplevel created by new T(). - Init (() => new T (), Driver == null ? driver : Driver, Driver == null ? mainLoopDriver : null, resetState: false); Run (Top, errorHandler); } } diff --git a/UICatalog/Scenarios/BackgroundWorkerCollection.cs b/UICatalog/Scenarios/BackgroundWorkerCollection.cs index 13ebcc1a4..2e566ebe5 100644 --- a/UICatalog/Scenarios/BackgroundWorkerCollection.cs +++ b/UICatalog/Scenarios/BackgroundWorkerCollection.cs @@ -12,10 +12,6 @@ namespace UICatalog.Scenarios { [ScenarioCategory ("Dialogs")] [ScenarioCategory ("Controls")] public class BackgroundWorkerCollection : Scenario { - public override void Init (ColorScheme colorScheme) - { - // Do not call Init as Application.Run will do it - } public override void Run () { diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index d4a7594d5..979b9eed7 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -117,6 +117,7 @@ namespace UICatalog { // Run UI Catalog UI. When it exits, if _selectedScenario is != null then // a Scenario was selected. Otherwise, the user wants to exit UI Catalog. + Application.Init (); Application.Run (); Application.Shutdown (); diff --git a/UnitTests/ApplicationTests.cs b/UnitTests/ApplicationTests.cs index ef1fd01e6..911154a66 100644 --- a/UnitTests/ApplicationTests.cs +++ b/UnitTests/ApplicationTests.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Linq; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -100,7 +101,7 @@ namespace Terminal.Gui.Core { Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); Toplevel topLevel = null; - Assert.Throws (() => Application.Init (() => topLevel = new TestToplevel (), new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)))); + Assert.Throws (() => Application.InternalInit (() => topLevel = new TestToplevel (), new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)))); Shutdown (); Assert.Null (Application.Top); @@ -109,7 +110,7 @@ namespace Terminal.Gui.Core { // Now try the other way topLevel = null; - Application.Init (() => topLevel = new TestToplevel (), new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + Application.InternalInit (() => topLevel = new TestToplevel (), new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); Assert.Throws (() => Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)))); Shutdown (); @@ -118,7 +119,7 @@ namespace Terminal.Gui.Core { Assert.Null (Application.MainLoop); Assert.Null (Application.Driver); } - + class TestToplevel : Toplevel { public TestToplevel () @@ -131,7 +132,7 @@ namespace Terminal.Gui.Core { public void Init_Begin_End_Cleans_Up () { Init (); - + // Begin will cause Run() to be called, which will call Begin(). Thus will block the tests // if we don't stop Application.Iteration = () => { @@ -145,7 +146,7 @@ namespace Terminal.Gui.Core { }; Application.NotifyNewRunState += NewRunStateFn; - Toplevel topLevel = new Toplevel(); + Toplevel topLevel = new Toplevel (); var rs = Application.Begin (topLevel); Assert.NotNull (rs); Assert.NotNull (runstate); @@ -181,7 +182,7 @@ namespace Terminal.Gui.Core { // NOTE: Run, when called after Init has been called behaves differently than // when called if Init has not been called. Toplevel topLevel = null; - Application.Init (() => topLevel = new TestToplevel (), new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + Application.InternalInit (() => topLevel = new TestToplevel (), new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); Application.RunState runstate = null; Action NewRunStateFn = (rs) => { @@ -272,9 +273,56 @@ namespace Terminal.Gui.Core { Application.Iteration = () => { Application.RequestStop (); }; - + // Init has been called and we're passing no driver to Run. This is ok. - Application.Run (errorHandler: null); + Application.Run (); + + Shutdown (); + + Assert.Null (Application.Top); + Assert.Null (Application.MainLoop); + Assert.Null (Application.Driver); + } + + [Fact] + public void Run_T_After_InitNullDriver_with_TestTopLevel_Throws () + { + var p = Environment.OSVersion.Platform; + if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) { + Assert.Throws (() => Application.Init (null, null)); + } else { + Application.Init (null, null); + Assert.Equal (typeof (CursesDriver), Application.Driver.GetType ()); + Application.Shutdown (); + } + + Application.Iteration = () => { + Application.RequestStop (); + }; + + // Init has been called without selecting a driver and we're passing no driver to Run. Bad + Assert.Throws (() => Application.Run ()); + + Shutdown (); + + Assert.Null (Application.Top); + Assert.Null (Application.MainLoop); + Assert.Null (Application.Driver); + } + + [Fact] + public void Run_T_Init_Driver_Cleared_with_TestTopLevel_Throws () + { + Init (); + + Application.Driver = null; + + Application.Iteration = () => { + Application.RequestStop (); + }; + + // Init has been called, but Driver has been set to null. Bad. + Assert.Throws (() => Application.Run ()); Shutdown (); @@ -379,6 +427,9 @@ namespace Terminal.Gui.Core { Application.Shutdown (); Assert.Equal (3, count); } + + // TODO: Add tests for Run that test errorHandler + #endregion #region ShutdownTests @@ -407,7 +458,7 @@ namespace Terminal.Gui.Core { Assert.Null (SynchronizationContext.Current); } #endregion - + [Fact] [AutoInitShutdown] public void SetCurrentAsTop_Run_A_Not_Modal_Toplevel_Make_It_The_Current_Application_Top ()