Skip to content

Commit

Permalink
Merge branch 'main' of tig:migueldeicaza/gui.cs
Browse files Browse the repository at this point in the history
  • Loading branch information
tig committed May 17, 2021
2 parents 4a09631 + c2d0a28 commit cbd29f4
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 41 deletions.
30 changes: 21 additions & 9 deletions Terminal.Gui/Core/MainLoop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public interface IMainLoopDriver {
bool EventsPending (bool wait);

/// <summary>
/// The interation function.
/// The iteration function.
/// </summary>
void MainIteration ();
}
Expand Down Expand Up @@ -107,22 +107,30 @@ public Func<bool> AddIdle (Func<bool> idleHandler)
/// Removes an idle handler added with <see cref="AddIdle(Func{bool})"/> from processing.
/// </summary>
/// <param name="token">A token returned by <see cref="AddIdle(Func{bool})"/></param>
public void RemoveIdle (Func<bool> token)
/// Returns <c>true</c>if the idle handler is successfully removed; otherwise, <c>false</c>.
/// This method also returns <c>false</c> if the idle handler is not found.
public bool RemoveIdle (Func<bool> token)
{
lock (token)
idleHandlers.Remove (token);
return idleHandlers.Remove (token);
}

void AddTimeout (TimeSpan time, Timeout timeout)
{
timeouts.Add ((DateTime.UtcNow + time).Ticks, timeout);
var k = (DateTime.UtcNow + time).Ticks;
lock (timeouts) {
while (timeouts.ContainsKey (k)) {
k = (DateTime.UtcNow + time).Ticks;
}
}
timeouts.Add (k, timeout);
}

/// <summary>
/// Adds a timeout to the mainloop.
/// </summary>
/// <remarks>
/// When time time specified passes, the callback will be invoked.
/// When time specified passes, the callback will be invoked.
/// If the callback returns true, the timeout will be reset, repeating
/// the invocation. If it returns false, the timeout will stop and be removed.
///
Expand All @@ -147,21 +155,25 @@ public object AddTimeout (TimeSpan time, Func<MainLoop, bool> callback)
/// <remarks>
/// The token parameter is the value returned by AddTimeout.
/// </remarks>
public void RemoveTimeout (object token)
/// Returns <c>true</c>if the timeout is successfully removed; otherwise, <c>false</c>.
/// This method also returns <c>false</c> if the timeout is not found.
public bool RemoveTimeout (object token)
{
var idx = timeouts.IndexOfValue (token as Timeout);
if (idx == -1)
return;
return false;
timeouts.RemoveAt (idx);
return true;
}

void RunTimers ()
{
long now = DateTime.UtcNow.Ticks;
var copy = timeouts;
timeouts = new SortedList<long, Timeout> ();
foreach (var k in copy.Keys) {
var timeout = copy [k];
foreach (var t in copy) {
var k = t.Key;
var timeout = t.Value;
if (k < now) {
if (timeout.Callback (this))
AddTimeout (timeout.Span, timeout);
Expand Down
129 changes: 97 additions & 32 deletions UnitTests/MainLoopTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Linq;
using System.Runtime.InteropServices.ComTypes;
using System.Threading;
using System.Threading.Tasks;
using Terminal.Gui;
using Xunit;
using Xunit.Sdk;
Expand Down Expand Up @@ -33,28 +34,31 @@ public void AddIdle_Adds_And_Removes ()
ml.AddIdle (fnTrue);
ml.AddIdle (fnFalse);

ml.RemoveIdle (fnTrue);
Assert.True (ml.RemoveIdle (fnTrue));

// BUGBUG: This doens't throw or indicate an error. Ideally RemoveIdle would either
// trhow an exception in this case, or return an error.
ml.RemoveIdle (fnTrue);
// 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));

ml.RemoveIdle (fnFalse);
Assert.True (ml.RemoveIdle (fnFalse));

// BUGBUG: This doesn't throw an exception or indicate an error. Ideally RemoveIdle would either
// trhow an exception in this case, or return an error.
ml.RemoveIdle (fnFalse);
// 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);

ml.RemoveIdle (fnTrue);
ml.RemoveIdle (fnTrue);
Assert.True (ml.RemoveIdle (fnTrue));
Assert.True (ml.RemoveIdle (fnTrue));

// BUGBUG: This doesn't throw an exception or indicate an error. Ideally RemoveIdle would either
// trhow an exception in this case, or return an error.
ml.RemoveIdle (fnTrue);
// throw an exception in this case, or return an error.
// No. Only need to return a boolean.
Assert.False (ml.RemoveIdle (fnTrue));
}

[Fact]
Expand Down Expand Up @@ -84,8 +88,7 @@ public void RemoveIdle_Function_NotCalled ()
return true;
};

functionCalled = 0;
ml.RemoveIdle (fn);
Assert.False (ml.RemoveIdle (fn));
ml.MainIteration ();
Assert.Equal (0, functionCalled);
}
Expand All @@ -101,9 +104,8 @@ public void AddThenRemoveIdle_Function_NotCalled ()
return true;
};

functionCalled = 0;
ml.AddIdle (fn);
ml.RemoveIdle (fn);
Assert.True (ml.RemoveIdle (fn));
ml.MainIteration ();
Assert.Equal (0, functionCalled);
}
Expand All @@ -119,21 +121,21 @@ public void AddTwice_Function_CalledTwice ()
return true;
};

functionCalled = 0;
ml.AddIdle (fn);
ml.AddIdle (fn);
ml.MainIteration ();
Assert.Equal (2, functionCalled);

functionCalled = 0;
ml.RemoveIdle (fn);
Assert.True (ml.RemoveIdle (fn));
ml.MainIteration ();
Assert.Equal (1, functionCalled);

functionCalled = 0;
ml.RemoveIdle (fn);
Assert.True (ml.RemoveIdle (fn));
ml.MainIteration ();
Assert.Equal (0, functionCalled);
Assert.False (ml.RemoveIdle (fn));
}

[Fact]
Expand Down Expand Up @@ -163,10 +165,11 @@ public void False_Idle_Stops_It_Being_Called_Again ()
ml.AddIdle (fnStop);
ml.AddIdle (fn1);
ml.Run ();
ml.RemoveIdle (fnStop);
ml.RemoveIdle (fn1);
Assert.True (ml.RemoveIdle (fnStop));
Assert.False (ml.RemoveIdle (fn1));

Assert.Equal (10, functionCalled);
Assert.Equal (20, stopCount);
}

[Fact]
Expand Down Expand Up @@ -194,9 +197,9 @@ public void AddIdle_Twice_Returns_False_Called_Twice ()
ml.AddIdle (fn1);
ml.AddIdle (fn1);
ml.Run ();
ml.RemoveIdle (fnStop);
ml.RemoveIdle (fn1);
ml.RemoveIdle (fn1);
Assert.True (ml.RemoveIdle (fnStop));
Assert.False (ml.RemoveIdle (fn1));
Assert.False (ml.RemoveIdle (fn1));

Assert.Equal (2, functionCalled);
}
Expand All @@ -217,7 +220,7 @@ public void Run_Runs_Idle_Stop_Stops_Idle ()

ml.AddIdle (fn);
ml.Run ();
ml.RemoveIdle (fn);
Assert.True (ml.RemoveIdle (fn));

Assert.Equal (10, functionCalled);
}
Expand All @@ -237,10 +240,11 @@ public void AddTimer_Adds_Removes_NoFaults ()

var token = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback);

ml.RemoveTimeout (token);
Assert.True (ml.RemoveTimeout (token));

// BUGBUG: This should probably fault?
ml.RemoveTimeout (token);
// Must return a boolean.
Assert.False (ml.RemoveTimeout (token));
}

[Fact]
Expand All @@ -258,11 +262,72 @@ public void AddTimer_Run_Called ()

var token = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback);
ml.Run ();
ml.RemoveTimeout (token);
Assert.True (ml.RemoveTimeout (token));

Assert.Equal (1, callbackCount);
}

[Fact]
public async Task AddTimer_Duplicate_Keys_Not_Allowed ()
{
var ml = new MainLoop (new FakeMainLoop (() => FakeConsole.ReadKey (true)));
const int ms = 100;
object token1 = null, token2 = null;

var callbackCount = 0;
Func<MainLoop, bool> callback = (MainLoop loop) => {
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 (() => FakeConsole.ReadKey (true)));
const int ms = 100;
object token1 = null, token2 = null;

var callbackCount = 0;
Func<MainLoop, bool> callback = (MainLoop loop) => {
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;
Expand Down Expand Up @@ -293,7 +358,7 @@ public void AddTimer_Run_CalledAtApproximatelyRightTime ()
// https://github.com/xunit/assert.xunit/pull/25
Assert.Equal<TimeSpan> (ms * callbackCount, watch.Elapsed, new MillisecondTolerance (100));

ml.RemoveTimeout (token);
Assert.True (ml.RemoveTimeout (token));
Assert.Equal (1, callbackCount);
}

Expand Down Expand Up @@ -321,7 +386,7 @@ public void AddTimer_Run_CalledTwiceApproximatelyRightTime ()
// https://github.com/xunit/assert.xunit/pull/25
Assert.Equal<TimeSpan> (ms * callbackCount, watch.Elapsed, new MillisecondTolerance (100));

ml.RemoveTimeout (token);
Assert.True (ml.RemoveTimeout (token));
Assert.Equal (2, callbackCount);
}

Expand Down Expand Up @@ -349,7 +414,7 @@ public void AddTimer_Remove_NotCalled ()
};

var token = ml.AddTimeout (ms, callback);
ml.RemoveTimeout (token);
Assert.True (ml.RemoveTimeout (token));
ml.Run ();
Assert.Equal (0, callbackCount);
}
Expand All @@ -363,7 +428,7 @@ public void AddTimer_ReturnFalse_StopsBeingCalled ()
// Force stop if 10 iterations
var stopCount = 0;
Func<bool> fnStop = () => {
Thread.Sleep (10); // Sleep to enable timeer to fire
Thread.Sleep (10); // Sleep to enable timer to fire
stopCount++;
if (stopCount == 10) {
ml.Stop ();
Expand All @@ -382,7 +447,7 @@ public void AddTimer_ReturnFalse_StopsBeingCalled ()
ml.Run ();
Assert.Equal (1, callbackCount);
Assert.Equal (10, stopCount);
ml.RemoveTimeout (token);
Assert.False (ml.RemoveTimeout (token));
}

// Invoke Tests
Expand Down

0 comments on commit cbd29f4

Please sign in to comment.