- Notifications
You must be signed in to change notification settings - Fork 324
/
Copy pathTaskSchedulerTestHelper.cs
97 lines (82 loc) · 4.18 KB
/
TaskSchedulerTestHelper.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
usingSystem;
usingSystem.Threading.Tasks;
namespaceCommunityToolkit.Mvvm.UnitTests.Helpers;
/// <summary>
/// A helper class to validate scenarios related to <see cref="TaskScheduler"/>.
/// </summary>
internalstaticclassTaskSchedulerTestHelper
{
/// <summary>
/// A custom <see cref="Delegate"/> for callbacks to <see cref="IsExceptionBubbledUpToUnobservedTaskExceptionAsync(TestCallback)"/>.
/// </summary>
/// <param name="throwAction">An <see cref="Action"/> instance that throws a test exception to track.</param>
/// <param name="completeAction">An <see cref="Action"/> that signals whenever the test has completed.</param>
publicdelegatevoidTestCallback(ActionthrowAction,ActioncompleteAction);
/// <summary>
/// Checks whether a given test exception is correctly bubbled up to <see cref="TaskScheduler.UnobservedTaskException"/>.
/// </summary>
/// <param name="callback">The <see cref="TestCallback"/> instance to use to run the test.</param>
/// <returns>Whether or not the test exception was correctly bubbled up to <see cref="TaskScheduler.UnobservedTaskException"/>.</returns>
publicstaticasyncTask<bool>IsExceptionBubbledUpToUnobservedTaskExceptionAsync(TestCallbackcallback)
{
TaskCompletionSource<object?>tcs=new(TaskCreationOptions.RunContinuationsAsynchronously);
stringguid=Guid.NewGuid().ToString();
boolexceptionFound=false;
voidTaskScheduler_UnobservedTaskException(object?sender,UnobservedTaskExceptionEventArgse)
{
e.SetObserved();
foreach(Exceptionexceptionine.Exception!.InnerExceptions)
{
if(exceptionisTestExceptiontestException&&
testException.Message==guid)
{
exceptionFound=true;
return;
}
}
}
EventHandler<UnobservedTaskExceptionEventArgs>handler=TaskScheduler_UnobservedTaskException;
TaskScheduler.UnobservedTaskException+=handler;
try
{
// Enqueue a continuation that will throw and ignore the returned task. This has
// to be a separate method to ensure the returned task isn't kept alive for longer.
callback(
()=>thrownewTestException(guid),
()=>tcs.SetResult(null));
// Await for the continuation to actually run
_=awaittcs.Task;
// Wait for some additional time to ensure the exception is propagated. This is a bit counterintuitive, but the delay is
// not actually for the event to be raised, but to ensure the task that is throwing has had time to be scheduled and fail.
// The event is raised only when the exception wrapper inside that task is collected and its finalizer has run (that's where
// the logic to raise the event is executed), which is why we're then calling GC.Collect() and GC.WaitForPendingFinalizers().
// That is, we can't use a task completion source from that event, because that event is only guaranteed to actually be raised
// when the finalizer for that task run, which is why we're calling the GC after the delay here.
awaitTask.Delay(200);
GC.Collect();
GC.WaitForPendingFinalizers();
}
finally
{
TaskScheduler.UnobservedTaskException-=handler;
}
returnexceptionFound;
}
/// <summary>
/// A custom exception to support <see cref="IsExceptionBubbledUpToUnobservedTaskExceptionAsync(TestCallback)"/>.
/// </summary>
privatesealedclassTestException:Exception
{
/// <summary>
/// Creates a new <see cref="TestException"/> instance with the specified parameters.
/// </summary>
/// <param name="message">The exception message.</param>
publicTestException(stringmessage)
:base(message)
{
}
}
}