From ef4990bce62d95381235a50b6cc11ac1b725dc38 Mon Sep 17 00:00:00 2001 From: Keith Dahlby Date: Tue, 16 Jul 2019 02:20:10 -0500 Subject: [PATCH] [Core] Missing await in Device.InvokeOnMainThreadAsync(Func) (#6718) * [Core] Use local functions * [Core] Extract DeviceUnitTests * [Core] Ensure actually called from main thread * [Core] Add InvokeOnMainThread tests WithAsyncTask is expected to fail. * [Core] Rename local wrapper functions * [Core] Avoid ambiguous call CS0121: The call is ambiguous between the following methods or properties: 'Device.InvokeOnMainThreadAsync(Action)' and 'Device.InvokeOnMainThreadAsync(Func)' https://github.com/dotnet/roslyn/issues/14885 https://github.com/dotnet/csharplang/issues/98 --- .../DeviceUnitTests.cs | 145 ++++++++++++++++++ Xamarin.Forms.Core.UnitTests/ViewUnitTests.cs | 18 --- .../Xamarin.Forms.Core.UnitTests.csproj | 1 + Xamarin.Forms.Core/Device.cs | 8 +- 4 files changed, 150 insertions(+), 22 deletions(-) create mode 100644 Xamarin.Forms.Core.UnitTests/DeviceUnitTests.cs diff --git a/Xamarin.Forms.Core.UnitTests/DeviceUnitTests.cs b/Xamarin.Forms.Core.UnitTests/DeviceUnitTests.cs new file mode 100644 index 00000000000..8d4cfabcdb5 --- /dev/null +++ b/Xamarin.Forms.Core.UnitTests/DeviceUnitTests.cs @@ -0,0 +1,145 @@ +using System; +using System.Threading.Tasks; +using NUnit.Framework; +using Xamarin.Forms.Internals; + +namespace Xamarin.Forms.Core.UnitTests +{ + [TestFixture] + public class DeviceUnitTests : BaseTestFixture + { + [Test] + public void TestBeginInvokeOnMainThread() + { + bool calledFromMainThread = false; + Device.PlatformServices = MockPlatformServices(() => calledFromMainThread = true); + + bool invoked = false; + Device.BeginInvokeOnMainThread(() => invoked = true); + + Assert.True(invoked, "Action not invoked."); + Assert.True(calledFromMainThread, "Action not invoked from main thread."); + } + + [Test] + public async Task TestInvokeOnMainThreadWithSyncFunc() + { + bool calledFromMainThread = false; + Device.PlatformServices = MockPlatformServices(() => calledFromMainThread = true); + + bool invoked = false; + var result = await Device.InvokeOnMainThreadAsync(() => { invoked = true; return true; }); + + Assert.True(invoked, "Action not invoked."); + Assert.True(calledFromMainThread, "Action not invoked from main thread."); + Assert.True(result, "Unexpected result."); + } + + [Test] + public async Task TestInvokeOnMainThreadWithSyncAction() + { + bool calledFromMainThread = false; + Device.PlatformServices = MockPlatformServices(() => calledFromMainThread = true); + + bool invoked = false; + await Device.InvokeOnMainThreadAsync(() => { invoked = true; }); + + Assert.True(invoked, "Action not invoked."); + Assert.True(calledFromMainThread, "Action not invoked from main thread."); + } + + [Test] + public async Task TestInvokeOnMainThreadWithAsyncFunc() + { + bool calledFromMainThread = false; + Device.PlatformServices = MockPlatformServices(() => calledFromMainThread = true, + invokeOnMainThread: action => Task.Delay(50).ContinueWith(_ => action())); + + bool invoked = false; + var task = Device.InvokeOnMainThreadAsync(async () => { invoked = true; return true; }); + Assert.True(calledFromMainThread, "Action not invoked from main thread."); + Assert.False(invoked, "Action invoked early."); + + var result = await task; + Assert.True(invoked, "Action not invoked."); + Assert.True(result, "Unexpected result."); + } + + [Test] + public async Task TestInvokeOnMainThreadWithAsyncFuncError() + { + bool calledFromMainThread = false; + Device.PlatformServices = MockPlatformServices(() => calledFromMainThread = true, + invokeOnMainThread: action => Task.Delay(50).ContinueWith(_ => action())); + + bool invoked = false; + async Task boom() { invoked = true; throw new ApplicationException(); } + var task = Device.InvokeOnMainThreadAsync(boom); + Assert.True(calledFromMainThread, "Action not invoked from main thread."); + Assert.False(invoked, "Action invoked early."); + + var aggregateEx = Assert.Throws(() => task.Wait(100)); + Assert.IsInstanceOf(aggregateEx.InnerException); + Assert.True(invoked, "Action not invoked."); + } + + [Test] + public async Task TestInvokeOnMainThreadWithAsyncAction() + { + bool calledFromMainThread = false; + Device.PlatformServices = MockPlatformServices(() => calledFromMainThread = true, + invokeOnMainThread: action => Task.Delay(50).ContinueWith(_ => action())); + + bool invoked = false; + var task = Device.InvokeOnMainThreadAsync(async () => { invoked = true; }); + Assert.True(calledFromMainThread, "Action not invoked from main thread."); + Assert.False(invoked, "Action invoked early."); + + await task; + Assert.True(invoked, "Action not invoked."); + } + + [Test] + public async Task TestInvokeOnMainThreadWithAsyncActionError() + { + bool calledFromMainThread = false; + Device.PlatformServices = MockPlatformServices(() => calledFromMainThread = true, + invokeOnMainThread: action => Task.Delay(50).ContinueWith(_ => action())); + + bool invoked = false; + async Task boom() { invoked = true; throw new ApplicationException(); } + var task = Device.InvokeOnMainThreadAsync(boom); + Assert.True(calledFromMainThread, "Action not invoked from main thread."); + Assert.False(invoked, "Action invoked early."); + + var aggregateEx = Assert.Throws(() => task.Wait(100)); + Assert.IsInstanceOf(aggregateEx.InnerException); + Assert.True(invoked, "Action not invoked."); + } + + [Test] + public void InvokeOnMainThreadThrowsWhenNull() + { + Device.PlatformServices = null; + Assert.Throws(() => Device.BeginInvokeOnMainThread(() => { })); + Assert.Throws(() => Device.InvokeOnMainThreadAsync(() => { }).Wait(100)); + Assert.Throws(() => Device.InvokeOnMainThreadAsync(() => true).Wait(100)); + Assert.Throws(() => Device.InvokeOnMainThreadAsync(async () => { }).Wait(100)); + Assert.Throws(() => Device.InvokeOnMainThreadAsync(async () => true).Wait(100)); + } + + private IPlatformServices MockPlatformServices(Action onInvokeOnMainThread, Action invokeOnMainThread = null) + { + return new MockPlatformServices( + invokeOnMainThread: action => + { + onInvokeOnMainThread(); + + if (invokeOnMainThread == null) + action(); + else + invokeOnMainThread(action); + }); + } + } +} diff --git a/Xamarin.Forms.Core.UnitTests/ViewUnitTests.cs b/Xamarin.Forms.Core.UnitTests/ViewUnitTests.cs index 6e46ab63b34..b937130e444 100644 --- a/Xamarin.Forms.Core.UnitTests/ViewUnitTests.cs +++ b/Xamarin.Forms.Core.UnitTests/ViewUnitTests.cs @@ -563,24 +563,6 @@ public void TestUnFocusedEvent () Assert.True (fired); } - [Test] - public void TestBeginInvokeOnMainThread () - { - Device.PlatformServices = new MockPlatformServices (invokeOnMainThread: action => action ()); - - bool invoked = false; - Device.BeginInvokeOnMainThread (() => invoked = true); - - Assert.True (invoked); - } - - [Test] - public void InvokeOnMainThreadThrowsWhenNull () - { - Device.PlatformServices = null; - Assert.Throws(() => Device.BeginInvokeOnMainThread (() => { })); - } - [Test] public void TestOpenUriAction () { diff --git a/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj b/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj index afeea9eb802..eb0f9d1f558 100644 --- a/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj +++ b/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj @@ -73,6 +73,7 @@ + diff --git a/Xamarin.Forms.Core/Device.cs b/Xamarin.Forms.Core/Device.cs index 57674b123c4..5ccfcb3154b 100644 --- a/Xamarin.Forms.Core/Device.cs +++ b/Xamarin.Forms.Core/Device.cs @@ -124,8 +124,8 @@ public static Task InvokeOnMainThreadAsync(Func func) public static Task InvokeOnMainThreadAsync(Action action) { - Func dummyFunc = () => { action(); return null; }; - return InvokeOnMainThreadAsync(dummyFunc); + string wrapAction() { action(); return null; } + return InvokeOnMainThreadAsync(wrapAction); } public static Task InvokeOnMainThreadAsync(Func> funcTask) @@ -151,8 +151,8 @@ public static Task InvokeOnMainThreadAsync(Func> funcTask) public static Task InvokeOnMainThreadAsync(Func funcTask) { - Func> dummyFunc = async () => { await funcTask().ConfigureAwait(false); return null; }; - return InvokeOnMainThreadAsync(dummyFunc); + async Task wrapFunction() { await funcTask().ConfigureAwait(false); return null; } + return InvokeOnMainThreadAsync(wrapFunction); } public static async Task GetMainThreadSynchronizationContextAsync()