Fixes #4125. Remove legacy MainLoop infrastructure (#4343)

* Initial plan

* Phase 1: Update IConsoleDriver.Init() to return void instead of MainLoop

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

* Phase 2: Remove legacy MainLoop infrastructure

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

* Complete Phase 1 and Phase 2 - All tests pass

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

* Update deep dive docs to reflect MainLoop removal

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

---------

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>
This commit is contained in:
Copilot
2025-10-26 13:39:44 -06:00
committed by GitHub
parent 4ea7d32d65
commit a0979368cb
31 changed files with 25 additions and 1533 deletions

View File

@@ -99,7 +99,6 @@ public class Arrangement : Scenario
progressBar.Fraction += 0.01f; progressBar.Fraction += 0.01f;
Application.Wakeup ();
progressBar.SetNeedsDraw (); progressBar.SetNeedsDraw ();
}; };

View File

@@ -100,7 +100,6 @@ public class Clipping : Scenario
{ {
tiledProgressBar1.Pulse (); tiledProgressBar1.Pulse ();
tiledProgressBar2.Pulse (); tiledProgressBar2.Pulse ();
Application.Wakeup ();
}; };
progressTimer.Start (); progressTimer.Start ();

View File

@@ -198,7 +198,6 @@ public class ProgressBarStyles : Scenario
button.Enabled = true; button.Enabled = true;
} }
Application.Wakeup ();
}, },
null, null,
0, 0,
@@ -282,7 +281,6 @@ public class ProgressBarStyles : Scenario
marqueesBlocksPB.Text = marqueesContinuousPB.Text = DateTime.Now.TimeOfDay.ToString (); marqueesBlocksPB.Text = marqueesContinuousPB.Text = DateTime.Now.TimeOfDay.ToString ();
marqueesBlocksPB.Pulse (); marqueesBlocksPB.Pulse ();
marqueesContinuousPB.Pulse (); marqueesContinuousPB.Pulse ();
Application.Wakeup ();
}, },
null, null,
0, 0,

View File

@@ -384,7 +384,6 @@ public class Shortcuts : Scenario
pb.Fraction += 0.01f; pb.Fraction += 0.01f;
Application.Wakeup ();
pb.SetNeedsDraw (); pb.SetNeedsDraw ();
} }

View File

@@ -131,7 +131,7 @@ public static partial class Application // Lifecycle (Init/Shutdown)
try try
{ {
MainLoop = Driver!.Init (); Driver!.Init ();
SubscribeDriverEvents (); SubscribeDriverEvents ();
} }
catch (InvalidOperationException ex) catch (InvalidOperationException ex)

View File

@@ -372,12 +372,6 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E
/// <param name="action">the action to be invoked on the main processing thread.</param> /// <param name="action">the action to be invoked on the main processing thread.</param>
public static void Invoke (Action action) { ApplicationImpl.Instance.Invoke (action); } public static void Invoke (Action action) { ApplicationImpl.Instance.Invoke (action); }
// TODO: Determine if this is really needed. The only code that calls WakeUp I can find
// is ProgressBarStyles, and it's not clear it needs to.
/// <summary>Wakes up the running application that might be waiting on input.</summary>
public static void Wakeup () { MainLoop?.Wakeup (); }
/// <summary> /// <summary>
/// Causes any Toplevels that need layout to be laid out. Then draws any Toplevels that need display. Only Views that /// Causes any Toplevels that need layout to be laid out. Then draws any Toplevels that need display. Only Views that
/// need to be laid out (see <see cref="View.NeedsLayout"/>) will be laid out. /// need to be laid out (see <see cref="View.NeedsLayout"/>) will be laid out.
@@ -396,10 +390,6 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E
/// <remarks>See also <see cref="Timeout"/></remarks> /// <remarks>See also <see cref="Timeout"/></remarks>
public static event EventHandler<IterationEventArgs>? Iteration; public static event EventHandler<IterationEventArgs>? Iteration;
/// <summary>The <see cref="MainLoop"/> driver for the application</summary>
/// <value>The main loop.</value>
internal static MainLoop? MainLoop { get; set; }
/// <summary> /// <summary>
/// Set to true to cause <see cref="End"/> to be called after the first iteration. Set to false (the default) to /// Set to true to cause <see cref="End"/> to be called after the first iteration. Set to false (the default) to
/// cause the application to continue running until Application.RequestStop () is called. /// cause the application to continue running until Application.RequestStop () is called.
@@ -417,11 +407,6 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E
for (state.Toplevel.Running = true; state.Toplevel?.Running == true;) for (state.Toplevel.Running = true; state.Toplevel?.Running == true;)
{ {
if (MainLoop is { })
{
MainLoop.Running = true;
}
if (EndAfterFirstIteration && !firstIteration) if (EndAfterFirstIteration && !firstIteration)
{ {
return; return;
@@ -430,11 +415,6 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E
firstIteration = RunIteration (ref state, firstIteration); firstIteration = RunIteration (ref state, firstIteration);
} }
if (MainLoop is { })
{
MainLoop.Running = false;
}
// Run one last iteration to consume any outstanding input events from Driver // Run one last iteration to consume any outstanding input events from Driver
// This is important for remaining OnKeyUp events. // This is important for remaining OnKeyUp events.
RunIteration (ref state, firstIteration); RunIteration (ref state, firstIteration);

View File

@@ -216,9 +216,6 @@ public static partial class Application
Top = null; Top = null;
_cachedRunStateToplevel = null; _cachedRunStateToplevel = null;
// MainLoop stuff
MainLoop?.Dispose ();
MainLoop = null;
MainThreadId = -1; MainThreadId = -1;
Iteration = null; Iteration = null;
EndAfterFirstIteration = false; EndAfterFirstIteration = false;

View File

@@ -1,24 +0,0 @@
#nullable enable
namespace Terminal.Gui.App;
/// <summary>Interface to create a platform specific <see cref="MainLoop"/> driver.</summary>
internal interface IMainLoopDriver
{
/// <summary>Must report whether there are any events pending, or even block waiting for events.</summary>
/// <returns><see langword="true"/>, if there were pending events, <see langword="false"/> otherwise.</returns>
bool EventsPending ();
/// <summary>The iteration function.</summary>
void Iteration ();
/// <summary>Initializes the <see cref="MainLoop"/>, gets the calling main loop for the initialization.</summary>
/// <remarks>Call <see cref="TearDown"/> to release resources.</remarks>
/// <param name="mainLoop">Main loop.</param>
void Setup (MainLoop mainLoop);
/// <summary>Tears down the <see cref="MainLoop"/> driver. Releases resources created in <see cref="Setup"/>.</summary>
void TearDown ();
/// <summary>Wakes up the <see cref="MainLoop"/> that might be waiting on input, must be thread safe.</summary>
void Wakeup ();
}

View File

@@ -1,122 +0,0 @@
#nullable enable
//
// LegacyMainLoopDriver.cs: IMainLoopDriver and MainLoop for legacy v1 driver based applications
//
// Authors:
// Miguel de Icaza (miguel@gnome.org)
//
using System.Collections.ObjectModel;
namespace Terminal.Gui.App;
/// <summary>
/// The main event loop of legacy v1 driver based applications.
/// </summary>
/// <remarks>
/// <para>
/// This class is provided for backward compatibility with the legacy FakeDriver implementation.
/// New code should use the modern <see cref="ApplicationMainLoop{T}"/> architecture instead.
/// </para>
/// <para>
/// Monitoring of file descriptors is only available on Unix, there does not seem to be a way of supporting this
/// on Windows.
/// </para>
/// </remarks>
[Obsolete ("This class is for legacy FakeDriver compatibility only. Use ApplicationMainLoop<T> for new code.")]
public class MainLoop : IDisposable
{
/// <summary>
/// Gets the class responsible for handling timeouts
/// </summary>
public ITimedEvents TimedEvents { get; } = new TimedEvents();
/// <summary>Creates a new MainLoop.</summary>
/// <remarks>Use <see cref="Dispose"/> to release resources.</remarks>
/// <param name="driver">
/// The <see cref="IConsoleDriver"/> instance (one of the implementations FakeMainLoop, UnixMainLoop,
/// NetMainLoop or WindowsMainLoop).
/// </param>
internal MainLoop (IMainLoopDriver driver)
{
MainLoopDriver = driver;
driver.Setup (this);
}
/// <summary>The current <see cref="IMainLoopDriver"/> in use.</summary>
/// <value>The main loop driver.</value>
internal IMainLoopDriver? MainLoopDriver { get; private set; }
/// <summary>Used for unit tests.</summary>
internal bool Running { get; set; }
/// <inheritdoc/>
public void Dispose ()
{
GC.SuppressFinalize (this);
Stop ();
Running = false;
MainLoopDriver?.TearDown ();
MainLoopDriver = null;
}
/// <summary>Determines whether there are pending events to be processed.</summary>
/// <remarks>
/// You can use this method if you want to probe if events are pending. Typically used if you need to flush the
/// input queue while still running some of your own code in your main thread.
/// </remarks>
internal bool EventsPending () { return MainLoopDriver!.EventsPending (); }
/// <summary>Runs the <see cref="MainLoop"/>. Used only for unit tests.</summary>
internal void Run ()
{
bool prev = Running;
Running = true;
while (Running)
{
EventsPending ();
RunIteration ();
}
Running = prev;
}
/// <summary>Runs one iteration of timers and file watches</summary>
/// <remarks>
/// Use this to process all pending events (timers handlers and file watches).
/// <code>
/// while (main.EventsPending ()) RunIteration ();
/// </code>
/// </remarks>
internal void RunIteration ()
{
RunAnsiScheduler ();
MainLoopDriver?.Iteration ();
TimedEvents.RunTimers ();
}
private void RunAnsiScheduler ()
{
Application.Driver?.GetRequestScheduler ().RunSchedule ();
}
/// <summary>Stops the main loop driver and calls <see cref="IMainLoopDriver.Wakeup"/>. Used only for unit tests.</summary>
internal void Stop ()
{
Running = false;
Wakeup ();
}
/// <summary>Wakes up the <see cref="MainLoop"/> that might be waiting on input.</summary>
internal void Wakeup () { MainLoopDriver?.Wakeup (); }
}

View File

@@ -10,23 +10,8 @@ internal sealed class MainLoopSyncContext : SynchronizationContext
public override void Post (SendOrPostCallback d, object state) public override void Post (SendOrPostCallback d, object state)
{ {
// Queue the task // Queue the task using the modern architecture
if (ApplicationImpl.Instance.IsLegacy) ApplicationImpl.Instance.Invoke (() => { d (state); });
{
Application.MainLoop?.TimedEvents.Add (TimeSpan.Zero,
() =>
{
d (state);
return false;
}
);
Application.MainLoop?.Wakeup ();
}
else
{
ApplicationImpl.Instance.Invoke (() => { d (state); });
}
} }
//_mainLoop.Driver.Wakeup (); //_mainLoop.Driver.Wakeup ();

View File

@@ -931,7 +931,7 @@ public static class EscSeqUtils
_isButtonClicked = false; _isButtonClicked = false;
_isButtonDoubleClicked = true; _isButtonDoubleClicked = true;
Application.MainLoop?.TimedEvents.Add (TimeSpan.Zero, ApplicationImpl.Instance.TimedEvents?.Add (TimeSpan.Zero,
() => () =>
{ {
Task.Run (async () => await ProcessButtonDoubleClickedAsync ()); Task.Run (async () => await ProcessButtonDoubleClickedAsync ());
@@ -970,7 +970,7 @@ public static class EscSeqUtils
mouseFlags.Add (GetButtonClicked (buttonState)); mouseFlags.Add (GetButtonClicked (buttonState));
_isButtonClicked = true; _isButtonClicked = true;
Application.MainLoop?.TimedEvents.Add (TimeSpan.Zero, ApplicationImpl.Instance.TimedEvents?.Add (TimeSpan.Zero,
() => () =>
{ {
Task.Run (async () => await ProcessButtonClickedAsync ()); Task.Run (async () => await ProcessButtonClickedAsync ());

View File

@@ -550,8 +550,7 @@ public abstract class ConsoleDriver : IConsoleDriver
#region Setup & Teardown #region Setup & Teardown
/// <summary>Initializes the driver</summary> /// <summary>Initializes the driver</summary>
/// <returns>Returns an instance of <see cref="MainLoop"/> using the <see cref="IMainLoopDriver"/> for the driver.</returns> public abstract void Init ();
public abstract MainLoop Init ();
/// <summary>Ends the execution of the console driver.</summary> /// <summary>Ends the execution of the console driver.</summary>
public abstract void End (); public abstract void End ();

View File

@@ -365,7 +365,7 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
/// <summary>Initializes the driver</summary> /// <summary>Initializes the driver</summary>
/// <returns>Returns an instance of <see cref="MainLoop"/> using the <see cref="IMainLoopDriver"/> for the driver.</returns> /// <returns>Returns an instance of <see cref="MainLoop"/> using the <see cref="IMainLoopDriver"/> for the driver.</returns>
public MainLoop Init () { throw new NotSupportedException (); } public void Init () { throw new NotSupportedException (); }
/// <summary>Ends the execution of the console driver.</summary> /// <summary>Ends the execution of the console driver.</summary>
public void End () public void End ()

View File

@@ -91,9 +91,7 @@ public class FakeDriver : ConsoleDriver
FakeConsole.Clear (); FakeConsole.Clear ();
} }
private FakeMainLoop? _mainLoopDriver; public override void Init ()
public override MainLoop Init ()
{ {
FakeConsole.MockKeyPresses.Clear (); FakeConsole.MockKeyPresses.Clear ();
@@ -102,12 +100,6 @@ public class FakeDriver : ConsoleDriver
FakeConsole.Clear (); FakeConsole.Clear ();
ResizeScreen (); ResizeScreen ();
CurrentAttribute = new Attribute (Color.White, Color.Black); CurrentAttribute = new Attribute (Color.White, Color.Black);
//ClearContents ();
_mainLoopDriver = new FakeMainLoop (this);
_mainLoopDriver.MockKeyPressed = MockKeyPressedHandler;
return new MainLoop (_mainLoopDriver);
} }
public override bool UpdateScreen () public override bool UpdateScreen ()
@@ -346,24 +338,6 @@ public class FakeDriver : ConsoleDriver
private CursorVisibility _savedCursorVisibility; private CursorVisibility _savedCursorVisibility;
private void MockKeyPressedHandler (ConsoleKeyInfo consoleKeyInfo)
{
if (consoleKeyInfo.Key == ConsoleKey.Packet)
{
consoleKeyInfo = ConsoleKeyMapping.DecodeVKPacketToKConsoleKeyInfo (consoleKeyInfo);
}
KeyCode map = MapKey (consoleKeyInfo);
if (IsValidInput (map, out map))
{
OnKeyDown (new (map));
OnKeyUp (new (map));
}
//OnKeyPressed (new KeyEventArgs (map));
}
/// <inheritdoc/> /// <inheritdoc/>
public override bool GetCursorVisibility (out CursorVisibility visibility) public override bool GetCursorVisibility (out CursorVisibility visibility)
{ {

View File

@@ -1,38 +0,0 @@

namespace Terminal.Gui.Drivers;
internal class FakeMainLoop : IMainLoopDriver
{
public Action<ConsoleKeyInfo> MockKeyPressed;
public FakeMainLoop (IConsoleDriver consoleDriver = null)
{
// No implementation needed for FakeMainLoop
}
public void Setup (MainLoop mainLoop)
{
// No implementation needed for FakeMainLoop
}
public void Wakeup ()
{
// No implementation needed for FakeMainLoop
}
public bool EventsPending ()
{
// Always return true for FakeMainLoop
return true;
}
public void Iteration ()
{
if (FakeConsole.MockKeyPresses.Count > 0)
{
MockKeyPressed?.Invoke (FakeConsole.MockKeyPresses.Pop ());
}
}
public void TearDown () { }
}

View File

@@ -214,8 +214,7 @@ public interface IConsoleDriver
void UpdateCursor (); void UpdateCursor ();
/// <summary>Initializes the driver</summary> /// <summary>Initializes the driver</summary>
/// <returns>Returns an instance of <see cref="MainLoop"/> using the <see cref="IMainLoopDriver"/> for the driver.</returns> void Init ();
MainLoop Init ();
/// <summary>Ends the execution of the console driver.</summary> /// <summary>Ends the execution of the console driver.</summary>
void End (); void End ();

View File

@@ -159,7 +159,6 @@ public class ApplicationTests
Application.Shutdown (); Application.Shutdown ();
Assert.Null (Application.Top); Assert.Null (Application.Top);
Assert.Null (Application.MainLoop);
Assert.Null (Application.Driver); Assert.Null (Application.Driver);
} }
@@ -316,7 +315,6 @@ public class ApplicationTests
// Don't check Application.Force16Colors // Don't check Application.Force16Colors
//Assert.False (Application.Force16Colors); //Assert.False (Application.Force16Colors);
Assert.Null (Application.Driver); Assert.Null (Application.Driver);
Assert.Null (Application.MainLoop);
Assert.False (Application.EndAfterFirstIteration); Assert.False (Application.EndAfterFirstIteration);
// Commented out because if CM changed the defaults, those changes should // Commented out because if CM changed the defaults, those changes should
@@ -472,7 +470,6 @@ public class ApplicationTests
Application.Shutdown (); Application.Shutdown ();
Assert.Null (Application.Top); Assert.Null (Application.Top);
Assert.Null (Application.MainLoop);
Assert.Null (Application.Driver); Assert.Null (Application.Driver);
} }
@@ -486,7 +483,6 @@ public class ApplicationTests
Application.Shutdown (); Application.Shutdown ();
Assert.Null (Application.Top); Assert.Null (Application.Top);
Assert.Null (Application.MainLoop);
Assert.Null (Application.Driver); Assert.Null (Application.Driver);
} }
@@ -522,14 +518,12 @@ public class ApplicationTests
Application.End (runstate); Application.End (runstate);
Assert.NotNull (Application.Top); Assert.NotNull (Application.Top);
Assert.NotNull (Application.MainLoop);
Assert.NotNull (Application.Driver); Assert.NotNull (Application.Driver);
topLevel.Dispose (); topLevel.Dispose ();
Application.Shutdown (); Application.Shutdown ();
Assert.Null (Application.Top); Assert.Null (Application.Top);
Assert.Null (Application.MainLoop);
Assert.Null (Application.Driver); Assert.Null (Application.Driver);
} }
@@ -676,7 +670,6 @@ public class ApplicationTests
Application.Shutdown (); Application.Shutdown ();
Assert.Null (Application.Top); Assert.Null (Application.Top);
Assert.Null (Application.MainLoop);
Assert.Null (Application.Driver); Assert.Null (Application.Driver);
} }
@@ -700,7 +693,6 @@ public class ApplicationTests
Application.Shutdown (); Application.Shutdown ();
Assert.Null (Application.Top); Assert.Null (Application.Top);
Assert.Null (Application.MainLoop);
Assert.Null (Application.Driver); Assert.Null (Application.Driver);
} }
@@ -734,7 +726,6 @@ public class ApplicationTests
Application.Shutdown (); Application.Shutdown ();
Assert.Null (Application.Top); Assert.Null (Application.Top);
Assert.Null (Application.MainLoop);
Assert.Null (Application.Driver); Assert.Null (Application.Driver);
} }
@@ -752,7 +743,6 @@ public class ApplicationTests
Application.Shutdown (); Application.Shutdown ();
Assert.Null (Application.Top); Assert.Null (Application.Top);
Assert.Null (Application.MainLoop);
Assert.Null (Application.Driver); Assert.Null (Application.Driver);
} }
@@ -774,7 +764,6 @@ public class ApplicationTests
Application.Shutdown (); Application.Shutdown ();
Assert.Null (Application.Top); Assert.Null (Application.Top);
Assert.Null (Application.MainLoop);
Assert.Null (Application.Driver); Assert.Null (Application.Driver);
a.After (null); a.After (null);
@@ -793,7 +782,6 @@ public class ApplicationTests
Application.Shutdown (); Application.Shutdown ();
Assert.Null (Application.Top); Assert.Null (Application.Top);
Assert.Null (Application.MainLoop);
Assert.Null (Application.Driver); Assert.Null (Application.Driver);
} }
@@ -812,7 +800,6 @@ public class ApplicationTests
Application.Shutdown (); Application.Shutdown ();
Assert.Null (Application.Top); Assert.Null (Application.Top);
Assert.Null (Application.MainLoop);
Assert.Null (Application.Driver); Assert.Null (Application.Driver);
} }
@@ -829,7 +816,6 @@ public class ApplicationTests
Application.Shutdown (); Application.Shutdown ();
Assert.Null (Application.Top); Assert.Null (Application.Top);
Assert.Null (Application.MainLoop);
Assert.Null (Application.Driver); Assert.Null (Application.Driver);
} }
@@ -849,7 +835,6 @@ public class ApplicationTests
top.Dispose (); top.Dispose ();
Application.Shutdown (); Application.Shutdown ();
Assert.Null (Application.Top); Assert.Null (Application.Top);
Assert.Null (Application.MainLoop);
Assert.Null (Application.Driver); Assert.Null (Application.Driver);
} }
@@ -872,7 +857,6 @@ public class ApplicationTests
top.Dispose (); top.Dispose ();
Application.Shutdown (); Application.Shutdown ();
Assert.Null (Application.Top); Assert.Null (Application.Top);
Assert.Null (Application.MainLoop);
Assert.Null (Application.Driver); Assert.Null (Application.Driver);
} }
@@ -892,7 +876,6 @@ public class ApplicationTests
top.Dispose (); top.Dispose ();
Application.Shutdown (); Application.Shutdown ();
Assert.Null (Application.Top); Assert.Null (Application.Top);
Assert.Null (Application.MainLoop);
Assert.Null (Application.Driver); Assert.Null (Application.Driver);
} }

View File

@@ -1,940 +0,0 @@
using System.Diagnostics;
using Xunit.Abstractions;
// Alias Console to MockConsole so we don't accidentally use Console
namespace UnitTests.ApplicationTests;
/// <summary>Tests MainLoop using the FakeMainLoop.</summary>
public class MainLoopTests
{
private readonly ITestOutputHelper _output;
public MainLoopTests (ITestOutputHelper output)
{
_output = output;
ConsoleDriver.RunningUnitTests = true;
}
private static Button btn;
private static string cancel;
private static string clickMe;
private static int four;
private static int one;
private static string pewPew;
private static bool taskCompleted;
// TODO: EventsPending tests
// - wait = true
// - wait = false
// TODO: Add IMainLoop tests
private static int three;
private static int total;
private static int two;
private static int zero;
// See Also ConsoleDRivers/MainLoopDriverTests.cs for tests of the MainLoopDriver
// Idle Handler tests
[Fact]
public void AddTimeout_Adds_And_Removes ()
{
var ml = new MainLoop (new FakeMainLoop ());
Func<bool> fnTrue = () => true;
Func<bool> fnFalse = () => false;
var a = ml.TimedEvents.Add (TimeSpan.Zero, fnTrue);
var b = ml.TimedEvents.Add (TimeSpan.Zero, fnFalse);
Assert.Equal (2, ml.TimedEvents.Timeouts.Count);
Assert.Equal (fnTrue, ml.TimedEvents.Timeouts.ElementAt (0).Value.Callback);
Assert.NotEqual (fnFalse, ml.TimedEvents.Timeouts.ElementAt (0).Value.Callback);
Assert.True (ml.TimedEvents.Remove (a));
Assert.Single (ml.TimedEvents.Timeouts);
// BUGBUG: This doesn't throw or indicate an error. Ideally RemoveIdle would either
// throw an exception in this case, or return an error.
// No. Only need to return a boolean.
Assert.False (ml.TimedEvents.Remove (a));
Assert.True (ml.TimedEvents.Remove (b));
// BUGBUG: This doesn't throw an exception or indicate an error. Ideally RemoveIdle would either
// throw an exception in this case, or return an error.
// No. Only need to return a boolean.
Assert.False (ml.TimedEvents.Remove(b));
}
[Fact]
public void AddTimeout_Function_GetsCalled_OnIteration ()
{
var ml = new MainLoop (new FakeMainLoop ());
var functionCalled = 0;
Func<bool> fn = () =>
{
functionCalled++;
return true;
};
ml.TimedEvents.Add (TimeSpan.Zero, fn);
ml.RunIteration ();
Assert.Equal (1, functionCalled);
}
[Fact]
public void AddTimeout_Twice_Returns_False_Called_Twice ()
{
var ml = new MainLoop (new FakeMainLoop ());
var functionCalled = 0;
Func<bool> fn1 = () =>
{
functionCalled++;
return false;
};
// Force stop if 10 iterations
var stopCount = 0;
Func<bool> fnStop = () =>
{
stopCount++;
if (stopCount == 10)
{
ml.Stop ();
}
return true;
};
var a = ml.TimedEvents.Add (TimeSpan.Zero, fnStop);
var b = ml.TimedEvents.Add (TimeSpan.Zero, fn1);
ml.Run ();
Assert.True (ml.TimedEvents.Remove(a));
Assert.False (ml.TimedEvents.Remove (a));
// Cannot remove b because it returned false i.e. auto removes itself
Assert.False (ml.TimedEvents.Remove (b));
Assert.Equal (1, functionCalled);
}
[Fact]
public void AddTimeoutTwice_Function_CalledTwice ()
{
var ml = new MainLoop (new FakeMainLoop ());
var functionCalled = 0;
Func<bool> fn = () =>
{
functionCalled++;
return true;
};
var a = ml.TimedEvents.Add (TimeSpan.Zero, fn);
var b = ml.TimedEvents.Add (TimeSpan.Zero, fn);
ml.RunIteration ();
Assert.Equal (2, functionCalled);
Assert.Equal (2, ml.TimedEvents.Timeouts.Count);
functionCalled = 0;
Assert.True (ml.TimedEvents.Remove (a));
Assert.Single (ml.TimedEvents.Timeouts);
ml.RunIteration ();
Assert.Equal (1, functionCalled);
functionCalled = 0;
Assert.True (ml.TimedEvents.Remove (b));
Assert.Empty (ml.TimedEvents.Timeouts);
ml.RunIteration ();
Assert.Equal (0, functionCalled);
Assert.False (ml.TimedEvents.Remove (b));
}
[Fact]
public void AddThenRemoveIdle_Function_NotCalled ()
{
var ml = new MainLoop (new FakeMainLoop ());
var functionCalled = 0;
Func<bool> fn = () =>
{
functionCalled++;
return true;
};
var a = ml.TimedEvents.Add (TimeSpan.Zero, fn);
Assert.True (ml.TimedEvents.Remove (a));
ml.RunIteration ();
Assert.Equal (0, functionCalled);
}
// Timeout Handler Tests
[Fact]
public void AddTimer_Adds_Removes_NoFaults ()
{
var ml = new MainLoop (new FakeMainLoop ());
var ms = 100;
var callbackCount = 0;
Func<bool> callback = () =>
{
callbackCount++;
return true;
};
object token = ml.TimedEvents.Add (TimeSpan.FromMilliseconds (ms), callback);
Assert.True (ml.TimedEvents.Remove (token));
// BUGBUG: This should probably fault?
// Must return a boolean.
Assert.False (ml.TimedEvents.Remove (token));
}
[Fact]
public async Task AddTimer_Duplicate_Keys_Not_Allowed ()
{
var ml = new MainLoop (new FakeMainLoop ());
const int ms = 100;
object token1 = null, token2 = null;
var callbackCount = 0;
Func<bool> callback = () =>
{
callbackCount++;
if (callbackCount == 2)
{
ml.Stop ();
}
return true;
};
var task1 = new Task (() => token1 = ml.TimedEvents.Add (TimeSpan.FromMilliseconds (ms), callback));
var task2 = new Task (() => token2 = ml.TimedEvents.Add (TimeSpan.FromMilliseconds (ms), callback));
Assert.Null (token1);
Assert.Null (token2);
task1.Start ();
task2.Start ();
ml.Run ();
Assert.NotNull (token1);
Assert.NotNull (token2);
await Task.WhenAll (task1, task2);
Assert.True (ml.TimedEvents.Remove (token1));
Assert.True (ml.TimedEvents.Remove (token2));
Assert.Equal (2, callbackCount);
}
// Timeout Handler Tests
[Fact]
public void AddTimer_EventFired ()
{
var ml = new MainLoop (new FakeMainLoop ());
var ms = 100;
// Use Stopwatch ticks since TimedEvents now uses Stopwatch.GetTimestamp internally
long originTicks = Stopwatch.GetTimestamp () * TimeSpan.TicksPerSecond / Stopwatch.Frequency;
var callbackCount = 0;
Func<bool> callback = () =>
{
callbackCount++;
return true;
};
object sender = null;
TimeoutEventArgs args = null;
ml.TimedEvents.Added += (s, e) =>
{
sender = s;
args = e;
};
object token = ml.TimedEvents.Add (TimeSpan.FromMilliseconds (ms), callback);
Assert.Same (ml.TimedEvents, sender);
Assert.NotNull (args.Timeout);
Assert.True (args.Ticks - originTicks >= 100 * TimeSpan.TicksPerMillisecond);
}
[Fact]
public void AddTimer_In_Parallel_Wont_Throw ()
{
var ml = new MainLoop (new FakeMainLoop ());
const int ms = 100;
object token1 = null, token2 = null;
var callbackCount = 0;
Func<bool> callback = () =>
{
callbackCount++;
if (callbackCount == 2)
{
ml.Stop ();
}
return true;
};
Parallel.Invoke (
() => token1 = ml.TimedEvents.Add (TimeSpan.FromMilliseconds (ms), callback),
() => token2 = ml.TimedEvents.Add (TimeSpan.FromMilliseconds (ms), callback)
);
ml.Run ();
Assert.NotNull (token1);
Assert.NotNull (token2);
Assert.True (ml.TimedEvents.Remove (token1));
Assert.True (ml.TimedEvents.Remove (token2));
Assert.Equal (2, callbackCount);
}
[Fact]
public void AddTimer_Remove_NotCalled ()
{
var ml = new MainLoop (new FakeMainLoop ());
TimeSpan ms = TimeSpan.FromMilliseconds (50);
// Force stop if 10 iterations
var stopCount = 0;
Func<bool> fnStop = () =>
{
stopCount++;
if (stopCount == 10)
{
ml.Stop ();
}
return true;
};
ml.TimedEvents.Add (TimeSpan.Zero, fnStop);
var callbackCount = 0;
Func<bool> callback = () =>
{
callbackCount++;
return true;
};
object token = ml.TimedEvents.Add (ms, callback);
Assert.True (ml.TimedEvents.Remove (token));
ml.Run ();
Assert.Equal (0, callbackCount);
}
[Fact]
public void AddTimer_ReturnFalse_StopsBeingCalled ()
{
var ml = new MainLoop (new FakeMainLoop ());
TimeSpan ms = TimeSpan.FromMilliseconds (50);
// Force stop if 10 iterations
var stopCount = 0;
Func<bool> fnStop = () =>
{
Thread.Sleep (10); // Sleep to enable timer to fire
stopCount++;
if (stopCount == 10)
{
ml.Stop ();
}
return true;
};
ml.TimedEvents.Add (TimeSpan.Zero, fnStop);
var callbackCount = 0;
Func<bool> callback = () =>
{
callbackCount++;
return false;
};
object token = ml.TimedEvents.Add (ms, callback);
ml.Run ();
Assert.Equal (1, callbackCount);
Assert.Equal (10, stopCount);
Assert.False (ml.TimedEvents.Remove (token));
}
[Fact]
public void AddTimer_Run_Called ()
{
var ml = new MainLoop (new FakeMainLoop ());
var ms = 100;
var callbackCount = 0;
Func<bool> callback = () =>
{
callbackCount++;
ml.Stop ();
return true;
};
object token = ml.TimedEvents.Add (TimeSpan.FromMilliseconds (ms), callback);
ml.Run ();
Assert.True (ml.TimedEvents.Remove (token));
Assert.Equal (1, callbackCount);
}
[Fact]
public void AddTimer_Run_CalledAtApproximatelyRightTime ()
{
var ml = new MainLoop (new FakeMainLoop ());
TimeSpan ms = TimeSpan.FromMilliseconds (50);
var watch = new Stopwatch ();
var callbackCount = 0;
Func<bool> callback = () =>
{
watch.Stop ();
callbackCount++;
ml.Stop ();
return true;
};
object token = ml.TimedEvents.Add (ms, callback);
watch.Start ();
ml.Run ();
// +/- 100ms should be good enuf
// https://github.com/xunit/assert.xunit/pull/25
Assert.Equal (ms * callbackCount, watch.Elapsed, new MillisecondTolerance (100));
Assert.True (ml.TimedEvents.Remove (token));
Assert.Equal (1, callbackCount);
}
[Fact]
public void AddTimer_Run_CalledTwiceApproximatelyRightTime ()
{
var ml = new MainLoop (new FakeMainLoop ());
TimeSpan ms = TimeSpan.FromMilliseconds (50);
var watch = new Stopwatch ();
var callbackCount = 0;
Func<bool> callback = () =>
{
callbackCount++;
if (callbackCount == 2)
{
watch.Stop ();
ml.Stop ();
}
return true;
};
object token = ml.TimedEvents.Add (ms, callback);
watch.Start ();
ml.Run ();
// +/- 100ms should be good enuf
// https://github.com/xunit/assert.xunit/pull/25
Assert.Equal (ms * callbackCount, watch.Elapsed, new MillisecondTolerance (100));
Assert.True (ml.TimedEvents.Remove (token));
Assert.Equal (2, callbackCount);
}
[Fact]
public void CheckTimersAndIdleHandlers_NoTimers_Returns_False ()
{
var ml = new MainLoop (new FakeMainLoop ());
bool retVal = ml.TimedEvents.CheckTimers(out int waitTimeOut);
Assert.False (retVal);
Assert.Equal (-1, waitTimeOut);
}
[Fact]
public void CheckTimersAndIdleHandlers_NoTimers_WithIdle_Returns_True ()
{
var ml = new MainLoop (new FakeMainLoop ());
Func<bool> fnTrue = () => true;
ml.TimedEvents.Add (TimeSpan.Zero, fnTrue);
bool retVal = ml.TimedEvents.CheckTimers(out int waitTimeOut);
Assert.True (retVal);
Assert.Equal (0, waitTimeOut);
}
[Fact]
public void CheckTimersAndIdleHandlers_With1Timer_Returns_Timer ()
{
var ml = new MainLoop (new FakeMainLoop ());
TimeSpan ms = TimeSpan.FromMilliseconds (50);
static bool Callback () { return false; }
_ = ml.TimedEvents.Add (ms, Callback);
bool retVal = ml.TimedEvents.CheckTimers (out int waitTimeOut);
Assert.True (retVal);
// It should take < 10ms to execute to here
Assert.True (ms.TotalMilliseconds <= waitTimeOut + 10);
}
[Fact]
public void CheckTimersAndIdleHandlers_With2Timers_Returns_Timer ()
{
var ml = new MainLoop (new FakeMainLoop ());
TimeSpan ms = TimeSpan.FromMilliseconds (50);
static bool Callback () { return false; }
_ = ml.TimedEvents.Add (ms, Callback);
_ = ml.TimedEvents.Add (ms, Callback);
bool retVal = ml.TimedEvents.CheckTimers (out int waitTimeOut);
Assert.True (retVal);
// It should take < 10ms to execute to here
Assert.True (ms.TotalMilliseconds <= waitTimeOut + 10);
}
[Fact]
public void False_Idle_Stops_It_Being_Called_Again ()
{
var ml = new MainLoop (new FakeMainLoop ());
var functionCalled = 0;
Func<bool> fn1 = () =>
{
functionCalled++;
if (functionCalled == 10)
{
return false;
}
return true;
};
// Force stop if 20 iterations
var stopCount = 0;
Func<bool> fnStop = () =>
{
stopCount++;
if (stopCount == 20)
{
ml.Stop ();
}
return true;
};
var a = ml.TimedEvents.Add (TimeSpan.Zero, fnStop);
var b = ml.TimedEvents.Add (TimeSpan.Zero, fn1);
ml.Run ();
Assert.True (ml.TimedEvents.Remove (a));
Assert.False (ml.TimedEvents.Remove (a));
Assert.Equal (10, functionCalled);
Assert.Equal (20, stopCount);
}
[Fact]
public void Internal_Tests ()
{
var testMainloop = new TestMainloop ();
var mainloop = new MainLoop (testMainloop);
Assert.Empty (mainloop.TimedEvents.Timeouts);
Assert.NotNull (
new Terminal.Gui.App.Timeout { Span = new (), Callback = () => true }
);
}
[Theory]
[MemberData (nameof (TestAddTimeout))]
public void Mainloop_Invoke_Or_AddTimeout_Can_Be_Used_For_Events_Or_Actions (
Action action,
string pclickMe,
string pcancel,
string ppewPew,
int pzero,
int pone,
int ptwo,
int pthree,
int pfour
)
{
// TODO: Expand this test to test all drivers
Application.Init (null, "fakedriver");
total = 0;
btn = null;
clickMe = pclickMe;
cancel = pcancel;
pewPew = ppewPew;
zero = pzero;
one = pone;
two = ptwo;
three = pthree;
four = pfour;
taskCompleted = false;
var btnLaunch = new Button { Text = "Open Window" };
btnLaunch.Accepting += (s, e) => action ();
var top = new Toplevel ();
top.Add (btnLaunch);
int iterations = -1;
Application.Iteration += (s, a) =>
{
iterations++;
if (iterations == 0)
{
Assert.Null (btn);
Assert.Equal (zero, total);
Assert.False (btnLaunch.NewKeyDownEvent (Key.Space));
if (btn == null)
{
Assert.Null (btn);
Assert.Equal (zero, total);
}
else
{
Assert.Equal (clickMe, btn.Text);
Assert.Equal (four, total);
}
}
else if (iterations == 1)
{
Assert.Equal (clickMe, btn.Text);
Assert.Equal (zero, total);
Assert.False (btn.NewKeyDownEvent (Key.Space));
Assert.Equal (cancel, btn.Text);
Assert.Equal (one, total);
}
else if (taskCompleted)
{
Application.RequestStop ();
}
};
Application.Run (top);
top.Dispose ();
Assert.True (taskCompleted);
Assert.Equal (clickMe, btn.Text);
Assert.Equal (four, total);
Application.Shutdown ();
}
[Fact]
public void RemoveIdle_Function_NotCalled ()
{
var ml = new MainLoop (new FakeMainLoop ());
var functionCalled = 0;
Func<bool> fn = () =>
{
functionCalled++;
return true;
};
Assert.False (ml.TimedEvents.Remove ("flibble"));
ml.RunIteration ();
Assert.Equal (0, functionCalled);
}
[Fact]
public void Run_Runs_Idle_Stop_Stops_Idle ()
{
var ml = new MainLoop (new FakeMainLoop ());
var functionCalled = 0;
Func<bool> fn = () =>
{
functionCalled++;
if (functionCalled == 10)
{
ml.Stop ();
}
return true;
};
var a = ml.TimedEvents.Add (TimeSpan.Zero, fn);
ml.Run ();
Assert.True (ml.TimedEvents.Remove (a));
Assert.Equal (10, functionCalled);
}
[Theory]
[InlineData ("fake")]
[InlineData ("windows")]
[InlineData ("dotnet")]
[InlineData ("unix")]
public void Application_Invoke_Run_TimedEvents (string driverName)
{
// Arrange
Application.Init (driverName: driverName);
var functionCalled = 0;
var stopwatch = new Stopwatch ();
// Act
Application.Invoke (() =>
{
// Stop the stopwatch *after* the function is called.
functionCalled++;
stopwatch.Stop ();
Application.RequestStop ();
});
// Start timing just before running the application loop.
stopwatch.Start ();
Application.Run ();
// Assert
Assert.NotNull (Application.Top);
Application.Top.Dispose ();
Application.Shutdown ();
Assert.Equal (1, functionCalled);
// Output the elapsed time for this test case.
// ReSharper disable once Xunit.XunitTestWithConsoleOutput
// ReSharper disable once LocalizableElement
Console.WriteLine ($"[{driverName}] Duration: {stopwatch.Elapsed.TotalMilliseconds:F2} ms");
// Output elapsed duration to xUnit's test output
_output.WriteLine ($"[{driverName}] Duration: {stopwatch.Elapsed.TotalMilliseconds:F2} ms");
}
[Theory]
[InlineData ("fake")]
[InlineData ("windows")]
[InlineData ("dotnet")]
[InlineData ("unix")]
public void Application_AddTimeout_Run_TimedEvents (string driverName)
{
// Arrange
Application.Init (driverName: driverName);
var functionCalled = 0;
var stopwatch = new Stopwatch ();
// Act
bool Function ()
{
functionCalled++;
if (functionCalled == 10 && Application.Top is { Running: true })
{
stopwatch.Stop ();
Application.RequestStop ();
return false;
}
return true;
}
Application.AddTimeout (TimeSpan.FromMilliseconds (1), Function);
// Start timing just before running the application loop.
stopwatch.Start ();
Application.Run ();
// Assert
Assert.NotNull (Application.Top);
Application.Top.Dispose ();
Application.Shutdown ();
Assert.Equal (10, functionCalled);
// Output the elapsed time for this test case.
// ReSharper disable once Xunit.XunitTestWithConsoleOutput
// ReSharper disable once LocalizableElement
Console.WriteLine ($"[{driverName}] Duration: {stopwatch.Elapsed.TotalMilliseconds:F2} ms");
// Output elapsed duration to xUnit's test output
_output.WriteLine ($"[{driverName}] Duration: {stopwatch.Elapsed.TotalMilliseconds:F2} ms");
}
public static IEnumerable<object []> TestAddTimeout
{
get
{
// Goes fine
Action a1 = StartWindow;
yield return new object [] { a1, "Click Me", "Cancel", "Pew Pew", 0, 1, 2, 3, 4 };
// Also goes fine
Action a2 = () => Application.Invoke (StartWindow);
yield return new object [] { a2, "Click Me", "Cancel", "Pew Pew", 0, 1, 2, 3, 4 };
}
}
private static async void RunAsyncTest (object sender, EventArgs e)
{
Assert.Equal (clickMe, btn.Text);
Assert.Equal (zero, total);
btn.Text = "Cancel";
Interlocked.Increment (ref total);
btn.SetNeedsDraw ();
await Task.Run (
() =>
{
try
{
Assert.Equal (cancel, btn.Text);
Assert.Equal (one, total);
RunSql ();
}
finally
{
SetReadyToRun ();
}
}
)
.ContinueWith (
async (s, e) =>
{
await Task.Delay (1000);
Assert.Equal (clickMe, btn.Text);
Assert.Equal (three, total);
Interlocked.Increment (ref total);
Assert.Equal (clickMe, btn.Text);
Assert.Equal (four, total);
taskCompleted = true;
},
TaskScheduler.FromCurrentSynchronizationContext ()
);
}
private static void RunSql ()
{
Thread.Sleep (100);
Assert.Equal (cancel, btn.Text);
Assert.Equal (one, total);
Application.Invoke (
() =>
{
btn.Text = "Pew Pew";
Interlocked.Increment (ref total);
btn.SetNeedsDraw ();
}
);
}
private static void SetReadyToRun ()
{
Thread.Sleep (100);
Assert.Equal (pewPew, btn.Text);
Assert.Equal (two, total);
Application.Invoke (
() =>
{
btn.Text = "Click Me";
Interlocked.Increment (ref total);
btn.SetNeedsDraw ();
}
);
}
private static void StartWindow ()
{
var startWindow = new Window { Modal = true };
btn = new() { Text = "Click Me" };
btn.Accepting += RunAsyncTest;
var totalbtn = new Button { X = Pos.Right (btn), Text = "total" };
totalbtn.Accepting += (s, e) => { MessageBox.Query ("Count", $"Count is {total}", "Ok"); };
startWindow.Add (btn);
startWindow.Add (totalbtn);
Application.Run (startWindow);
Assert.Equal (clickMe, btn.Text);
Assert.Equal (four, total);
Application.RequestStop ();
}
private class MillisecondTolerance : IEqualityComparer<TimeSpan>
{
public MillisecondTolerance (int tolerance) { _tolerance = tolerance; }
private readonly int _tolerance;
public bool Equals (TimeSpan x, TimeSpan y) { return Math.Abs (x.Milliseconds - y.Milliseconds) <= _tolerance; }
public int GetHashCode (TimeSpan obj) { return obj.GetHashCode (); }
}
private class TestMainloop : IMainLoopDriver
{
private MainLoop mainLoop;
public bool EventsPending () { throw new NotImplementedException (); }
public void Iteration () { throw new NotImplementedException (); }
public void TearDown () { throw new NotImplementedException (); }
public void Setup (MainLoop mainLoop) { this.mainLoop = mainLoop; }
public void Wakeup () { throw new NotImplementedException (); }
}
}

View File

@@ -45,7 +45,6 @@ public class RunStateTests
#endif #endif
Assert.Null (Application.Top); Assert.Null (Application.Top);
// Assert.Null (Application.MainLoop);
Assert.Null (Application.Driver); Assert.Null (Application.Driver);
} }

View File

@@ -50,8 +50,8 @@ public class ConsoleDriverTests
public void Init_Inits (Type driverType) public void Init_Inits (Type driverType)
{ {
var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
MainLoop ml = driver.Init (); driver.Init ();
Assert.NotNull (ml); // Note: MainLoop is no longer returned from Init() as part of legacy MainLoop removal
Assert.NotNull (driver.Clipboard); Assert.NotNull (driver.Clipboard);
Console.ForegroundColor = ConsoleColor.Red; Console.ForegroundColor = ConsoleColor.Red;
Assert.Equal (ConsoleColor.Red, Console.ForegroundColor); Assert.Equal (ConsoleColor.Red, Console.ForegroundColor);

View File

@@ -12,7 +12,6 @@ public class AllViewsDrawTests (ITestOutputHelper output) : TestsAllViews
{ {
Application.ResetState (true); Application.ResetState (true);
// Required for spinner view that wants to register timeouts // Required for spinner view that wants to register timeouts
Application.MainLoop = new MainLoop (new FakeMainLoop (Application.Driver));
var view = (View)CreateInstanceIfNotGeneric (viewType); var view = (View)CreateInstanceIfNotGeneric (viewType);

View File

@@ -12,7 +12,6 @@ public class LayoutTests (ITestOutputHelper output) : TestsAllViews
{ {
// Required for spinner view that wants to register timeouts // Required for spinner view that wants to register timeouts
Application.MainLoop = new MainLoop (new FakeMainLoop (Application.Driver));
var view = (View)CreateInstanceIfNotGeneric (viewType); var view = (View)CreateInstanceIfNotGeneric (viewType);

View File

@@ -14,7 +14,6 @@ public class AllViewsTests (ITestOutputHelper output) : TestsAllViews
public void AllViews_Center_Properly (Type viewType) public void AllViews_Center_Properly (Type viewType)
{ {
// Required for spinner view that wants to register timeouts // Required for spinner view that wants to register timeouts
Application.MainLoop = new (new FakeMainLoop (Application.Driver));
var view = CreateInstanceIfNotGeneric (viewType); var view = CreateInstanceIfNotGeneric (viewType);

View File

@@ -623,7 +623,6 @@ public class MenuBarv1Tests (ITestOutputHelper output)
Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5), Flags = MouseFlags.Button1Clicked }); Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5), Flags = MouseFlags.Button1Clicked });
// Need to fool MainLoop into thinking it's running // Need to fool MainLoop into thinking it's running
Application.MainLoop.Running = true;
AutoInitShutdownAttribute.RunIteration (); AutoInitShutdownAttribute.RunIteration ();
Assert.Equal (items [0], menu.Menus [0].Title); Assert.Equal (items [0], menu.Menus [0].Title);
@@ -815,7 +814,6 @@ public class MenuBarv1Tests (ITestOutputHelper output)
Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5), Flags = MouseFlags.Button1Clicked }); Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5), Flags = MouseFlags.Button1Clicked });
// Need to fool MainLoop into thinking it's running // Need to fool MainLoop into thinking it's running
Application.MainLoop.Running = true;
AutoInitShutdownAttribute.RunIteration (); AutoInitShutdownAttribute.RunIteration ();
Assert.Equal (items [0], menu.Menus [0].Title); Assert.Equal (items [0], menu.Menus [0].Title);

View File

@@ -1,300 +0,0 @@
using Xunit.Abstractions;
// Alias Console to MockConsole so we don't accidentally use Console
namespace UnitTests_Parallelizable.DriverTests;
public class MainLoopDriverTests : UnitTests.Parallelizable.ParallelizableBase
{
public MainLoopDriverTests (ITestOutputHelper output) { ConsoleDriver.RunningUnitTests = true; }
[Theory]
[InlineData (typeof (FakeDriver), typeof (FakeMainLoop))]
//[InlineData (typeof (DotNetDriver), typeof (NetMainLoop))]
//[InlineData (typeof (UnixDriver), typeof (UnixMainLoop))]
//[InlineData (typeof (WindowsDriver), typeof (WindowsMainLoop))]
//[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
public void MainLoop_AddTimeout_ValidIdleHandler_ReturnsToken (Type driverType, Type mainLoopDriverType)
{
var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
var mainLoop = new MainLoop (mainLoopDriver);
var idleHandlerInvoked = false;
bool IdleHandler ()
{
idleHandlerInvoked = true;
return false;
}
var token = mainLoop.TimedEvents.Add(TimeSpan.Zero, IdleHandler);
Assert.NotNull (token);
Assert.False (idleHandlerInvoked); // Idle handler should not be invoked immediately
mainLoop.RunIteration (); // Run an iteration to process the idle handler
Assert.True (idleHandlerInvoked); // Idle handler should be invoked after processing
mainLoop.Dispose ();
}
[Theory]
[InlineData (typeof (FakeDriver), typeof (FakeMainLoop))]
//[InlineData (typeof (DotNetDriver), typeof (NetMainLoop))]
//[InlineData (typeof (UnixDriver), typeof (UnixMainLoop))]
//[InlineData (typeof (WindowsDriver), typeof (WindowsMainLoop))]
//[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
public void MainLoop_AddTimeout_ValidParameters_ReturnsToken (Type driverType, Type mainLoopDriverType)
{
var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
var mainLoop = new MainLoop (mainLoopDriver);
var callbackInvoked = false;
object token = mainLoop.TimedEvents.Add (
TimeSpan.FromMilliseconds (100),
() =>
{
callbackInvoked = true;
return false;
}
);
Assert.NotNull (token);
mainLoop.RunIteration (); // Run an iteration to process the timeout
Assert.False (callbackInvoked); // Callback should not be invoked immediately
Thread.Sleep (200); // Wait for the timeout
mainLoop.RunIteration (); // Run an iteration to process the timeout
Assert.True (callbackInvoked); // Callback should be invoked after the timeout
mainLoop.Dispose ();
}
[Theory]
[InlineData (typeof (FakeDriver), typeof (FakeMainLoop))]
//[InlineData (typeof (DotNetDriver), typeof (NetMainLoop))]
//[InlineData (typeof (UnixDriver), typeof (UnixMainLoop))]
//[InlineData (typeof (WindowsDriver), typeof (WindowsMainLoop))]
//[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
public void MainLoop_CheckTimersAndIdleHandlers_IdleHandlersActive_ReturnsTrue (
Type driverType,
Type mainLoopDriverType
)
{
var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
var mainLoop = new MainLoop (mainLoopDriver);
mainLoop.TimedEvents.Add (TimeSpan.Zero, () => false);
bool result = mainLoop.TimedEvents.CheckTimers (out int waitTimeout);
Assert.True (result);
Assert.Equal (0, waitTimeout);
mainLoop.Dispose ();
}
[Theory]
[InlineData (typeof (FakeDriver), typeof (FakeMainLoop))]
//[InlineData (typeof (DotNetDriver), typeof (NetMainLoop))]
//[InlineData (typeof (UnixDriver), typeof (UnixMainLoop))]
//[InlineData (typeof (WindowsDriver), typeof (WindowsMainLoop))]
//[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
public void MainLoop_CheckTimers_NoTimersOrIdleHandlers_ReturnsFalse (
Type driverType,
Type mainLoopDriverType
)
{
var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
var mainLoop = new MainLoop (mainLoopDriver);
bool result = mainLoop.TimedEvents.CheckTimers (out int waitTimeout);
Assert.False (result);
Assert.Equal (-1, waitTimeout);
mainLoop.Dispose ();
}
[Theory]
[InlineData (typeof (FakeDriver), typeof (FakeMainLoop))]
//[InlineData (typeof (DotNetDriver), typeof (NetMainLoop))]
//[InlineData (typeof (UnixDriver), typeof (UnixMainLoop))]
//[InlineData (typeof (WindowsDriver), typeof (WindowsMainLoop))]
//[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
public void MainLoop_CheckTimersAndIdleHandlers_TimersActive_ReturnsTrue (
Type driverType,
Type mainLoopDriverType
)
{
var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
var mainLoop = new MainLoop (mainLoopDriver);
mainLoop.TimedEvents.Add (TimeSpan.FromMilliseconds (100), () => false);
bool result = mainLoop.TimedEvents.CheckTimers(out int waitTimeout);
Assert.True (result);
Assert.True (waitTimeout >= 0);
mainLoop.Dispose ();
}
[Theory]
[InlineData (typeof (FakeDriver), typeof (FakeMainLoop))]
//[InlineData (typeof (DotNetDriver), typeof (NetMainLoop))]
//[InlineData (typeof (UnixDriver), typeof (UnixMainLoop))]
//[InlineData (typeof (WindowsDriver), typeof (WindowsMainLoop))]
//[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
public void MainLoop_Constructs_Disposes (Type driverType, Type mainLoopDriverType)
{
var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
var mainLoop = new MainLoop (mainLoopDriver);
// Check default values
Assert.NotNull (mainLoop);
Assert.Equal (mainLoopDriver, mainLoop.MainLoopDriver);
Assert.Empty (mainLoop.TimedEvents.Timeouts);
Assert.False (mainLoop.Running);
// Clean up
mainLoop.Dispose ();
// TODO: It'd be nice if we could really verify IMainLoopDriver.TearDown was called
// and that it was actually cleaned up.
Assert.Null (mainLoop.MainLoopDriver);
Assert.Empty (mainLoop.TimedEvents.Timeouts);
Assert.False (mainLoop.Running);
}
[Theory]
[InlineData (typeof (FakeDriver), typeof (FakeMainLoop))]
//[InlineData (typeof (DotNetDriver), typeof (NetMainLoop))]
//[InlineData (typeof (UnixDriver), typeof (UnixMainLoop))]
//[InlineData (typeof (WindowsDriver), typeof (WindowsMainLoop))]
//[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
public void MainLoop_RemoveIdle_InvalidToken_ReturnsFalse (Type driverType, Type mainLoopDriverType)
{
var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
var mainLoop = new MainLoop (mainLoopDriver);
bool result = mainLoop.TimedEvents.Remove("flibble");
Assert.False (result);
mainLoop.Dispose ();
}
[Theory]
[InlineData (typeof (FakeDriver), typeof (FakeMainLoop))]
//[InlineData (typeof (DotNetDriver), typeof (NetMainLoop))]
//[InlineData (typeof (UnixDriver), typeof (UnixMainLoop))]
//[InlineData (typeof (WindowsDriver), typeof (WindowsMainLoop))]
//[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
public void MainLoop_RemoveIdle_ValidToken_ReturnsTrue (Type driverType, Type mainLoopDriverType)
{
var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
var mainLoop = new MainLoop (mainLoopDriver);
bool IdleHandler () { return false; }
var token = mainLoop.TimedEvents.Add (TimeSpan.Zero, IdleHandler);
bool result = mainLoop.TimedEvents.Remove (token);
Assert.True (result);
mainLoop.Dispose ();
}
[Theory]
[InlineData (typeof (FakeDriver), typeof (FakeMainLoop))]
//[InlineData (typeof (DotNetDriver), typeof (NetMainLoop))]
//[InlineData (typeof (UnixDriver), typeof (UnixMainLoop))]
//[InlineData (typeof (WindowsDriver), typeof (WindowsMainLoop))]
//[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
public void MainLoop_RemoveTimeout_InvalidToken_ReturnsFalse (Type driverType, Type mainLoopDriverType)
{
var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
var mainLoop = new MainLoop (mainLoopDriver);
bool result = mainLoop.TimedEvents.Remove (new object ());
Assert.False (result);
}
[Theory]
[InlineData (typeof (FakeDriver), typeof (FakeMainLoop))]
//[InlineData (typeof (DotNetDriver), typeof (NetMainLoop))]
//[InlineData (typeof (UnixDriver), typeof (UnixMainLoop))]
//[InlineData (typeof (WindowsDriver), typeof (WindowsMainLoop))]
//[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
public void MainLoop_RemoveTimeout_ValidToken_ReturnsTrue (Type driverType, Type mainLoopDriverType)
{
var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
var mainLoop = new MainLoop (mainLoopDriver);
object token = mainLoop.TimedEvents.Add (TimeSpan.FromMilliseconds (100), () => false);
bool result = mainLoop.TimedEvents.Remove (token);
Assert.True (result);
mainLoop.Dispose ();
}
[Theory]
[InlineData (typeof (FakeDriver), typeof (FakeMainLoop))]
//[InlineData (typeof (DotNetDriver), typeof (NetMainLoop))]
//[InlineData (typeof (UnixDriver), typeof (UnixMainLoop))]
//[InlineData (typeof (WindowsDriver), typeof (WindowsMainLoop))]
//[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
public void MainLoop_RunIteration_ValidIdleHandler_CallsIdleHandler (Type driverType, Type mainLoopDriverType)
{
var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
var mainLoop = new MainLoop (mainLoopDriver);
var idleHandlerInvoked = false;
Func<bool> idleHandler = () =>
{
idleHandlerInvoked = true;
return false;
};
mainLoop.TimedEvents.Add (TimeSpan.Zero, idleHandler);
mainLoop.RunIteration (); // Run an iteration to process the idle handler
Assert.True (idleHandlerInvoked);
mainLoop.Dispose ();
}
//[Theory]
//[InlineData (typeof (FakeDriver), typeof (FakeMainLoop))]
////[InlineData (typeof (DotNetDriver), typeof (NetMainLoop))]
////[InlineData (typeof (UnixDriver), typeof (UnixMainLoop))]
////[InlineData (typeof (WindowsDriver), typeof (WindowsMainLoop))]
//public void MainLoop_Invoke_ValidAction_RunsAction (Type driverType, Type mainLoopDriverType)
//{
// var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
// var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, new object [] { driver });
// var mainLoop = new MainLoop (mainLoopDriver);
// var actionInvoked = false;
// mainLoop.Invoke (() => { actionInvoked = true; });
// mainLoop.RunIteration (); // Run an iteration to process the action.
// Assert.True (actionInvoked);
// mainLoop.Dispose ();
//}
}

View File

@@ -155,7 +155,7 @@ internal class MockConsoleDriver : IConsoleDriver
public void UpdateCursor () {} public void UpdateCursor () {}
/// <inheritdoc /> /// <inheritdoc />
public MainLoop Init () { return null!; } public void Init () { }
/// <inheritdoc /> /// <inheritdoc />
public void End () { } public void End () { }

View File

@@ -47,7 +47,6 @@ public class GlobalTestSetup : IDisposable
// Don't check Application.Force16Colors // Don't check Application.Force16Colors
//Assert.False (Application.Force16Colors); //Assert.False (Application.Force16Colors);
Assert.Null (Application.Driver); Assert.Null (Application.Driver);
Assert.Null (Application.MainLoop);
Assert.False (Application.EndAfterFirstIteration); Assert.False (Application.EndAfterFirstIteration);
Assert.Equal (Key.Tab.WithShift, Application.PrevTabKey); Assert.Equal (Key.Tab.WithShift, Application.PrevTabKey);
Assert.Equal (Key.Tab, Application.NextTabKey); Assert.Equal (Key.Tab, Application.NextTabKey);

View File

@@ -291,6 +291,8 @@ Application.Init();
Terminal.Gui v1 drivers that implement `IConsoleDriver` but not `IConsoleDriverFacade` are still supported through a legacy compatibility layer. However, they do not benefit from the v2 architecture improvements (multi-threading, component separation, etc.). Terminal.Gui v1 drivers that implement `IConsoleDriver` but not `IConsoleDriverFacade` are still supported through a legacy compatibility layer. However, they do not benefit from the v2 architecture improvements (multi-threading, component separation, etc.).
**Note**: The legacy `MainLoop` infrastructure (including the `MainLoop` class, `IMainLoopDriver` interface, and `FakeMainLoop`) has been removed in favor of the modern architecture. All drivers now use the `MainLoopCoordinator` and `ApplicationMainLoop` system exclusively.
## See Also ## See Also
- @Terminal.Gui.Drivers - API Reference - @Terminal.Gui.Drivers - API Reference

View File

@@ -456,17 +456,26 @@ Additionally, the `Toggle` event was renamed `CheckStateChanging` and made cance
+cb.AdvanceCheckState (); +cb.AdvanceCheckState ();
``` ```
## `MainLoop` is no longer accessible from `Application` ## `MainLoop` has been removed from `Application`
In v1, you could add timeouts via `Application.MainLoop.AddTimeout` among other things. In v2, the `MainLoop` object is internal to `Application` and methods previously accessed via `MainLoop` can now be accessed directly via `Application` In v1, you could add timeouts via `Application.MainLoop.AddTimeout` and access the `MainLoop` object directly. In v2, the legacy `MainLoop` class has been completely removed as part of the architectural modernization. Timeout functionality and other features previously accessed via `MainLoop` are now available directly through `Application` or `ApplicationImpl`.
### How to Fix ### How to Fix
Replace any `Application.MainLoop` references:
```diff ```diff
- Application.MainLoop.AddTimeout (TimeSpan time, Func<MainLoop, bool> callback) - Application.MainLoop.AddTimeout (TimeSpan time, Func<MainLoop, bool> callback)
+ Application.AddTimeout (TimeSpan time, Func<bool> callback) + Application.AddTimeout (TimeSpan time, Func<bool> callback)
``` ```
```diff
- Application.MainLoop.Wakeup ()
+ // No replacement needed - wakeup is handled automatically by the modern architecture
```
**Note**: The legacy `MainLoop` infrastructure (including `IMainLoopDriver` and `FakeMainLoop`) has been removed. The modern v2 architecture uses `ApplicationImpl`, `MainLoopCoordinator`, and `ApplicationMainLoop` instead.
## `SendSubViewXXX` renamed and corrected ## `SendSubViewXXX` renamed and corrected
In v1, the `View` methods to move SubViews within the SubViews list were poorly named and actually operated in reverse of what their names suggested. In v1, the `View` methods to move SubViews within the SubViews list were poorly named and actually operated in reverse of what their names suggested.