Skip to content

Commit 55b600b

Browse files
committed
Add unit tests for UnobservedTaskExceptions propagation
1 parent 33c58e3 commit 55b600b

File tree

4 files changed

+214
-0
lines changed

4 files changed

+214
-0
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Threading.Tasks;
7+
8+
namespace CommunityToolkit.Mvvm.UnitTests.Helpers;
9+
10+
/// <summary>
11+
/// A helper class to validate scenarios related to <see cref="TaskScheduler"/>.
12+
/// </summary>
13+
internal static class TaskSchedulerTestHelper
14+
{
15+
/// <summary>
16+
/// A custom <see cref="Delegate"/> for callbacks to <see cref="IsExceptionBubbledUpToUnobservedTaskExceptionAsync(TestCallback)"/>.
17+
/// </summary>
18+
/// <param name="throwAction">An <see cref="Action"/> instance that throws a test exception to track.</param>
19+
/// <param name="completeAction">An <see cref="Action"/> that signals whenever the test has completed.</param>
20+
public delegate void TestCallback(Action throwAction, Action completeAction);
21+
22+
/// <summary>
23+
/// Checks whether a given test exception is correctly bubbled up to <see cref="TaskScheduler.UnobservedTaskException"/>.
24+
/// </summary>
25+
/// <param name="callback">The <see cref="TestCallback"/> instance to use to run the test.</param>
26+
/// <returns>Whether or not the test exception was correctly bubbled up to <see cref="TaskScheduler.UnobservedTaskException"/>.</returns>
27+
public static async Task<bool> IsExceptionBubbledUpToUnobservedTaskExceptionAsync(TestCallback callback)
28+
{
29+
TaskCompletionSource<object?> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
30+
string guid = Guid.NewGuid().ToString();
31+
bool exceptionFound = false;
32+
33+
void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
34+
{
35+
e.SetObserved();
36+
37+
foreach (Exception exception in e.Exception!.InnerExceptions)
38+
{
39+
if (exception is TestException testException &&
40+
testException.Message == guid)
41+
{
42+
exceptionFound = true;
43+
44+
return;
45+
}
46+
}
47+
}
48+
49+
EventHandler<UnobservedTaskExceptionEventArgs> handler = TaskScheduler_UnobservedTaskException;
50+
51+
TaskScheduler.UnobservedTaskException += handler;
52+
53+
try
54+
{
55+
// Enqueue a continuation that will throw and ignore the returned task. This has
56+
// to be a separate method to ensure the returned task isn't kept alive for longer.
57+
callback(
58+
() => throw new TestException(guid),
59+
() => tcs.SetResult(null));
60+
61+
// Await for the continuation to actually run
62+
_ = await tcs.Task;
63+
64+
// Some additional time to ensure the exception is propagated
65+
await Task.Delay(200);
66+
67+
GC.Collect();
68+
GC.WaitForPendingFinalizers();
69+
}
70+
finally
71+
{
72+
TaskScheduler.UnobservedTaskException -= handler;
73+
}
74+
75+
return exceptionFound;
76+
}
77+
78+
/// <summary>
79+
/// A custom exception to support <see cref="IsExceptionBubbledUpToUnobservedTaskExceptionAsync(TestCallback)"/>.
80+
/// </summary>
81+
private sealed class TestException : Exception
82+
{
83+
/// <summary>
84+
/// Creates a new <see cref="TestException"/> instance with the specified parameters.
85+
/// </summary>
86+
/// <param name="message">The exception message.</param>
87+
public TestException(string message)
88+
: base(message)
89+
{
90+
}
91+
}
92+
}

tests/CommunityToolkit.Mvvm.UnitTests/Test_AsyncRelayCommand.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.ComponentModel;
88
using System.Threading.Tasks;
99
using CommunityToolkit.Mvvm.Input;
10+
using CommunityToolkit.Mvvm.UnitTests.Helpers;
1011
using Microsoft.VisualStudio.TestTools.UnitTesting;
1112

1213
namespace CommunityToolkit.Mvvm.UnitTests;
@@ -298,4 +299,30 @@ private static async Task Test_AsyncRelayCommand_AllowConcurrentExecutions_TestL
298299

299300
Assert.IsFalse(command.IsRunning);
300301
}
302+
303+
[TestMethod]
304+
public async Task Test_AsyncRelayCommand_ThrowingTaskBubblesToUnobservedTaskException()
305+
{
306+
static async Task TestMethodAsync(Action action)
307+
{
308+
await Task.Delay(100);
309+
310+
action();
311+
}
312+
313+
async void TestCallback(Action throwAction, Action completeAction)
314+
{
315+
AsyncRelayCommand command = new(() => TestMethodAsync(throwAction));
316+
317+
command.Execute(null);
318+
319+
await Task.Delay(200);
320+
321+
completeAction();
322+
}
323+
324+
bool success = await TaskSchedulerTestHelper.IsExceptionBubbledUpToUnobservedTaskExceptionAsync(TestCallback);
325+
326+
Assert.IsTrue(success);
327+
}
301328
}

tests/CommunityToolkit.Mvvm.UnitTests/Test_AsyncRelayCommand{T}.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System;
66
using System.Threading.Tasks;
77
using CommunityToolkit.Mvvm.Input;
8+
using CommunityToolkit.Mvvm.UnitTests.Helpers;
89
using Microsoft.VisualStudio.TestTools.UnitTesting;
910

1011
namespace CommunityToolkit.Mvvm.UnitTests;
@@ -196,4 +197,30 @@ private static async Task Test_AsyncRelayCommandOfT_AllowConcurrentExecutions_Te
196197

197198
_ = Assert.ThrowsException<InvalidCastException>(() => command.Execute(new object()));
198199
}
200+
201+
[TestMethod]
202+
public async Task Test_AsyncRelayCommandOfT_ThrowingTaskBubblesToUnobservedTaskException()
203+
{
204+
static async Task TestMethodAsync(Action action)
205+
{
206+
await Task.Delay(100);
207+
208+
action();
209+
}
210+
211+
async void TestCallback(Action throwAction, Action completeAction)
212+
{
213+
AsyncRelayCommand<string> command = new(s => TestMethodAsync(throwAction));
214+
215+
command.Execute(null);
216+
217+
await Task.Delay(200);
218+
219+
completeAction();
220+
}
221+
222+
bool success = await TaskSchedulerTestHelper.IsExceptionBubbledUpToUnobservedTaskExceptionAsync(TestCallback);
223+
224+
Assert.IsTrue(success);
225+
}
199226
}

tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservableObject.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.ComponentModel;
77
using System.Threading.Tasks;
88
using CommunityToolkit.Mvvm.ComponentModel;
9+
using CommunityToolkit.Mvvm.UnitTests.Helpers;
910
using Microsoft.VisualStudio.TestTools.UnitTesting;
1011

1112
namespace CommunityToolkit.Mvvm.UnitTests;
@@ -225,4 +226,71 @@ public Task<T>? Data
225226
set => SetPropertyAndNotifyOnCompletion(ref data, value);
226227
}
227228
}
229+
230+
[TestMethod]
231+
public async Task Test_ObservableObject_ThrowingTaskBubblesToUnobservedTaskException()
232+
{
233+
SampleModelWithTask model = new();
234+
235+
static async Task TestMethodAsync(Action action)
236+
{
237+
await Task.Delay(100);
238+
239+
action();
240+
}
241+
242+
async void TestCallback(Action throwAction, Action completeAction)
243+
{
244+
model.Data = TestMethodAsync(throwAction);
245+
model.Data = null;
246+
247+
await Task.Delay(200);
248+
249+
completeAction();
250+
}
251+
252+
bool success = await TaskSchedulerTestHelper.IsExceptionBubbledUpToUnobservedTaskExceptionAsync(TestCallback);
253+
254+
Assert.IsTrue(success);
255+
}
256+
257+
[TestMethod]
258+
public async Task Test_ObservableObject_ThrowingTaskOfTBubblesToUnobservedTaskException()
259+
{
260+
SampleModelWithTask<int> model = new();
261+
262+
static async Task<int> TestMethodAsync(Action action)
263+
{
264+
await Task.Delay(100);
265+
266+
action();
267+
268+
return 42;
269+
}
270+
271+
async void TestCallback(Action throwAction, Action completeAction)
272+
{
273+
model.Data = TestMethodAsync(throwAction);
274+
model.Data = null;
275+
276+
await Task.Delay(200);
277+
278+
completeAction();
279+
}
280+
281+
bool success = await TaskSchedulerTestHelper.IsExceptionBubbledUpToUnobservedTaskExceptionAsync(TestCallback);
282+
283+
Assert.IsTrue(success);
284+
}
285+
286+
public class SampleModelWithTask : ObservableObject
287+
{
288+
private TaskNotifier? taskNotifier;
289+
290+
public Task? Data
291+
{
292+
get => taskNotifier;
293+
set => SetPropertyAndNotifyOnCompletion(ref taskNotifier, value);
294+
}
295+
}
228296
}

0 commit comments

Comments
 (0)