A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using NUnit.Framework;
6using osu.Framework.Threading;
7using osu.Framework.Timing;
8
9namespace osu.Framework.Tests.Threading
10{
11 [TestFixture]
12 public class SchedulerTest
13 {
14 private Scheduler scheduler;
15
16 private bool fromMainThread;
17
18 [SetUp]
19 public void Setup()
20 {
21 scheduler = new Scheduler(() => fromMainThread, new StopwatchClock(true));
22 }
23
24 [Test]
25 public void TestScheduleOnce([Values(false, true)] bool fromMainThread, [Values(false, true)] bool forceScheduled)
26 {
27 this.fromMainThread = fromMainThread;
28
29 int invocations = 0;
30
31 var del = scheduler.Add(() => invocations++, forceScheduled);
32
33 if (fromMainThread && !forceScheduled)
34 {
35 Assert.AreEqual(1, invocations);
36 Assert.That(del, Is.Null);
37 }
38 else
39 {
40 Assert.AreEqual(0, invocations);
41 Assert.That(del, Is.Not.Null);
42 }
43
44 scheduler.Update();
45 Assert.AreEqual(1, invocations);
46
47 // Ensures that the delegate runs only once in the scheduled/not on main thread branch
48 scheduler.Update();
49 Assert.AreEqual(1, invocations);
50 }
51
52 [Test]
53 public void TestCancelScheduledDelegate()
54 {
55 int invocations = 0;
56
57 ScheduledDelegate del;
58 scheduler.Add(del = new ScheduledDelegate(() => invocations++));
59
60 del.Cancel();
61
62 scheduler.Update();
63 Assert.AreEqual(0, invocations);
64 }
65
66 [Test]
67 public void TestAddCancelledDelegate()
68 {
69 int invocations = 0;
70
71 ScheduledDelegate del = new ScheduledDelegate(() => invocations++);
72 del.Cancel();
73
74 scheduler.Add(del);
75
76 scheduler.Update();
77 Assert.AreEqual(0, invocations);
78 }
79
80 [Test]
81 public void TestCustomScheduledDelegateRunsOnce()
82 {
83 int invocations = 0;
84
85 scheduler.Add(new ScheduledDelegate(() => invocations++));
86
87 scheduler.Update();
88 Assert.AreEqual(1, invocations);
89
90 scheduler.Update();
91 Assert.AreEqual(1, invocations);
92 }
93
94 [Test]
95 public void TestDelayedDelegatesAreScheduledWithCorrectTime()
96 {
97 var clock = new StopwatchClock();
98 scheduler.UpdateClock(clock);
99
100 ScheduledDelegate del = scheduler.AddDelayed(() => { }, 1000);
101
102 Assert.AreEqual(1000, del.ExecutionTime);
103
104 clock.Seek(500);
105 del = scheduler.AddDelayed(() => { }, 1000);
106
107 Assert.AreEqual(1500, del.ExecutionTime);
108 }
109
110 [Test]
111 public void TestDelayedDelegateDoesNotRunUntilExpectedTime()
112 {
113 var clock = new StopwatchClock();
114 scheduler.UpdateClock(clock);
115
116 int invocations = 0;
117
118 scheduler.AddDelayed(() => invocations++, 1000);
119
120 clock.Seek(1000);
121 scheduler.AddDelayed(() => invocations++, 1000);
122
123 for (double d = 0; d <= 2500; d += 100)
124 {
125 clock.Seek(d);
126 scheduler.Update();
127
128 int expectedInvocations = 0;
129
130 if (d >= 1000)
131 expectedInvocations++;
132 if (d >= 2000)
133 expectedInvocations++;
134
135 Assert.AreEqual(expectedInvocations, invocations);
136 }
137 }
138
139 [Test]
140 public void TestCancelDelayedDelegate()
141 {
142 var clock = new StopwatchClock();
143 scheduler.UpdateClock(clock);
144
145 int invocations = 0;
146
147 ScheduledDelegate del;
148 scheduler.Add(del = new ScheduledDelegate(() => invocations++, 1000));
149 del.Cancel();
150
151 clock.Seek(1500);
152 scheduler.Update();
153 Assert.AreEqual(0, invocations);
154 }
155
156 [Test]
157 public void TestCancelDelayedDelegateDuringRun()
158 {
159 var clock = new StopwatchClock();
160 scheduler.UpdateClock(clock);
161
162 int invocations = 0;
163
164 ScheduledDelegate del = null;
165
166 scheduler.Add(del = new ScheduledDelegate(() =>
167 {
168 invocations++;
169
170 // ReSharper disable once AccessToModifiedClosure
171 del?.Cancel();
172 }, 1000));
173
174 Assert.AreEqual(ScheduledDelegate.RunState.Waiting, del.State);
175
176 clock.Seek(1500);
177 scheduler.Update();
178 Assert.AreEqual(1, invocations);
179
180 Assert.AreEqual(ScheduledDelegate.RunState.Cancelled, del.State);
181
182 clock.Seek(2500);
183 scheduler.Update();
184 Assert.AreEqual(1, invocations);
185 }
186
187 [Test]
188 public void TestRepeatingDelayedDelegate()
189 {
190 var clock = new StopwatchClock();
191 scheduler.UpdateClock(clock);
192
193 int invocations = 0;
194
195 scheduler.Add(new ScheduledDelegate(() => invocations++, 500, 500));
196 scheduler.Add(new ScheduledDelegate(() => invocations++, 1000, 500));
197
198 for (double d = 0; d <= 2500; d += 100)
199 {
200 clock.Seek(d);
201 scheduler.Update();
202
203 int expectedInvocations = 0;
204
205 if (d >= 500)
206 expectedInvocations += 1 + (int)((d - 500) / 500);
207 if (d >= 1000)
208 expectedInvocations += 1 + (int)((d - 1000) / 500);
209
210 Assert.AreEqual(expectedInvocations, invocations);
211 }
212 }
213
214 [Test]
215 public void TestZeroDelayRepeatingDelegate()
216 {
217 var clock = new StopwatchClock();
218 scheduler.UpdateClock(clock);
219
220 int invocations = 0;
221
222 Assert.Zero(scheduler.TotalPendingTasks);
223
224 scheduler.Add(new ScheduledDelegate(() => invocations++, 500, 0));
225
226 Assert.AreEqual(1, scheduler.TotalPendingTasks);
227
228 int expectedInvocations = 0;
229
230 for (double d = 0; d <= 2500; d += 100)
231 {
232 clock.Seek(d);
233 scheduler.Update();
234
235 if (d >= 500)
236 expectedInvocations++;
237
238 Assert.AreEqual(expectedInvocations, invocations);
239 Assert.AreEqual(1, scheduler.TotalPendingTasks);
240 }
241 }
242
243 [TestCase(false)]
244 [TestCase(true)]
245 public void TestRepeatingDelayedDelegateCatchUp(bool performCatchUp)
246 {
247 var clock = new StopwatchClock();
248 scheduler.UpdateClock(clock);
249
250 int invocations = 0;
251
252 scheduler.Add(new ScheduledDelegate(() => invocations++, 500, 500)
253 {
254 PerformRepeatCatchUpExecutions = performCatchUp
255 });
256
257 for (double d = 0; d <= 10000; d += 2000)
258 {
259 clock.Seek(d);
260
261 for (int i = 0; i < 10; i++)
262 // allow catch-up to potentially occur.
263 scheduler.Update();
264
265 int expectedInovations;
266
267 if (performCatchUp)
268 expectedInovations = (int)(d / 500);
269 else
270 expectedInovations = (int)(d / 2000);
271
272 Assert.AreEqual(expectedInovations, invocations);
273 }
274 }
275
276 [Test]
277 public void TestCancelAfterRepeatReschedule()
278 {
279 var clock = new StopwatchClock();
280 scheduler.UpdateClock(clock);
281
282 int invocations = 0;
283
284 ScheduledDelegate del;
285 scheduler.Add(del = new ScheduledDelegate(() => invocations++, 500, 500));
286
287 Assert.AreEqual(ScheduledDelegate.RunState.Waiting, del.State);
288
289 clock.Seek(750);
290 scheduler.Update();
291 Assert.AreEqual(1, invocations);
292
293 Assert.AreEqual(ScheduledDelegate.RunState.Complete, del.State);
294
295 del.Cancel();
296
297 Assert.AreEqual(ScheduledDelegate.RunState.Cancelled, del.State);
298
299 clock.Seek(1250);
300 scheduler.Update();
301 Assert.AreEqual(1, invocations);
302
303 Assert.AreEqual(ScheduledDelegate.RunState.Cancelled, del.State);
304 }
305
306 [Test]
307 public void TestAddOnce()
308 {
309 int invocations = 0;
310
311 void action() => invocations++;
312
313 scheduler.AddOnce(action);
314 scheduler.AddOnce(action);
315
316 scheduler.Update();
317 Assert.AreEqual(1, invocations);
318 }
319
320 [Test]
321 public void TestPerUpdateTask()
322 {
323 int invocations = 0;
324
325 scheduler.AddDelayed(() => invocations++, 0, true);
326 Assert.AreEqual(0, invocations);
327
328 scheduler.Update();
329 Assert.AreEqual(1, invocations);
330
331 scheduler.Update();
332 Assert.AreEqual(2, invocations);
333 }
334
335 [Test]
336 public void TestScheduleFromInsideDelegate([Values(false, true)] bool forceScheduled)
337 {
338 const int max_reschedules = 3;
339
340 fromMainThread = !forceScheduled;
341
342 int reschedules = 0;
343
344 scheduleTask();
345
346 if (forceScheduled)
347 {
348 for (int i = 0; i <= max_reschedules; i++)
349 {
350 scheduler.Update();
351 Assert.AreEqual(Math.Min(max_reschedules, i + 1), reschedules);
352 }
353 }
354 else
355 Assert.AreEqual(max_reschedules, reschedules);
356
357 void scheduleTask() => scheduler.Add(() =>
358 {
359 if (reschedules == max_reschedules)
360 return;
361
362 reschedules++;
363 scheduleTask();
364 }, forceScheduled);
365 }
366
367 [Test]
368 public void TestInvokeBeforeSchedulerRun()
369 {
370 int invocations = 0;
371
372 ScheduledDelegate del = new ScheduledDelegate(() => invocations++);
373
374 scheduler.Add(del);
375 Assert.AreEqual(0, invocations);
376
377 del.RunTask();
378 Assert.AreEqual(1, invocations);
379
380 scheduler.Update();
381 Assert.AreEqual(1, invocations);
382 }
383
384 [Test]
385 public void TestInvokeAfterSchedulerRun()
386 {
387 int invocations = 0;
388
389 ScheduledDelegate del = new ScheduledDelegate(() => invocations++);
390
391 scheduler.Add(del);
392 Assert.AreEqual(0, invocations);
393
394 scheduler.Update();
395 Assert.AreEqual(1, invocations);
396
397 Assert.Throws<InvalidOperationException>(del.RunTask);
398 Assert.AreEqual(1, invocations);
399 }
400
401 [Test]
402 public void TestInvokeBeforeScheduleUpdate()
403 {
404 int invocations = 0;
405 ScheduledDelegate del;
406 scheduler.Add(del = new ScheduledDelegate(() => invocations++));
407 Assert.AreEqual(0, invocations);
408 del.RunTask();
409 Assert.AreEqual(1, invocations);
410 scheduler.Update();
411 Assert.AreEqual(1, invocations);
412 }
413
414 [Test]
415 public void TestRepeatAlreadyCompletedSchedule()
416 {
417 int invocations = 0;
418 var del = new ScheduledDelegate(() => invocations++);
419 del.RunTask();
420 Assert.AreEqual(1, invocations);
421 Assert.Throws<InvalidOperationException>(() => scheduler.Add(del));
422 scheduler.Update();
423 Assert.AreEqual(1, invocations);
424 }
425
426 /// <summary>
427 /// Tests that delegates added from inside a scheduled callback don't get executed when the scheduled callback cancels a prior intermediate task.
428 ///
429 /// Delegate 1 - Added at the start.
430 /// Delegate 2 - Added at the start, cancelled by Delegate 1.
431 /// Delegate 3 - Added during Delegate 1 callback, should not get executed.
432 /// </summary>
433 [Test]
434 public void TestDelegateAddedInCallbackNotExecutedAfterIntermediateCancelledDelegate()
435 {
436 int invocations = 0;
437
438 // Delegate 2
439 var cancelled = new ScheduledDelegate(() => { });
440
441 // Delegate 1
442 scheduler.Add(() =>
443 {
444 invocations++;
445
446 cancelled.Cancel();
447
448 // Delegate 3
449 scheduler.Add(() => invocations++);
450 });
451
452 scheduler.Add(cancelled);
453
454 scheduler.Update();
455 Assert.That(invocations, Is.EqualTo(1));
456 }
457 }
458}