Skip to content

Commit cbd29f4

Browse files
committed
Merge branch 'main' of tig:migueldeicaza/gui.cs
2 parents 4a09631 + c2d0a28 commit cbd29f4

File tree

2 files changed

+118
-41
lines changed

2 files changed

+118
-41
lines changed

Terminal.Gui/Core/MainLoop.cs

+21-9
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public interface IMainLoopDriver {
3131
bool EventsPending (bool wait);
3232

3333
/// <summary>
34-
/// The interation function.
34+
/// The iteration function.
3535
/// </summary>
3636
void MainIteration ();
3737
}
@@ -107,22 +107,30 @@ public Func<bool> AddIdle (Func<bool> idleHandler)
107107
/// Removes an idle handler added with <see cref="AddIdle(Func{bool})"/> from processing.
108108
/// </summary>
109109
/// <param name="token">A token returned by <see cref="AddIdle(Func{bool})"/></param>
110-
public void RemoveIdle (Func<bool> token)
110+
/// Returns <c>true</c>if the idle handler is successfully removed; otherwise, <c>false</c>.
111+
/// This method also returns <c>false</c> if the idle handler is not found.
112+
public bool RemoveIdle (Func<bool> token)
111113
{
112114
lock (token)
113-
idleHandlers.Remove (token);
115+
return idleHandlers.Remove (token);
114116
}
115117

116118
void AddTimeout (TimeSpan time, Timeout timeout)
117119
{
118-
timeouts.Add ((DateTime.UtcNow + time).Ticks, timeout);
120+
var k = (DateTime.UtcNow + time).Ticks;
121+
lock (timeouts) {
122+
while (timeouts.ContainsKey (k)) {
123+
k = (DateTime.UtcNow + time).Ticks;
124+
}
125+
}
126+
timeouts.Add (k, timeout);
119127
}
120128

121129
/// <summary>
122130
/// Adds a timeout to the mainloop.
123131
/// </summary>
124132
/// <remarks>
125-
/// When time time specified passes, the callback will be invoked.
133+
/// When time specified passes, the callback will be invoked.
126134
/// If the callback returns true, the timeout will be reset, repeating
127135
/// the invocation. If it returns false, the timeout will stop and be removed.
128136
///
@@ -147,21 +155,25 @@ public object AddTimeout (TimeSpan time, Func<MainLoop, bool> callback)
147155
/// <remarks>
148156
/// The token parameter is the value returned by AddTimeout.
149157
/// </remarks>
150-
public void RemoveTimeout (object token)
158+
/// Returns <c>true</c>if the timeout is successfully removed; otherwise, <c>false</c>.
159+
/// This method also returns <c>false</c> if the timeout is not found.
160+
public bool RemoveTimeout (object token)
151161
{
152162
var idx = timeouts.IndexOfValue (token as Timeout);
153163
if (idx == -1)
154-
return;
164+
return false;
155165
timeouts.RemoveAt (idx);
166+
return true;
156167
}
157168

158169
void RunTimers ()
159170
{
160171
long now = DateTime.UtcNow.Ticks;
161172
var copy = timeouts;
162173
timeouts = new SortedList<long, Timeout> ();
163-
foreach (var k in copy.Keys) {
164-
var timeout = copy [k];
174+
foreach (var t in copy) {
175+
var k = t.Key;
176+
var timeout = t.Value;
165177
if (k < now) {
166178
if (timeout.Callback (this))
167179
AddTimeout (timeout.Span, timeout);

UnitTests/MainLoopTests.cs

+97-32
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Linq;
66
using System.Runtime.InteropServices.ComTypes;
77
using System.Threading;
8+
using System.Threading.Tasks;
89
using Terminal.Gui;
910
using Xunit;
1011
using Xunit.Sdk;
@@ -33,28 +34,31 @@ public void AddIdle_Adds_And_Removes ()
3334
ml.AddIdle (fnTrue);
3435
ml.AddIdle (fnFalse);
3536

36-
ml.RemoveIdle (fnTrue);
37+
Assert.True (ml.RemoveIdle (fnTrue));
3738

38-
// BUGBUG: This doens't throw or indicate an error. Ideally RemoveIdle would either
39-
// trhow an exception in this case, or return an error.
40-
ml.RemoveIdle (fnTrue);
39+
// BUGBUG: This doesn't throw or indicate an error. Ideally RemoveIdle would either
40+
// throw an exception in this case, or return an error.
41+
// No. Only need to return a boolean.
42+
Assert.False (ml.RemoveIdle (fnTrue));
4143

42-
ml.RemoveIdle (fnFalse);
44+
Assert.True (ml.RemoveIdle (fnFalse));
4345

4446
// BUGBUG: This doesn't throw an exception or indicate an error. Ideally RemoveIdle would either
45-
// trhow an exception in this case, or return an error.
46-
ml.RemoveIdle (fnFalse);
47+
// throw an exception in this case, or return an error.
48+
// No. Only need to return a boolean.
49+
Assert.False (ml.RemoveIdle (fnFalse));
4750

4851
// Add again, but with dupe
4952
ml.AddIdle (fnTrue);
5053
ml.AddIdle (fnTrue);
5154

52-
ml.RemoveIdle (fnTrue);
53-
ml.RemoveIdle (fnTrue);
55+
Assert.True (ml.RemoveIdle (fnTrue));
56+
Assert.True (ml.RemoveIdle (fnTrue));
5457

5558
// BUGBUG: This doesn't throw an exception or indicate an error. Ideally RemoveIdle would either
56-
// trhow an exception in this case, or return an error.
57-
ml.RemoveIdle (fnTrue);
59+
// throw an exception in this case, or return an error.
60+
// No. Only need to return a boolean.
61+
Assert.False (ml.RemoveIdle (fnTrue));
5862
}
5963

6064
[Fact]
@@ -84,8 +88,7 @@ public void RemoveIdle_Function_NotCalled ()
8488
return true;
8589
};
8690

87-
functionCalled = 0;
88-
ml.RemoveIdle (fn);
91+
Assert.False (ml.RemoveIdle (fn));
8992
ml.MainIteration ();
9093
Assert.Equal (0, functionCalled);
9194
}
@@ -101,9 +104,8 @@ public void AddThenRemoveIdle_Function_NotCalled ()
101104
return true;
102105
};
103106

104-
functionCalled = 0;
105107
ml.AddIdle (fn);
106-
ml.RemoveIdle (fn);
108+
Assert.True (ml.RemoveIdle (fn));
107109
ml.MainIteration ();
108110
Assert.Equal (0, functionCalled);
109111
}
@@ -119,21 +121,21 @@ public void AddTwice_Function_CalledTwice ()
119121
return true;
120122
};
121123

122-
functionCalled = 0;
123124
ml.AddIdle (fn);
124125
ml.AddIdle (fn);
125126
ml.MainIteration ();
126127
Assert.Equal (2, functionCalled);
127128

128129
functionCalled = 0;
129-
ml.RemoveIdle (fn);
130+
Assert.True (ml.RemoveIdle (fn));
130131
ml.MainIteration ();
131132
Assert.Equal (1, functionCalled);
132133

133134
functionCalled = 0;
134-
ml.RemoveIdle (fn);
135+
Assert.True (ml.RemoveIdle (fn));
135136
ml.MainIteration ();
136137
Assert.Equal (0, functionCalled);
138+
Assert.False (ml.RemoveIdle (fn));
137139
}
138140

139141
[Fact]
@@ -163,10 +165,11 @@ public void False_Idle_Stops_It_Being_Called_Again ()
163165
ml.AddIdle (fnStop);
164166
ml.AddIdle (fn1);
165167
ml.Run ();
166-
ml.RemoveIdle (fnStop);
167-
ml.RemoveIdle (fn1);
168+
Assert.True (ml.RemoveIdle (fnStop));
169+
Assert.False (ml.RemoveIdle (fn1));
168170

169171
Assert.Equal (10, functionCalled);
172+
Assert.Equal (20, stopCount);
170173
}
171174

172175
[Fact]
@@ -194,9 +197,9 @@ public void AddIdle_Twice_Returns_False_Called_Twice ()
194197
ml.AddIdle (fn1);
195198
ml.AddIdle (fn1);
196199
ml.Run ();
197-
ml.RemoveIdle (fnStop);
198-
ml.RemoveIdle (fn1);
199-
ml.RemoveIdle (fn1);
200+
Assert.True (ml.RemoveIdle (fnStop));
201+
Assert.False (ml.RemoveIdle (fn1));
202+
Assert.False (ml.RemoveIdle (fn1));
200203

201204
Assert.Equal (2, functionCalled);
202205
}
@@ -217,7 +220,7 @@ public void Run_Runs_Idle_Stop_Stops_Idle ()
217220

218221
ml.AddIdle (fn);
219222
ml.Run ();
220-
ml.RemoveIdle (fn);
223+
Assert.True (ml.RemoveIdle (fn));
221224

222225
Assert.Equal (10, functionCalled);
223226
}
@@ -237,10 +240,11 @@ public void AddTimer_Adds_Removes_NoFaults ()
237240

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

240-
ml.RemoveTimeout (token);
243+
Assert.True (ml.RemoveTimeout (token));
241244

242245
// BUGBUG: This should probably fault?
243-
ml.RemoveTimeout (token);
246+
// Must return a boolean.
247+
Assert.False (ml.RemoveTimeout (token));
244248
}
245249

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

259263
var token = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback);
260264
ml.Run ();
261-
ml.RemoveTimeout (token);
265+
Assert.True (ml.RemoveTimeout (token));
262266

263267
Assert.Equal (1, callbackCount);
264268
}
265269

270+
[Fact]
271+
public async Task AddTimer_Duplicate_Keys_Not_Allowed ()
272+
{
273+
var ml = new MainLoop (new FakeMainLoop (() => FakeConsole.ReadKey (true)));
274+
const int ms = 100;
275+
object token1 = null, token2 = null;
276+
277+
var callbackCount = 0;
278+
Func<MainLoop, bool> callback = (MainLoop loop) => {
279+
callbackCount++;
280+
if (callbackCount == 2) {
281+
ml.Stop ();
282+
}
283+
return true;
284+
};
285+
286+
var task1 = new Task (() => token1 = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback));
287+
var task2 = new Task (() => token2 = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback));
288+
Assert.Null (token1);
289+
Assert.Null (token2);
290+
task1.Start ();
291+
task2.Start ();
292+
ml.Run ();
293+
Assert.NotNull (token1);
294+
Assert.NotNull (token2);
295+
await Task.WhenAll (task1, task2);
296+
Assert.True (ml.RemoveTimeout (token1));
297+
Assert.True (ml.RemoveTimeout (token2));
298+
299+
Assert.Equal (2, callbackCount);
300+
}
301+
302+
[Fact]
303+
public void AddTimer_In_Parallel_Wont_Throw ()
304+
{
305+
var ml = new MainLoop (new FakeMainLoop (() => FakeConsole.ReadKey (true)));
306+
const int ms = 100;
307+
object token1 = null, token2 = null;
308+
309+
var callbackCount = 0;
310+
Func<MainLoop, bool> callback = (MainLoop loop) => {
311+
callbackCount++;
312+
if (callbackCount == 2) {
313+
ml.Stop ();
314+
}
315+
return true;
316+
};
317+
318+
Parallel.Invoke (
319+
() => token1 = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback),
320+
() => token2 = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback)
321+
);
322+
ml.Run ();
323+
Assert.NotNull (token1);
324+
Assert.NotNull (token2);
325+
Assert.True (ml.RemoveTimeout (token1));
326+
Assert.True (ml.RemoveTimeout (token2));
327+
328+
Assert.Equal (2, callbackCount);
329+
}
330+
266331

267332
class MillisecondTolerance : IEqualityComparer<TimeSpan> {
268333
int _tolerance = 0;
@@ -293,7 +358,7 @@ public void AddTimer_Run_CalledAtApproximatelyRightTime ()
293358
// https://github.com/xunit/assert.xunit/pull/25
294359
Assert.Equal<TimeSpan> (ms * callbackCount, watch.Elapsed, new MillisecondTolerance (100));
295360

296-
ml.RemoveTimeout (token);
361+
Assert.True (ml.RemoveTimeout (token));
297362
Assert.Equal (1, callbackCount);
298363
}
299364

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

324-
ml.RemoveTimeout (token);
389+
Assert.True (ml.RemoveTimeout (token));
325390
Assert.Equal (2, callbackCount);
326391
}
327392

@@ -349,7 +414,7 @@ public void AddTimer_Remove_NotCalled ()
349414
};
350415

351416
var token = ml.AddTimeout (ms, callback);
352-
ml.RemoveTimeout (token);
417+
Assert.True (ml.RemoveTimeout (token));
353418
ml.Run ();
354419
Assert.Equal (0, callbackCount);
355420
}
@@ -363,7 +428,7 @@ public void AddTimer_ReturnFalse_StopsBeingCalled ()
363428
// Force stop if 10 iterations
364429
var stopCount = 0;
365430
Func<bool> fnStop = () => {
366-
Thread.Sleep (10); // Sleep to enable timeer to fire
431+
Thread.Sleep (10); // Sleep to enable timer to fire
367432
stopCount++;
368433
if (stopCount == 10) {
369434
ml.Stop ();
@@ -382,7 +447,7 @@ public void AddTimer_ReturnFalse_StopsBeingCalled ()
382447
ml.Run ();
383448
Assert.Equal (1, callbackCount);
384449
Assert.Equal (10, stopCount);
385-
ml.RemoveTimeout (token);
450+
Assert.False (ml.RemoveTimeout (token));
386451
}
387452

388453
// Invoke Tests

0 commit comments

Comments
 (0)