Files
Terminal.Gui/UnitTests/Application/MainLoopTests.cs
Tig dcb3b359ad Fixes #2926 - Refactor KeyEvent and KeyEventEventArgs to simplify (#2927)
* Adds basic MainLoop unit tests

* Remove WinChange action from Curses

* Remove WinChange action from Curses

* Remove ProcessInput action from Windows MainLoop

* Simplified MainLoop/ConsoleDriver by making MainLoop internal and moving impt fns to Application

* Modernized Terminal resize events

* Modernized Terminal resize events

* Removed un used property

* for _isWindowsTerminal devenv->wininit; not sure what changed

* Modernized mouse/keyboard events (Action->EventHandler)

* Updated OnMouseEvent API docs

* Using WT_SESSION to detect WT

* removes hacky GetParentProcess

* Updates to fix #2634 (clear last line)

* removes hacky GetParentProcess2

* Addressed mac resize issue

* Addressed mac resize issue

* Removes ConsoleDriver.PrepareToRun, has Init return MainLoop

* Removes unneeded Attribute methods

* Removed GetProcesssName

* Removed GetProcesssName

* Refactored KeyEvent and KeyEventEventArgs into a single class

* Revert "Refactored KeyEvent and KeyEventEventArgs into a single class"

This reverts commit 88a00658db.

* Fixed key repeat issue; reverted stupidity on 1049/1047 confusion

* Updated CSI API Docs

* merge

* Rearranged Event.cs to Keyboard.cs and Mouse.cs

* Renamed KeyEventEventArgs KeyEventArgs

* temp renamed KeyEvent OldKeyEvent

* Merged KeyEvent into KeyEventArgs

* Renamed Application.ProcessKey members

* Renamed Application.ProcessKey members

* Renamed Application.ProcessKey members

* Added Responder.KeyPressed

* Removed unused references

* Fixed arg naming

* InvokeKeybindings->InvokeKeyBindings

* InvokeKeybindings->InvokeKeyBindings

* Fixed unit tests fail

* More progress on refactoring key input; still broken and probably wrong

* Moved OnKeyPressed out of Responder and made ProcessKeyPrssed non-virtual

* Updated API docs

* Moved key handling from Responder to View

* Updated API docs

* Updated HotKey API docs

* Updated shortcut API docs

* Fixed responder unit tests

* Removed Shortcut from View as it is not used

* Removed unneeded OnHotKey override from Button

* Fixed BackTab logic

* Button now uses Key Bindings exclusively

* Button now uses Key Bindings exclusively

* Updated keyboard.md docs

* Fixed unit tests to account for Toplevel handling default button

* Added View.InvokeCommand API

* Modernized RadioGroup

* Removed ColdKey

* Modernized (partially) StatusBar

* Worked around FileDialog issue with Ctrl-F

* Fixed driver unit test; view must be focused to reciev key pressed

* Application code cleanup

* Start on updaing menu

* Menu now mostly works

* Menu Select refinement

* Fixed known menu bugs!

* Enabled HotKey to cause focus- experimental

* Fixes #3022 & adds unit test to prove it

* Actually Fixes #3022 & adds unit test to prove it

* Working through hotkey issues

* Misc fixes

* removed hot/cold key stuff from Keys scenario

* Fixed scenarios

* Simplified shortcut string handling

* Modernized Checkbox

* Modernized TileView

* Updated API docs

* Updated API docs

* attempting to publish v2 docs

* Revert "attempting to publish v2 docs"

This reverts commit 59dcec111b.

* Playing with api docs

* Removed Key.BackTab

* Removed Caps/Scroll/Numlock

* Partial removal of keymodifiers - unit tests pass

* Partial removal of keymodifiers - broke netdriver somewhere

* WindowsDriver & added KeyEventArgsTests

* Fixing menu shortcut/hotkeys - broke Menu.cs into separate files

* Fixed MenuBar!

* Finished modernizing Menu/MenuBar

* Removed Key.a-z. Broke lots of stuff

* checkout@v4

* progress on key mapping and formatting

* VK tests are still failing

* Fixed some unit tests

* Added Hotkey and Keybinding unit tests

* fixed unit test

* All unit tests pass again...

* Fixed broken unit tests

* KeyEventArgs.KeyValue -> AsRune

* Fixed bugs. Still some broken

* Added KeyEventArgs.IsAlpha. Added KeyEventArgs.cast ops. Fixed bugs. Unit tests pass

* Fixed WindowsDriver

* Oops.

* Refactoring based on bdisp's help. Not complete!

* removed calling into subviews from OnKeyBindings

* removed calling into subviews from OnKeyBindings

* Improved View KeyEvent unit tests

* More hotkey unit tests

* BIg change - Got rid of KeyPress w/in Application/Drivers

* Unit tests now pass again

* Refreshed API docs

* Better HotKey logic. More progress. Getting close.

* Fixed handling of shifted chars like ö

* Minor code cleanup

* Minor code cleanup2

* Why is build Action failing?

* Why is build Action failing??

* upgraded to .net8 to try to fix weird CI/CD build errors

* upgraded to .net8 to try to fix weird CI/CD build errors2

* Disabling TextViewTests to diagnose build errors

* reenable TextViewTests to diagnose build errors

* Arrrrrrg

* Merged v2_develop

* Fixed uppercase accented keys in WindowsDriver

* Fixed key binding api docs

* Experimental impl of CommandScope.SubViews for MenuBar

* Removed dead code from application.cs

* Removed dead code from application.cs

* Removed dead code from ConsoleDriver.cs

* Cleaned up some key binding stuff

* Disabled Alt to activate menu for now

* Updated label commands

* Fixed menu bugs. Upgraded menu unit tests

* Fixed unit tests

* Working on NetDriver

* fixed netdriver

* Fixed issues called out by @bdisp CR

* fixed CursesDriver

* added todo to netdriver

* Cherry picked treeview test fix 1b415e5

* Fix NetDriver.

* CommandScope->KeyBindingScope

* Address some tznind feedback

* Refactored KeyBindings big time!

* Added key consts to KeyEventArgs and renamed Key to ConsoleDriverKey

* Fixed some API docs

* Moved ConsoleDriverKey to ConsoleDriver.cs

* Renamed Key->ConsoleDriverKey

* Renamed Key->ConsoleDriverKey

* Renamed Key->ConsoleDriverKey

* renamed file I forgot to rename before

* Updated name and API docs of KeyEventArgs.isAlpha

* Fixed issues with OnKeyUp not doing the right thing.

* Fixed MainLoop.Running never being used

* Fixed MainLoop.Running never being used - unit tests

* Claned up BUGBUG comments

* Disabled a unit test to see why ci/cd tests are failing

* Removed defunct commented code

* Removed more defunct commented code

* Re-eanbled unit test; jsut removing one test case...

* Disabled more...

* Renambed Global->Applicaton and updated scope API docs

* Disabled more unit tests...

* Removed dead code

* Disabled more unit tests...2

* Disabled more unit tests...3

* Renambed Global->Applicaton and updated scope API docs 2

* Added more KeyBinding scope tests

* Added more KeyBinding scope tests2

* ConsoleDriverKey too long. Key too ambiguous. Settled on KeyCode. (Partialy because eventually I want to intro a class named Key).

* KeyEventArgs improvements. cast to Rune must be explicit as it's lossy

* Fixed warnings

* Renamed KeyEventArgs to Key... progress on fixing broken stuff that resulted

* Fix ConsoleKeyMapping bugs.

* Fix NetDriver issue from converting a lower case to a upper case.

* Started migration to Key from KeyCode - e.g. made HotKeys all consistent.

* Fixed build warnings

* Added key defns to Key

* KeyBindings now uses Key vs. KeyCode

* Verified by tweaking UICatalog

* Fixed treeview test ... again

* Renamed ProcessKeyDown/Up to NewKeyDown/Up and OnKeyPressed to OnProcessKeyDown to make things more clear

* Added test AllViews_KeyDown_All_EventsFire unit tests and fixed a few Views that were wrong

* fixed stupid KeyUp event bug

* If key not handled, return false for datefield

* dotnet test --no-restore --verbosity diag

* dotnet test --blame

* run tests on windows

* Fix TestVKPacket unit test and move it to ConsoleKeyMappingTests.cs file.

* Remove unnecessary commented code.

* Tweaked unit tests and removed Key.BareKey

* Fixed little details and updated api docs

* updated api docs

* AddKeyBindingsForHotKey: KeyCode->Key

* Cleaned up more old KeyCode usages. Added TODOs

---------

Co-authored-by: BDisp <bd.bdisp@gmail.com>
2023-12-16 12:04:23 -07:00

814 lines
19 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
// Alias Console to MockConsole so we don't accidentally use Console
namespace Terminal.Gui.ApplicationTests;
/// <summary>
/// Tests MainLoop using the FakeMainLoop.
/// </summary>
public class MainLoopTests {
// See Also ConsoleDRivers/MainLoopDriverTests.cs for tests of the MainLoopDriver
// Idle Handler tests
[Fact]
public void AddIdle_Adds_And_Removes ()
{
var ml = new MainLoop (new FakeMainLoop ());
Func<bool> fnTrue = () => true;
Func<bool> fnFalse = () => false;
ml.AddIdle (fnTrue);
ml.AddIdle (fnFalse);
Assert.Equal (2, ml.IdleHandlers.Count);
Assert.Equal (fnTrue, ml.IdleHandlers [0]);
Assert.NotEqual (fnFalse, ml.IdleHandlers [0]);
Assert.True (ml.RemoveIdle (fnTrue));
Assert.Single (ml.IdleHandlers);
// 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.RemoveIdle (fnTrue));
Assert.True (ml.RemoveIdle (fnFalse));
// 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.RemoveIdle (fnFalse));
// Add again, but with dupe
ml.AddIdle (fnTrue);
ml.AddIdle (fnTrue);
Assert.Equal (2, ml.IdleHandlers.Count);
Assert.Equal (fnTrue, ml.IdleHandlers [0]);
Assert.True (ml.IdleHandlers [0] ());
Assert.Equal (fnTrue, ml.IdleHandlers [1]);
Assert.True (ml.IdleHandlers [1] ());
Assert.True (ml.RemoveIdle (fnTrue));
Assert.Single (ml.IdleHandlers);
Assert.Equal (fnTrue, ml.IdleHandlers [0]);
Assert.NotEqual (fnFalse, ml.IdleHandlers [0]);
Assert.True (ml.RemoveIdle (fnTrue));
Assert.Empty (ml.IdleHandlers);
// 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.RemoveIdle (fnTrue));
}
[Fact]
public void AddIdle_Function_GetsCalled_OnIteration ()
{
var ml = new MainLoop (new FakeMainLoop ());
var functionCalled = 0;
Func<bool> fn = () => {
functionCalled++;
return true;
};
ml.AddIdle (fn);
ml.RunIteration ();
Assert.Equal (1, functionCalled);
}
[Fact]
public void RemoveIdle_Function_NotCalled ()
{
var ml = new MainLoop (new FakeMainLoop ());
var functionCalled = 0;
Func<bool> fn = () => {
functionCalled++;
return true;
};
Assert.False (ml.RemoveIdle (fn));
ml.RunIteration ();
Assert.Equal (0, functionCalled);
}
[Fact]
public void AddThenRemoveIdle_Function_NotCalled ()
{
var ml = new MainLoop (new FakeMainLoop ());
var functionCalled = 0;
Func<bool> fn = () => {
functionCalled++;
return true;
};
ml.AddIdle (fn);
Assert.True (ml.RemoveIdle (fn));
ml.RunIteration ();
Assert.Equal (0, functionCalled);
}
[Fact]
public void AddIdleTwice_Function_CalledTwice ()
{
var ml = new MainLoop (new FakeMainLoop ());
var functionCalled = 0;
Func<bool> fn = () => {
functionCalled++;
return true;
};
ml.AddIdle (fn);
ml.AddIdle (fn);
ml.RunIteration ();
Assert.Equal (2, functionCalled);
Assert.Equal (2, ml.IdleHandlers.Count);
functionCalled = 0;
Assert.True (ml.RemoveIdle (fn));
Assert.Single (ml.IdleHandlers);
ml.RunIteration ();
Assert.Equal (1, functionCalled);
functionCalled = 0;
Assert.True (ml.RemoveIdle (fn));
Assert.Empty (ml.IdleHandlers);
ml.RunIteration ();
Assert.Equal (0, functionCalled);
Assert.False (ml.RemoveIdle (fn));
}
[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;
};
ml.AddIdle (fnStop);
ml.AddIdle (fn1);
ml.Run ();
Assert.True (ml.RemoveIdle (fnStop));
Assert.False (ml.RemoveIdle (fn1));
Assert.Equal (10, functionCalled);
Assert.Equal (20, stopCount);
}
[Fact]
public void AddIdle_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;
};
ml.AddIdle (fnStop);
ml.AddIdle (fn1);
ml.AddIdle (fn1);
ml.Run ();
Assert.True (ml.RemoveIdle (fnStop));
Assert.False (ml.RemoveIdle (fn1));
Assert.False (ml.RemoveIdle (fn1));
Assert.Equal (2, 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;
};
ml.AddIdle (fn);
ml.Run ();
Assert.True (ml.RemoveIdle (fn));
Assert.Equal (10, 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;
};
var token = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback);
Assert.True (ml.RemoveTimeout (token));
// BUGBUG: This should probably fault?
// Must return a boolean.
Assert.False (ml.RemoveTimeout (token));
}
// Timeout Handler Tests
[Fact]
public void AddTimer_EventFired ()
{
var ml = new MainLoop (new FakeMainLoop ());
var ms = 100;
var originTicks = DateTime.UtcNow.Ticks;
var callbackCount = 0;
Func<bool> callback = () => {
callbackCount++;
return true;
};
object sender = null;
TimeoutEventArgs args = null;
ml.TimeoutAdded += (s, e) => {
sender = s;
args = e;
};
var token = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback);
Assert.Same (ml, sender);
Assert.NotNull (args.Timeout);
Assert.True (args.Ticks - originTicks >= 100 * TimeSpan.TicksPerMillisecond);
}
[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;
};
var token = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback);
ml.Run ();
Assert.True (ml.RemoveTimeout (token));
Assert.Equal (1, callbackCount);
}
[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.AddTimeout (TimeSpan.FromMilliseconds (ms), callback));
var task2 = new Task (() => token2 = ml.AddTimeout (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.RemoveTimeout (token1));
Assert.True (ml.RemoveTimeout (token2));
Assert.Equal (2, callbackCount);
}
[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.AddTimeout (TimeSpan.FromMilliseconds (ms), callback),
() => token2 = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback)
);
ml.Run ();
Assert.NotNull (token1);
Assert.NotNull (token2);
Assert.True (ml.RemoveTimeout (token1));
Assert.True (ml.RemoveTimeout (token2));
Assert.Equal (2, callbackCount);
}
class MillisecondTolerance : IEqualityComparer<TimeSpan> {
int _tolerance = 0;
public MillisecondTolerance (int tolerance) { _tolerance = tolerance; }
public bool Equals (TimeSpan x, TimeSpan y) => Math.Abs (x.Milliseconds - y.Milliseconds) <= _tolerance;
public int GetHashCode (TimeSpan obj) => obj.GetHashCode ();
}
[Fact]
public void AddTimer_Run_CalledAtApproximatelyRightTime ()
{
var ml = new MainLoop (new FakeMainLoop ());
var ms = TimeSpan.FromMilliseconds (50);
var watch = new System.Diagnostics.Stopwatch ();
var callbackCount = 0;
Func<bool> callback = () => {
watch.Stop ();
callbackCount++;
ml.Stop ();
return true;
};
var token = ml.AddTimeout (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.RemoveTimeout (token));
Assert.Equal (1, callbackCount);
}
[Fact]
public void AddTimer_Run_CalledTwiceApproximatelyRightTime ()
{
var ml = new MainLoop (new FakeMainLoop ());
var ms = TimeSpan.FromMilliseconds (50);
var watch = new System.Diagnostics.Stopwatch ();
var callbackCount = 0;
Func<bool> callback = () => {
callbackCount++;
if (callbackCount == 2) {
watch.Stop ();
ml.Stop ();
}
return true;
};
var token = ml.AddTimeout (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.RemoveTimeout (token));
Assert.Equal (2, callbackCount);
}
[Fact]
public void AddTimer_Remove_NotCalled ()
{
var ml = new MainLoop (new FakeMainLoop ());
var ms = TimeSpan.FromMilliseconds (50);
// Force stop if 10 iterations
var stopCount = 0;
Func<bool> fnStop = () => {
stopCount++;
if (stopCount == 10) ml.Stop ();
return true;
};
ml.AddIdle (fnStop);
var callbackCount = 0;
Func<bool> callback = () => {
callbackCount++;
return true;
};
var token = ml.AddTimeout (ms, callback);
Assert.True (ml.RemoveTimeout (token));
ml.Run ();
Assert.Equal (0, callbackCount);
}
[Fact]
public void AddTimer_ReturnFalse_StopsBeingCalled ()
{
var ml = new MainLoop (new FakeMainLoop ());
var 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.AddIdle (fnStop);
var callbackCount = 0;
Func<bool> callback = () => {
callbackCount++;
return false;
};
var token = ml.AddTimeout (ms, callback);
ml.Run ();
Assert.Equal (1, callbackCount);
Assert.Equal (10, stopCount);
Assert.False (ml.RemoveTimeout (token));
}
[Fact]
public void CheckTimersAndIdleHandlers_NoTimers_Returns_False ()
{
var ml = new MainLoop (new FakeMainLoop ());
var retVal = ml.CheckTimersAndIdleHandlers (out var 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.AddIdle (fnTrue);
var retVal = ml.CheckTimersAndIdleHandlers (out var waitTimeOut);
Assert.True (retVal);
Assert.Equal (-1, waitTimeOut);
}
[Fact]
public void CheckTimersAndIdleHandlers_With1Timer_Returns_Timer ()
{
var ml = new MainLoop (new FakeMainLoop ());
var ms = TimeSpan.FromMilliseconds (50);
static bool Callback () => false;
_ = ml.AddTimeout (ms, Callback);
var retVal = ml.CheckTimersAndIdleHandlers (out var 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 ());
var ms = TimeSpan.FromMilliseconds (50);
static bool Callback () => false;
_ = ml.AddTimeout (ms, Callback);
_ = ml.AddTimeout (ms, Callback);
var retVal = ml.CheckTimersAndIdleHandlers (out var waitTimeOut);
Assert.True (retVal);
// It should take < 10ms to execute to here
Assert.True (ms.TotalMilliseconds <= (waitTimeOut + 10));
}
[Fact]
public void Internal_Tests ()
{
var testMainloop = new TestMainloop ();
var mainloop = new MainLoop (testMainloop);
Assert.Empty (mainloop._timeouts);
Assert.Empty (mainloop._idleHandlers);
Assert.NotNull (new Timeout () {
Span = new TimeSpan (),
Callback = () => true
});
}
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 ();
}
}
// TODO: EventsPending tests
// - wait = true
// - wait = false
// TODO: Add IMainLoop tests
volatile static int tbCounter = 0;
static ManualResetEventSlim _wakeUp = new ManualResetEventSlim (false);
private static void Launch (Random r, TextField tf, int target)
{
Task.Run (() => {
Thread.Sleep (r.Next (2, 4));
Application.Invoke (() => {
tf.Text = $"index{r.Next ()}";
Interlocked.Increment (ref tbCounter);
if (target == tbCounter) {
// On last increment wake up the check
_wakeUp.Set ();
}
});
});
}
private static void RunTest (Random r, TextField tf, int numPasses, int numIncrements, int pollMs)
{
for (int j = 0; j < numPasses; j++) {
_wakeUp.Reset ();
for (var i = 0; i < numIncrements; i++) Launch (r, tf, (j + 1) * numIncrements);
while (tbCounter != (j + 1) * numIncrements) // Wait for tbCounter to reach expected value
{
var tbNow = tbCounter;
_wakeUp.Wait (pollMs);
if (tbCounter == tbNow) {
// No change after wait: Idle handlers added via Application.Invoke have gone missing
Application.Invoke (() => Application.RequestStop ());
throw new TimeoutException (
$"Timeout: Increment lost. tbCounter ({tbCounter}) didn't " +
$"change after waiting {pollMs} ms. Failed to reach {(j + 1) * numIncrements} on pass {j + 1}");
}
};
}
Application.Invoke (() => Application.RequestStop ());
}
[Fact]
[AutoInitShutdown]
public async Task InvokeLeakTest ()
{
Random r = new ();
TextField tf = new ();
Application.Top.Add (tf);
const int numPasses = 5;
const int numIncrements = 500;
const int pollMs = 2500;
var task = Task.Run (() => RunTest (r, tf, numPasses, numIncrements, pollMs));
// blocks here until the RequestStop is processed at the end of the test
Application.Run ();
await task; // Propagate exception if any occurred
Assert.Equal (numIncrements * numPasses, tbCounter);
}
private static int total;
private static Button btn;
private static string clickMe;
private static string cancel;
private static string pewPew;
private static int zero;
private static int one;
private static int two;
private static int three;
private static int four;
private static bool taskCompleted;
[Theory, AutoInitShutdown]
[MemberData (nameof (TestAddIdle))]
public void Mainloop_Invoke_Or_AddIdle_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)
{
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 ("Open Window");
btnLaunch.Clicked += (s, e) => action ();
Application.Top.Add (btnLaunch);
var iterations = -1;
Application.Iteration += (s, a) => {
iterations++;
if (iterations == 0) {
Assert.Null (btn);
Assert.Equal (zero, total);
Assert.True (btnLaunch.NewKeyDownEvent (new (KeyCode.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.True (btn.NewKeyDownEvent (new (KeyCode.Space)));
Assert.Equal (cancel, btn.Text);
Assert.Equal (one, total);
} else if (taskCompleted) {
Application.RequestStop ();
}
};
Application.Run ();
Assert.True (taskCompleted);
Assert.Equal (clickMe, btn.Text);
Assert.Equal (four, total);
}
public static IEnumerable<object []> TestAddIdle {
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 void StartWindow ()
{
var startWindow = new Window {
Modal = true
};
btn = new Button {
Text = "Click Me"
};
btn.Clicked += RunAsyncTest;
var totalbtn = new Button () {
X = Pos.Right (btn),
Text = "total"
};
totalbtn.Clicked += (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 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.SetNeedsDisplay ();
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.SetNeedsDisplay ();
});
}
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.SetNeedsDisplay ();
});
}
}