Files
Terminal.Gui/Tests/UnitTests/Application/ApplicationTests.cs
Copilot e6a0ec64ca Fixes #4361 - Consolidate FakeDriver into library and refactor driver architecture (#4362)
* Initial plan

* Add ScreenChanged event, SetScreenSize method, and fix FakeDriver buffer initialization

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Add comprehensive tests for ScreenChanged event and buffer integrity

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Replace obsolete SizeChanged usage with ScreenChanged in core and tests

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Refactor terminal size event handling

Replaced `ScreenChanged` with `SizeChanged` across the codebase to standardize naming and improve clarity. Updated event handling logic, including subscriptions, unsubscriptions, and raising methods. Removed deprecated `ScreenChanged` event and backward compatibility code.

Refactored driver initialization to handle legacy `IConsoleDriver` types separately. Updated tests and mock implementations to align with the new `SizeChanged` event. Improved documentation and comments to reflect these changes.

These updates enhance maintainability, consistency, and modernize the architecture.

* Refactor & Code Cleanup: Replace IWindowSizeMonitor with IConsoleSizeMonitor

Renamed `IWindowSizeMonitor` to `IConsoleSizeMonitor` across the codebase, updating all references, method signatures, and event handlers. Replaced the `WindowSizeMonitor` class with the new `ConsoleSizeMonitor` implementation, which includes improved terminal size change handling via the `Poll` method.

Enabled nullable reference types in several files to enhance code safety. Updated test cases to reflect the new `IConsoleSizeMonitor` interface. Removed redundant code, simplified null checks, and corrected minor typos in comments. Streamlined the codebase by removing the obsolete `WindowSizeMonitor` class and its interface.

* Code cleanup - Refactor and enhance ShadowView and FakeDriverTests

Updated ShadowView.cs to use null-conditional operators and added null checks for safer access to `ScreenContents`. Refined XML documentation in View.Layout.cs for clarity and consistency.

Refactored FakeDriverTests.cs to leverage modern C# features, including shorthand object instantiation, inline lambdas, and tuple-like syntax for `Size` and `Rectangle`. Removed redundant tests and improved test readability and reliability.

Enhanced error handling with null checks and ensured backward compatibility for deprecated events. Improved test coverage for resizing, clipboard operations, and invalid coordinates. Verified buffer integrity and screen updates after resizing.

General improvements include replacing explicit type declarations with `var`, removing unused imports, and aligning code formatting for better readability.

Refactor and improve code quality and test coverage

Updated `ShadowView` for null safety using null-conditional operators. Simplified object initializations and modernized syntax across the codebase, including shorthand initializations and inline lambdas. Enhanced event handling logic and ensured compatibility with obsolete members.

Refactored `FakeDriverTests` by removing redundant code, standardizing formatting, and improving test setup. Suppressed obsolete warnings where necessary. Improved XML documentation in `View.Layout.cs` for clarity and removed outdated references.

Performed general cleanup, including removing unused namespaces, redundant comments, and ensuring consistent formatting. These changes enhance readability, maintainability, and runtime safety.

* Code cleanup

Refactor TimedEventsTests for readability and consistency

Improved code readability and maintainability:
- Enabled nullable reference types with `#nullable enable`.
- Removed unused `using System.Diagnostics;`.
- Updated namespace to `UnitTests.ApplicationTests`.
- Replaced `Terminal.Gui.App.TimedEvents` with `TimedEvents`.
- Reformatted XML documentation comments for alignment.
- Used `var` and target-typed new expressions for consistency.
- Reformatted `Parallel.For` loops and lambdas for clarity.
- Added `Thread.Sleep(10)` to prevent excessive CPU usage in tests.
- Improved assertions and event handler formatting in tests.

Aligned with modern C# coding practices.

* Code Cleanup - No more driver warnings.

Refactor codebase and introduce FakeClipboard

- Adjusted `.editorconfig` to change severity levels for CS0612, CS0618, and CS0672 diagnostics.
- Replaced `FakeDriver.FakeClipboard` with a new `FakeClipboard` class for testing purposes, supporting exception handling and clipboard data manipulation.
- Removed redundant methods (`MakeColor`, `MapKey`) and unused classes (`MockConsoleDriver`) to streamline the codebase.
- Refactored `ConsoleDriverFacade` and `FakeDriver` to simplify logic and improve maintainability.
- Updated tests to use `CreateFakeDriver` and removed or commented out obsolete tests.
- Reformatted and cleaned up code for readability across multiple files.

* Refactor FakeDriver - Code Cleanup

Standardized console size management by replacing `WindowSizeMonitor` with `ConsoleSizeMonitor` across the codebase. Updated methods `GetWindowSize` and `SetWindowSize` to `GetSize` and `SetSize` for consistency.

Refactored `FakeDriver` to use `SetScreenSize` and removed redundant methods. Simplified driver initialization by removing legacy `InternalInit` logic.

Standardized ANSI escape sequences by replacing `CSI_ReportTerminalSizeInChars` with `CSI_ReportWindowSizeInChars`.

Updated test cases to align with the new `ConsoleSizeMonitor` and `SetScreenSize` methods. Removed obsolete test utilities like `FakeSizeMonitor` and `FakeWindowSizeMonitor`.

Performed general code cleanup, including removing unused classes, redundant code, and improving formatting. Fixed resizing logic issues and improved exception handling in driver methods.

* Update Terminal.Gui/Drivers/OutputBuffer.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Terminal.Gui/Drivers/MouseButtonStateEx.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Terminal.Gui/App/MainLoop/IApplicationMainLoop.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Tests/UnitTests/Views/ToplevelTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Terminal.Gui/ViewBase/View.Layout.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Moved all Drawing tests to Paralleizable - proving Fakedriver works

Enhanced `Ruler` and `Thickness` classes by adding an optional `IConsoleDriver? driver` parameter to decouple rendering from the default `Application.Driver`, improving flexibility and testability. Updated `DriverAssert` to support nullable drivers and added defensive checks.

Refactored and expanded test cases for `Ruler`, `Thickness`, `LineCanvas`, and `StraightLineExtensions` to ensure comprehensive coverage, including zero-length intersections, line rendering, and exclusion logic. Migrated rendering-dependent tests to a parallelizable namespace.

Removed redundant tests, improved naming conventions, and updated documentation for better maintainability and clarity.

* Fixed Run<T> startup hang.

Refactor: Simplify driver logic and update SetSize methods

Removed FakeDriver safeguard in unit tests to simplify
CreateDriver logic. Updated SetSize methods in NetOutput,
UnixOutput, and WindowsOutput to do nothing instead of
throwing NotImplementedException. Modified SizeChanged
event in ConsoleDriverFacade to call SetScreenSize directly.
Commented out unnecessary debug validation in DimAuto.
These changes improve maintainability and reduce complexity.

* Fixed intermittent unit test bug.

Refactored `_cachedRunStateToplevel` to `CachedRunStateToplevel` as an internal static property, delegating its management to `ApplicationImpl` for improved encapsulation. Updated all references to use the new property and centralized its handling in `ApplicationImpl`.

Removed the `MouseGrabHandler` property from `ApplicationImpl` and simplified driver-related assignments by replacing `Application.ForceDriver` and `Application.Screen` with direct references.

Reset `CachedRunStateToplevel` during cleanup to ensure proper state management. Updated the `Invoke` method to use `Top` directly, aligning with the refactored design. Improved debug assertions and performed general cleanup to enhance code readability and maintainability.

* Fixed intermittent bug an massive code cleanup of warnings.

Refactor and enhance codebase for maintainability

- Applied null-conditional operator (`!`) to improve null-safety.
- Refactored tests for clarity, added new cases, and removed redundancies.
- Introduced `FakeApplicationFactory` and `FakeSizeMonitor` for testing.
- Removed unused code, including legacy `DecodeEscSeq` logic.
- Reformatted code for readability and consistency.
- Updated assertions for more accurate validation in tests.
- Fixed potential null reference issues across multiple files.
- Improved event handling with proper null checks.
- Enhanced documentation for new classes and methods.
- Modernized code with C# features like `record struct` and `nullable enable`.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tig <585482+tig@users.noreply.github.com>
Co-authored-by: Tig <tig@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-27 23:34:14 -06:00

1202 lines
37 KiB
C#

using System.Diagnostics;
using Xunit.Abstractions;
using static Terminal.Gui.Configuration.ConfigurationManager;
// Alias Console to MockConsole so we don't accidentally use Console
namespace UnitTests.ApplicationTests;
public class ApplicationTests
{
public ApplicationTests (ITestOutputHelper output)
{
_output = output;
ConsoleDriver.RunningUnitTests = true;
#if DEBUG_IDISPOSABLE
View.EnableDebugIDisposableAsserts = true;
View.Instances.Clear ();
RunState.Instances.Clear ();
#endif
}
private readonly ITestOutputHelper _output;
private object _timeoutLock;
[Fact]
public void AddTimeout_Fires ()
{
Assert.Null (_timeoutLock);
_timeoutLock = new ();
uint timeoutTime = 250;
var initialized = false;
var iteration = 0;
var shutdown = false;
object timeout = null;
var timeoutCount = 0;
Application.InitializedChanged += OnApplicationOnInitializedChanged;
var a = new AutoInitShutdownAttribute ();
a.Before (null);
Assert.True (initialized);
Assert.False (shutdown);
_output.WriteLine ("Application.Run<Toplevel> ().Dispose ()..");
Application.Run<Toplevel> ().Dispose ();
_output.WriteLine ("Back from Application.Run<Toplevel> ().Dispose ()");
Assert.True (initialized);
Assert.False (shutdown);
Assert.Equal (1, timeoutCount);
Application.Shutdown ();
Application.InitializedChanged -= OnApplicationOnInitializedChanged;
lock (_timeoutLock)
{
if (timeout is { })
{
Application.RemoveTimeout (timeout);
timeout = null;
}
}
Assert.True (initialized);
Assert.True (shutdown);
#if DEBUG_IDISPOSABLE
Assert.Empty (View.Instances);
#endif
lock (_timeoutLock)
{
_timeoutLock = null;
}
a.After (null);
return;
void OnApplicationOnInitializedChanged (object s, EventArgs<bool> a)
{
if (a.Value)
{
Application.Iteration += OnApplicationOnIteration;
initialized = true;
lock (_timeoutLock)
{
_output.WriteLine ($"Setting timeout for {timeoutTime}ms");
timeout = Application.AddTimeout (TimeSpan.FromMilliseconds (timeoutTime), TimeoutCallback);
}
}
else
{
Application.Iteration -= OnApplicationOnIteration;
shutdown = true;
}
}
bool TimeoutCallback ()
{
lock (_timeoutLock)
{
_output.WriteLine ($"TimeoutCallback. Count: {++timeoutCount}. Application Iteration: {iteration}");
if (timeout is { })
{
_output.WriteLine (" Nulling timeout.");
timeout = null;
}
}
// False means "don't re-do timer and remove it"
return false;
}
void OnApplicationOnIteration (object s, IterationEventArgs a)
{
lock (_timeoutLock)
{
if (timeoutCount > 0)
{
_output.WriteLine ($"Iteration #{iteration} - Timeout fired. Calling Application.RequestStop.");
Application.RequestStop ();
return;
}
}
iteration++;
// Simulate a delay
Thread.Sleep ((int)timeoutTime / 10);
// Worst case scenario - something went wrong
if (Application.Initialized && iteration > 25)
{
_output.WriteLine ($"Too many iterations ({iteration}): Calling Application.RequestStop.");
Application.RequestStop ();
}
}
}
[Fact]
[AutoInitShutdown]
public void Begin_Null_Toplevel_Throws ()
{
// Test null Toplevel
Assert.Throws<ArgumentNullException> (() => Application.Begin (null));
Application.Shutdown ();
Assert.Null (Application.Top);
Assert.Null (Application.Driver);
}
[Fact]
[AutoInitShutdown (verifyShutdown: true)]
public void Begin_Sets_Application_Top_To_Console_Size ()
{
Assert.Null (Application.Top);
Application.Driver!.SetScreenSize (80, 25);
Toplevel top = new ();
Application.Begin (top);
Assert.Equal (new (0, 0, 80, 25), Application.Top!.Frame);
Application.Driver!.SetScreenSize (5, 5);
Assert.Equal (new (0, 0, 5, 5), Application.Top!.Frame);
top.Dispose ();
}
[Fact]
[AutoInitShutdown]
public void End_And_Shutdown_Should_Not_Dispose_ApplicationTop ()
{
Assert.Null (Application.Top);
RunState rs = Application.Begin (new ());
Application.Top!.Title = "End_And_Shutdown_Should_Not_Dispose_ApplicationTop";
Assert.Equal (rs.Toplevel, Application.Top);
Application.End (rs);
#if DEBUG_IDISPOSABLE
Assert.True (rs.WasDisposed);
Assert.False (Application.Top!.WasDisposed); // Is true because the rs.Toplevel is the same as Application.Top
#endif
Assert.Null (rs.Toplevel);
Toplevel top = Application.Top;
#if DEBUG_IDISPOSABLE
Exception exception = Record.Exception (Application.Shutdown);
Assert.NotNull (exception);
Assert.False (top.WasDisposed);
top.Dispose ();
Assert.True (top.WasDisposed);
#endif
Application.Shutdown ();
Assert.Null (Application.Top);
}
[Fact]
[AutoInitShutdown]
public void Init_Begin_End_Cleans_Up ()
{
// Start stopwatch
var stopwatch = new Stopwatch ();
stopwatch.Start ();
// Begin will cause Run() to be called, which will call Begin(). Thus will block the tests
// if we don't stop
Application.Iteration += (s, a) => { Application.RequestStop (); };
RunState runstate = null;
EventHandler<RunStateEventArgs> newRunStateFn = (s, e) =>
{
Assert.NotNull (e.State);
runstate = e.State;
};
Application.NotifyNewRunState += newRunStateFn;
var topLevel = new Toplevel ();
RunState rs = Application.Begin (topLevel);
Assert.NotNull (rs);
Assert.NotNull (runstate);
Assert.Equal (rs, runstate);
Assert.Equal (topLevel, Application.Top);
Application.NotifyNewRunState -= newRunStateFn;
Application.End (runstate);
Assert.NotNull (Application.Top);
Assert.NotNull (Application.Driver);
topLevel.Dispose ();
Application.Shutdown ();
Assert.Null (Application.Top);
Assert.Null (Application.Driver);
// Stop stopwatch
stopwatch.Stop ();
_output.WriteLine ($"Load took {stopwatch.ElapsedMilliseconds} ms");
}
// Legacy driver test - all InlineData commented out
//[Theory]
////[InlineData (typeof (DotNetDriver))]
////[InlineData (typeof (ANSIDriver))]
////[InlineData (typeof (WindowsDriver))]
////[InlineData (typeof (UnixDriver))]
//public void Init_DriverName_Should_Pick_Correct_Driver (Type driverType)
//{
// var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
// Application.Init (driverName: driverType.Name);
// Assert.NotNull (Application.Driver);
// Assert.NotEqual (driver, Application.Driver);
// Assert.Equal (driverType, Application.Driver?.GetType ());
// Application.Shutdown ();
//}
[Fact]
public void Init_Null_Driver_Should_Pick_A_Driver ()
{
Application.Init ();
Assert.NotNull (Application.Driver);
Application.Shutdown ();
}
[Theory]
[InlineData (typeof (FakeDriver))]
//[InlineData (typeof (DotNetDriver))]
//[InlineData (typeof (WindowsDriver))]
//[InlineData (typeof (UnixDriver))]
public void Init_ResetState_Resets_Properties (Type driverType)
{
ThrowOnJsonErrors = true;
// For all the fields/properties of Application, check that they are reset to their default values
// Set some values
Application.Init (driverName: driverType.Name);
// Application.IsInitialized = true;
// Reset
Application.ResetState ();
void CheckReset ()
{
// Check that all fields and properties are set to their default values
// Public Properties
Assert.Null (Application.Top);
Assert.Null (Application.Mouse.MouseGrabView);
// Don't check Application.ForceDriver
// Assert.Empty (Application.ForceDriver);
// Don't check Application.Force16Colors
//Assert.False (Application.Force16Colors);
Assert.Null (Application.Driver);
Assert.False (Application.EndAfterFirstIteration);
// Commented out because if CM changed the defaults, those changes should
// persist across Inits.
//Assert.Equal (Key.Tab.WithShift, Application.PrevTabKey);
//Assert.Equal (Key.Tab, Application.NextTabKey);
//Assert.Equal (Key.F6.WithShift, Application.PrevTabGroupKey);
//Assert.Equal (Key.F6, Application.NextTabGroupKey);
//Assert.Equal (Key.Esc, Application.QuitKey);
// Internal properties
Assert.False (Application.Initialized);
Assert.Equal (Application.GetSupportedCultures (), Application.SupportedCultures);
Assert.Equal (Application.GetAvailableCulturesFromEmbeddedResources (), Application.SupportedCultures);
Assert.False (Application._forceFakeConsole);
Assert.Equal (-1, Application.MainThreadId);
Assert.Empty (Application.TopLevels);
Assert.Empty (Application.CachedViewsUnderMouse);
// Mouse
// Do not reset _lastMousePosition
//Assert.Null (Application._lastMousePosition);
// Navigation
Assert.Null (Application.Navigation);
// Popover
Assert.Null (Application.Popover);
// Events - Can't check
//Assert.Null (Application.NotifyNewRunState);
//Assert.Null (Application.NotifyNewRunState);
//Assert.Null (Application.Iteration);
//Assert.Null (Application.SizeChanging);
//Assert.Null (Application.GrabbedMouse);
//Assert.Null (Application.UnGrabbingMouse);
//Assert.Null (Application.GrabbedMouse);
//Assert.Null (Application.UnGrabbedMouse);
//Assert.Null (Application.MouseEvent);
//Assert.Null (Application.KeyDown);
//Assert.Null (Application.KeyUp);
}
CheckReset ();
// Set the values that can be set
Application.Initialized = true;
Application._forceFakeConsole = true;
Application.MainThreadId = 1;
//Application._topLevels = new List<Toplevel> ();
Application.CachedViewsUnderMouse.Clear ();
//Application.SupportedCultures = new List<CultureInfo> ();
Application.Force16Colors = true;
//Application.ForceDriver = "driver";
Application.EndAfterFirstIteration = true;
Application.PrevTabGroupKey = Key.A;
Application.NextTabGroupKey = Key.B;
Application.QuitKey = Key.C;
Application.KeyBindings.Add (Key.D, Command.Cancel);
Application.CachedViewsUnderMouse.Clear ();
//Application.WantContinuousButtonPressedView = new View ();
// Mouse
Application.LastMousePosition = new Point (1, 1);
Application.Navigation = new ();
Application.ResetState ();
CheckReset ();
ThrowOnJsonErrors = false;
}
[Fact]
public void Init_Shutdown_Cleans_Up ()
{
// Verify initial state is per spec
//Pre_Init_State ();
Application.Init (new FakeDriver ());
// Verify post-Init state is correct
//Post_Init_State ();
Application.Shutdown ();
// Verify state is back to initial
//Pre_Init_State ();
#if DEBUG_IDISPOSABLE
// Validate there are no outstanding Responder-based instances
// after a scenario was selected to run. This proves the main UI Catalog
// 'app' closed cleanly.
Assert.Empty (View.Instances);
#endif
}
[Fact]
public void Shutdown_Alone_Does_Nothing () { Application.Shutdown (); }
[Theory]
[InlineData (typeof (FakeDriver))]
//[InlineData (typeof (DotNetDriver))]
//[InlineData (typeof (WindowsDriver))]
//[InlineData (typeof (UnixDriver))]
public void Init_Shutdown_Fire_InitializedChanged (Type driverType)
{
var initialized = false;
var shutdown = false;
Application.InitializedChanged += OnApplicationOnInitializedChanged;
Application.Init (driverName: driverType.Name);
Assert.True (initialized);
Assert.False (shutdown);
Application.Shutdown ();
Assert.True (initialized);
Assert.True (shutdown);
Application.InitializedChanged -= OnApplicationOnInitializedChanged;
return;
void OnApplicationOnInitializedChanged (object s, EventArgs<bool> a)
{
if (a.Value)
{
initialized = true;
}
else
{
shutdown = true;
}
}
}
[Fact]
[AutoInitShutdown]
public void Init_Unbalanced_Throws ()
{
Assert.Throws<InvalidOperationException> (() =>
Application.Init (null, "fake")
);
Application.Shutdown ();
Assert.Null (Application.Top);
Assert.Null (Application.Driver);
}
[Fact]
[AutoInitShutdown]
public void Init_Unbalanced_Throws2 ()
{
// Now try the other way
Assert.Throws<InvalidOperationException> (() => Application.Init (new FakeDriver ()));
Application.Shutdown ();
Assert.Null (Application.Top);
Assert.Null (Application.Driver);
}
[Fact]
public void Init_WithoutTopLevelFactory_Begin_End_Cleans_Up ()
{
// Begin will cause Run() to be called, which will call Begin(). Thus will block the tests
// if we don't stop
Application.Iteration += (s, a) => { Application.RequestStop (); };
// NOTE: Run<T>, when called after Init has been called behaves differently than
// when called if Init has not been called.
Toplevel topLevel = new ();
Application.Init (null, "fake");
RunState runstate = null;
EventHandler<RunStateEventArgs> newRunStateFn = (s, e) =>
{
Assert.NotNull (e.State);
runstate = e.State;
};
Application.NotifyNewRunState += newRunStateFn;
RunState rs = Application.Begin (topLevel);
Assert.NotNull (rs);
Assert.NotNull (runstate);
Assert.Equal (rs, runstate);
Assert.Equal (topLevel, Application.Top);
Application.NotifyNewRunState -= newRunStateFn;
Application.End (runstate);
Assert.NotNull (Application.Top);
Assert.NotNull (Application.Driver);
topLevel.Dispose ();
Application.Shutdown ();
Assert.Null (Application.Top);
Assert.Null (Application.Driver);
}
[Fact (Skip = "FakeDriver is not allowed, use AutoInitShutdown attribute instead")]
public void Init_NoParam_ForceDriver_Works ()
{
Application.ForceDriver = "Fake";
Application.Init ();
//Assert.IsType<FakeConsoleInput>(Application.Drive);
//Assert.IsType<FakeDriver> (Application.Driver);
Application.ResetState ();
}
[Fact]
public void Init_KeyBindings_Are_Not_Reset ()
{
Debug.Assert (!IsEnabled);
try
{
// arrange
ThrowOnJsonErrors = true;
Application.QuitKey = Key.Q;
Assert.Equal (Key.Q, Application.QuitKey);
Application.Init (new FakeDriver ());
Assert.Equal (Key.Q, Application.QuitKey);
}
finally
{
Application.ResetState ();
}
}
[Fact]
[AutoInitShutdown (verifyShutdown: true)]
public void Internal_Properties_Correct ()
{
Assert.True (Application.Initialized);
Assert.Null (Application.Top);
RunState rs = Application.Begin (new ());
Assert.Equal (Application.Top, rs.Toplevel);
Assert.Null (Application.Mouse.MouseGrabView); // public
Application.Top!.Dispose ();
}
// Invoke Tests
// TODO: Test with threading scenarios
[Fact]
[AutoInitShutdown]
public void Invoke_Adds_Idle ()
{
Toplevel top = new ();
RunState rs = Application.Begin (top);
var actionCalled = 0;
Application.Invoke (() => { actionCalled++; });
ApplicationImpl.Instance.TimedEvents!.RunTimers ();
Assert.Equal (1, actionCalled);
top.Dispose ();
Application.Shutdown ();
}
[Fact]
public void Run_Iteration_Fires ()
{
var iteration = 0;
Application.Init (null, "fake");
Application.Iteration += Application_Iteration;
Application.Run<Toplevel> ().Dispose ();
Assert.Equal (1, iteration);
Application.Shutdown ();
return;
void Application_Iteration (object sender, IterationEventArgs e)
{
if (iteration > 0)
{
Assert.Fail ();
}
iteration++;
Application.RequestStop ();
}
}
[Fact]
[AutoInitShutdown]
public void Screen_Size_Changes ()
{
IConsoleDriver driver = Application.Driver;
Application.Driver!.SetScreenSize (80, 25);
Assert.Equal (new (0, 0, 80, 25), driver.Screen);
Assert.Equal (new (0, 0, 80, 25), Application.Screen);
// TODO: Should not be possible to manually change these at whim!
driver.Cols = 100;
driver.Rows = 30;
// IConsoleDriver.Screen isn't assignable
//driver.Screen = new (0, 0, driver.Cols, Rows);
Application.Driver!.SetScreenSize (100, 30);
Assert.Equal (new (0, 0, 100, 30), driver.Screen);
// Assert does not make sense
// Assert.NotEqual (new (0, 0, 100, 30), Application.Screen);
// Assert.Equal (new (0, 0, 80, 25), Application.Screen);
Application.Screen = new (0, 0, driver.Cols, driver.Rows);
Assert.Equal (new (0, 0, 100, 30), driver.Screen);
Application.Shutdown ();
}
[Fact]
public void InitState_Throws_If_Driver_Is_Null () { Assert.Throws<ArgumentNullException> (static () => Application.SubscribeDriverEvents ()); }
#region RunTests
[Fact]
[AutoInitShutdown]
public void Run_T_After_InitWithDriver_with_TopLevel_Does_Not_Throws ()
{
Application.Iteration += (s, e) => Application.RequestStop ();
// Run<Toplevel> when already initialized or not with a Driver will not throw (because Window is derived from Toplevel)
// Using another type not derived from Toplevel will throws at compile time
Application.Run<Window> ();
Assert.True (Application.Top is Window);
Application.Top!.Dispose ();
Application.Shutdown ();
Assert.Null (Application.Top);
Assert.Null (Application.Driver);
}
[Fact]
public void Run_T_After_InitWithDriver_with_TopLevel_and_Driver_Does_Not_Throws ()
{
Application.Iteration += (s, e) => Application.RequestStop ();
// Run<Toplevel> when already initialized or not with a Driver will not throw (because Window is derived from Toplevel)
// Using another type not derived from Toplevel will throws at compile time
Application.Run<Window> (null, new FakeDriver ());
Assert.True (Application.Top is Window);
Application.Top!.Dispose ();
// Run<Toplevel> when already initialized or not with a Driver will not throw (because Dialog is derived from Toplevel)
Application.Run<Dialog> (null, new FakeDriver ());
Assert.True (Application.Top is Dialog);
Application.Top!.Dispose ();
Application.Shutdown ();
Assert.Null (Application.Top);
Assert.Null (Application.Driver);
}
[Fact]
[AutoInitShutdown]
[TestRespondersDisposed]
public void Run_T_After_Init_Does_Not_Disposes_Application_Top ()
{
// Init doesn't create a Toplevel and assigned it to Application.Top
// but Begin does
var initTop = new Toplevel ();
Application.Iteration += (s, a) =>
{
Assert.NotEqual (initTop, Application.Top);
#if DEBUG_IDISPOSABLE
Assert.False (initTop.WasDisposed);
#endif
Application.RequestStop ();
};
Application.Run<Toplevel> ();
#if DEBUG_IDISPOSABLE
Assert.False (initTop.WasDisposed);
initTop.Dispose ();
Assert.True (initTop.WasDisposed);
#endif
Application.Top!.Dispose ();
Application.Shutdown ();
Assert.Null (Application.Top);
Assert.Null (Application.Driver);
}
[Fact]
[TestRespondersDisposed]
[AutoInitShutdown]
public void Run_T_After_InitWithDriver_with_TestTopLevel_DoesNotThrow ()
{
Application.Iteration += (s, a) => { Application.RequestStop (); };
// Init has been called and we're passing no driver to Run<TestTopLevel>. This is ok.
Application.Run<Toplevel> ();
Application.Top!.Dispose ();
Application.Shutdown ();
Assert.Null (Application.Top);
Assert.Null (Application.Driver);
}
[Fact]
[TestRespondersDisposed]
public void Run_T_After_InitNullDriver_with_TestTopLevel_DoesNotThrow ()
{
Application.ForceDriver = "FakeDriver";
var a = new AutoInitShutdownAttribute ();
a.Before (null);
Application.Iteration += (s, a) => { Application.RequestStop (); };
// Init has been called, selecting FakeDriver; we're passing no driver to Run<TestTopLevel>. Should be fine.
Application.Run<Toplevel> ();
Application.Top!.Dispose ();
Application.Shutdown ();
Assert.Null (Application.Top);
Assert.Null (Application.Driver);
a.After (null);
}
[Fact]
[TestRespondersDisposed]
[AutoInitShutdown]
public void Run_T_Init_Driver_Cleared_with_TestTopLevel_Throws ()
{
Application.Driver = null;
// Init has been called, but Driver has been set to null. Bad.
Assert.Throws<InvalidOperationException> (() => Application.Run<Toplevel> ());
Application.Shutdown ();
Assert.Null (Application.Top);
Assert.Null (Application.Driver);
}
[Fact (Skip = "FakeDriver is not allowed, use AutoInitShutdown attribute instead")]
[TestRespondersDisposed]
public void Run_T_NoInit_DoesNotThrow ()
{
Application.ForceDriver = "FakeDriver";
Application.Iteration += (s, a) => { Application.RequestStop (); };
Application.Run<Toplevel> ();
Assert.Equal (typeof (FakeDriver), Application.Driver?.GetType ());
Application.Top!.Dispose ();
Application.Shutdown ();
Assert.Null (Application.Top);
Assert.Null (Application.Driver);
}
[Fact]
[TestRespondersDisposed]
public void Run_T_NoInit_WithDriver_DoesNotThrow ()
{
Application.Iteration += (s, a) => { Application.RequestStop (); };
// Init has NOT been called and we're passing a valid driver to Run<TestTopLevel>. This is ok.
Application.Run<Toplevel> (null, new FakeDriver ());
Application.Top!.Dispose ();
Application.Shutdown ();
Assert.Null (Application.Top);
Assert.Null (Application.Driver);
}
[Fact]
[TestRespondersDisposed]
[AutoInitShutdown]
public void Run_RequestStop_Stops ()
{
var top = new Toplevel ();
RunState rs = Application.Begin (top);
Assert.NotNull (rs);
Application.Iteration += (s, a) => { Application.RequestStop (); };
Application.Run (top);
top.Dispose ();
Application.Shutdown ();
Assert.Null (Application.Top);
Assert.Null (Application.Driver);
}
[Fact]
[AutoInitShutdown]
public void Run_Sets_Running_True ()
{
var top = new Toplevel ();
RunState rs = Application.Begin (top);
Assert.NotNull (rs);
Application.Iteration += (s, a) =>
{
Assert.True (top.Running);
top.Running = false;
};
Application.Run (top);
top.Dispose ();
Application.Shutdown ();
Assert.Null (Application.Top);
Assert.Null (Application.Driver);
}
[Fact]
[TestRespondersDisposed]
[AutoInitShutdown]
public void Run_RunningFalse_Stops ()
{
var top = new Toplevel ();
RunState rs = Application.Begin (top);
Assert.NotNull (rs);
Application.Iteration += (s, a) => { top.Running = false; };
Application.Run (top);
top.Dispose ();
Application.Shutdown ();
Assert.Null (Application.Top);
Assert.Null (Application.Driver);
}
[Fact]
[AutoInitShutdown]
[TestRespondersDisposed]
public void Run_Loaded_Ready_Unloaded_Events ()
{
Toplevel top = new ();
var count = 0;
top.Loaded += (s, e) => count++;
top.Ready += (s, e) => count++;
top.Unloaded += (s, e) => count++;
Application.Iteration += (s, a) => Application.RequestStop ();
Application.Run (top);
top.Dispose ();
Application.Shutdown ();
Assert.Equal (3, count);
}
// TODO: All Toplevel layout tests should be moved to ToplevelTests.cs
[Fact]
[AutoInitShutdown]
public void Run_A_Modal_Toplevel_Refresh_Background_On_Moving ()
{
// Don't use Dialog here as it has more layout logic. Use Window instead.
var w = new Window
{
Width = 5, Height = 5,
Arrangement = ViewArrangement.Movable
};
Application.Driver!.SetScreenSize (10, 10);
RunState rs = Application.Begin (w);
// Don't use visuals to test as style of border can change over time.
Assert.Equal (new (0, 0), w.Frame.Location);
Application.RaiseMouseEvent (new () { Flags = MouseFlags.Button1Pressed });
Assert.Equal (w.Border, Application.Mouse.MouseGrabView);
Assert.Equal (new (0, 0), w.Frame.Location);
// Move down and to the right.
Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 1), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition });
Assert.Equal (new (1, 1), w.Frame.Location);
Application.End (rs);
w.Dispose ();
Application.Shutdown ();
}
[Fact]
[AutoInitShutdown]
public void End_Does_Not_Dispose ()
{
var top = new Toplevel ();
Window w = new ();
w.Ready += (s, e) => Application.RequestStop (); // Causes `End` to be called
Application.Run (w);
#if DEBUG_IDISPOSABLE
Assert.False (w.WasDisposed);
#endif
Assert.NotNull (w);
Assert.Equal (string.Empty, w.Title); // Valid - w has not been disposed. The user may want to run it again
Assert.NotNull (Application.Top);
Assert.Equal (w, Application.Top);
Assert.NotEqual (top, Application.Top);
Application.Run (w); // Valid - w has not been disposed.
#if DEBUG_IDISPOSABLE
Assert.False (w.WasDisposed);
Exception exception = Record.Exception (Application.Shutdown); // Invalid - w has not been disposed.
Assert.NotNull (exception);
w.Dispose ();
Assert.True (w.WasDisposed);
//exception = Record.Exception (
// () => Application.Run (
// w)); // Invalid - w has been disposed. Run it in debug mode will throw, otherwise the user may want to run it again
//Assert.NotNull (exception);
// TODO: Re-enable this when we are done debug logging of ctx.Source.Title in RaiseSelecting
//exception = Record.Exception (() => Assert.Equal (string.Empty, w.Title)); // Invalid - w has been disposed and cannot be accessed
//Assert.NotNull (exception);
//exception = Record.Exception (() => w.Title = "NewTitle"); // Invalid - w has been disposed and cannot be accessed
//Assert.NotNull (exception);
#endif
Application.Shutdown ();
Assert.NotNull (w);
Assert.NotNull (top);
Assert.Null (Application.Top);
}
[Fact]
public void Run_Creates_Top_Without_Init ()
{
var driver = new FakeDriver ();
Assert.Null (Application.Top);
Application.Iteration += (s, e) =>
{
Assert.NotNull (Application.Top);
Application.RequestStop ();
};
Toplevel top = Application.Run (null, driver);
#if DEBUG_IDISPOSABLE
Assert.Equal (top, Application.Top);
Assert.False (top.WasDisposed);
Exception exception = Record.Exception (Application.Shutdown);
Assert.NotNull (exception);
Assert.False (top.WasDisposed);
#endif
// It's up to caller to dispose it
top.Dispose ();
#if DEBUG_IDISPOSABLE
Assert.True (top.WasDisposed);
#endif
Assert.NotNull (Application.Top);
Application.Shutdown ();
Assert.Null (Application.Top);
}
[Fact]
public void Run_T_Creates_Top_Without_Init ()
{
var driver = new FakeDriver ();
Assert.Null (Application.Top);
Application.Iteration += (s, e) =>
{
Assert.NotNull (Application.Top);
Application.RequestStop ();
};
Application.Run<Toplevel> (null, driver);
#if DEBUG_IDISPOSABLE
Assert.False (Application.Top!.WasDisposed);
Exception exception = Record.Exception (Application.Shutdown);
Assert.NotNull (exception);
Assert.False (Application.Top!.WasDisposed);
// It's up to caller to dispose it
Application.Top!.Dispose ();
Assert.True (Application.Top!.WasDisposed);
#endif
Assert.NotNull (Application.Top);
Application.Shutdown ();
Assert.Null (Application.Top);
}
[Fact]
public void Run_t_Does_Not_Creates_Top_Without_Init ()
{
// When a Toplevel is created it must already have all the Application configuration loaded
// This is only possible by two ways:
// 1 - Using Application.Init first
// 2 - Using Application.Run() or Application.Run<T>()
// The Application.Run(new(Toplevel)) must always call Application.Init() first because
// the new(Toplevel) may be a derived class that is possible using Application static
// properties that is only available after the Application.Init was called
var driver = new FakeDriver ();
Assert.Null (Application.Top);
Assert.Throws<NotInitializedException> (() => Application.Run (new Toplevel ()));
Application.Init (driver);
Application.Iteration += (s, e) =>
{
Assert.NotNull (Application.Top);
Application.RequestStop ();
};
Application.Run (new Toplevel ());
#if DEBUG_IDISPOSABLE
Assert.False (Application.Top!.WasDisposed);
Exception exception = Record.Exception (Application.Shutdown);
Assert.NotNull (exception);
Assert.False (Application.Top!.WasDisposed);
// It's up to caller to dispose it
Application.Top!.Dispose ();
Assert.True (Application.Top!.WasDisposed);
#endif
Assert.NotNull (Application.Top);
Application.Shutdown ();
Assert.Null (Application.Top);
}
private class TestToplevel : Toplevel
{ }
private readonly object _forceDriverLock = new ();
/*
[Theory]
// This test wants to Run which results in console handle errors, it wants to rely non drivers checking ConsoleDriver.RunningUnitTests
// And suppressing things that might fail, this is anti pattern, instead we should test this kind of thing with Mocking
// [InlineData ("v2win", typeof (ConsoleDriverFacade<WindowsConsole.InputRecord>))]
// [InlineData ("v2net", typeof (ConsoleDriverFacade<ConsoleKeyInfo>))]
// FakeDriver is not allowed, use AutoInitShutdown attribute instead
//[InlineData ("FakeDriver", typeof (FakeDriver))]
//[InlineData ("DotNetDriver", typeof (DotNetDriver))]
//[InlineData ("WindowsDriver", typeof (WindowsDriver))]
//[InlineData ("UnixDriver", typeof (UnixDriver))]
public void Run_T_Call_Init_ForceDriver_Should_Pick_Correct_Driver (string driverName, Type expectedType)
{
Assert.True (ConsoleDriver.RunningUnitTests);
var result = false;
lock (_forceDriverLock)
{
Task.Run (() =>
{
while (!Application.Initialized)
{
Task.Delay (300).Wait ();
}
})
.ContinueWith (
(t, _) =>
{
// no longer loading
Assert.True (Application.Initialized);
Application.Invoke (() =>
{
result = true;
Application.RequestStop ();
});
},
TaskScheduler.FromCurrentSynchronizationContext ());
}
Application.ForceDriver = driverName;
Application.Run<TestToplevel> ();
Assert.NotNull (Application.Driver);
Assert.Equal (expectedType, Application.Driver?.GetType ());
Assert.NotNull (Application.Top);
Assert.False (Application.Top!.Running);
Application.Top!.Dispose ();
Application.Shutdown ();
Assert.True (result);
}
*/
[Fact]
public void Run_T_With_Legacy_Driver_Does_Not_Call_ResetState_After_Init ()
{
Assert.False (Application.Initialized);
Application.Init ();
Assert.True (Application.Initialized);
Application.Iteration += (_, _) => Application.RequestStop ();
Application.Run<TestToplevel> ();
Assert.NotNull (Application.Driver);
Assert.NotNull (Application.Top);
Assert.False (Application.Top!.Running);
Application.Top!.Dispose ();
Application.Shutdown ();
}
[Fact]
public void Run_T_With_V2_Driver_Does_Not_Call_ResetState_After_Init ()
{
Assert.False (Application.Initialized);
Application.Init (null, "v2net");
Assert.True (Application.Initialized);
Task.Run (() => { Task.Delay (300).Wait (); })
.ContinueWith (
(t, _) =>
{
// no longer loading
Application.Invoke (() => { Application.RequestStop (); });
},
TaskScheduler.FromCurrentSynchronizationContext ());
Application.Run<TestToplevel> ();
Assert.NotNull (Application.Driver);
Assert.NotNull (Application.Top);
Assert.False (Application.Top!.Running);
Application.Top!.Dispose ();
Application.Shutdown ();
}
// TODO: Add tests for Run that test errorHandler
#endregion
#region ShutdownTests
[Fact]
public async Task Shutdown_Allows_Async ()
{
var isCompletedSuccessfully = false;
async Task TaskWithAsyncContinuation ()
{
await Task.Yield ();
await Task.Yield ();
isCompletedSuccessfully = true;
}
Application.Shutdown ();
Assert.False (isCompletedSuccessfully);
await TaskWithAsyncContinuation ();
Thread.Sleep (100);
Assert.True (isCompletedSuccessfully);
}
[Fact]
public void Shutdown_Resets_SyncContext ()
{
Application.Shutdown ();
Assert.Null (SynchronizationContext.Current);
}
#endregion
}