diff --git a/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsSynchronizationContextProvider.cs b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsSynchronizationContextProvider.cs index 1aa8b6f..76a74fe 100644 --- a/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsSynchronizationContextProvider.cs +++ b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsSynchronizationContextProvider.cs @@ -4,18 +4,23 @@ using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; -using System.Windows.Forms; /// +[SuppressMessage( + "Usage", + "VSTHRD001:Avoid legacy thread switching APIs", + Justification = "As designed." +)] internal sealed class WindowsFormsSynchronizationContextProvider : IWindowsFormsSynchronizationContextProvider { - internal WindowsFormsSynchronizationContext Context { get; set; } = default!; + internal SynchronizationContext Context { get; set; } = default!; /// public void Invoke([NotNull] Action action) { ArgumentNullException.ThrowIfNull(action); + ArgumentNullException.ThrowIfNull(Context); Context.Send( delegate @@ -31,6 +36,7 @@ public void Invoke([NotNull] Action action) public TResult Invoke([NotNull] Func action) { ArgumentNullException.ThrowIfNull(action); + ArgumentNullException.ThrowIfNull(Context); TResult result = default!; Context.Send( @@ -40,6 +46,7 @@ public TResult Invoke([NotNull] Func action) }, null ); + return result; } @@ -48,6 +55,7 @@ public TResult Invoke([NotNull] Func action) public TResult Invoke([NotNull] Func action, TInput input) { ArgumentNullException.ThrowIfNull(action); + ArgumentNullException.ThrowIfNull(Context); TResult result = default!; Context.Send( @@ -57,6 +65,7 @@ public TResult Invoke([NotNull] Func action, T }, null ); + return result; } @@ -67,6 +76,7 @@ public async ValueTask InvokeAsync( ) { ArgumentNullException.ThrowIfNull(action); + ArgumentNullException.ThrowIfNull(Context); var tcs = new TaskCompletionSource(); Context.Post( @@ -95,6 +105,7 @@ public async ValueTask InvokeAsync( ) { ArgumentNullException.ThrowIfNull(action); + ArgumentNullException.ThrowIfNull(Context); var tcs = new TaskCompletionSource(); Context.Post( @@ -112,6 +123,7 @@ public async ValueTask InvokeAsync( }, tcs ); + return await tcs.Task.WaitAsync(cancellationToken).ConfigureAwait(true); } @@ -123,6 +135,7 @@ public async ValueTask InvokeAsync( ) { ArgumentNullException.ThrowIfNull(action); + ArgumentNullException.ThrowIfNull(Context); var tcs = new TaskCompletionSource(); Context.Post( @@ -140,6 +153,7 @@ public async ValueTask InvokeAsync( }, tcs ); + return await tcs.Task.WaitAsync(cancellationToken).ConfigureAwait(true); } } diff --git a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/WindowsFormsSynchronizationContextProviderTests.cs b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/WindowsFormsSynchronizationContextProviderTests.cs new file mode 100644 index 0000000..650ed72 --- /dev/null +++ b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/WindowsFormsSynchronizationContextProviderTests.cs @@ -0,0 +1,365 @@ +namespace NetEvolve.Extensions.Hosting.WinForms.Tests.Unit.Internals; + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using NetEvolve.Extensions.Hosting.WinForms.Internals; +using Xunit; + +public class WindowsFormsSynchronizationContextProviderTests +{ + [Fact] + public void Invoke_ActionNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = Assert.Throws("action", () => provider.Invoke(null!)); + } + + [Fact] + public void Invoke_Action_ContextNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = Assert.Throws("Context", () => provider.Invoke(() => { })); + } + + [Fact] + public void Invoke_Action_Expected() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act / Assert + provider.Invoke(() => { }); + + Assert.True(true, "No exception was thrown."); + } + + [Fact] + public void Invoke_FuncNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = Assert.Throws("action", () => provider.Invoke((Func)null!)); + } + + [Fact] + public void Invoke_Func_ContextNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = Assert.Throws("Context", () => provider.Invoke(() => 42)); + } + + [Fact] + public void Invoke_Func_Expected() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + var result = provider.Invoke(() => 42); + + // Assert + Assert.Equal(42, result); + } + + [Fact] + public void Invoke_FuncWithInputNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = Assert.Throws( + "action", + () => provider.Invoke((Func)null!, 42) + ); + } + + [Fact] + public void Invoke_FuncWithInput_ContextNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = Assert.Throws( + "Context", + () => provider.Invoke((int input) => input * 2, 21) + ); + } + + [Fact] + public void Invoke_FuncWithInput_Expected() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + var result = provider.Invoke((int input) => input * 2, 21); + + // Assert + Assert.Equal(42, result); + } + + [Fact] + public async Task InvokeAsync_ActionNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = await Assert.ThrowsAsync( + "action", + async () => await provider.InvokeAsync(null!) + ); + } + + [Fact] + public async Task InvokeAsync_Action_ContextNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = await Assert.ThrowsAsync( + "Context", + async () => await provider.InvokeAsync(() => { }) + ); + } + + [Fact] + public async Task InvokeAsync_ActionThrows_ExpectedException() + { // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + _ = await Assert.ThrowsAsync( + async () => + await provider.InvokeAsync(() => + { + throw new NotImplementedException(); + }) + ); + } + + [Fact] + public async Task InvokeAsync_Action_CancellationTokenCanceled_ThrowsTaskCanceledException() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + _ = await Assert.ThrowsAsync( + async () => await provider.InvokeAsync(() => { }, new CancellationToken(true)) + ); + } + + [Fact] + public async Task InvokeAsync_FuncNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = await Assert.ThrowsAsync( + "action", + async () => await provider.InvokeAsync((Func)null!) + ); + } + + [Fact] + public async Task InvokeAsync_Func_ContextNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = await Assert.ThrowsAsync( + "Context", + async () => await provider.InvokeAsync(() => 42) + ); + } + + [Fact] + public async Task InvokeAsync_FuncThrows_ExpectedException() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + _ = await Assert.ThrowsAsync( + async () => + await provider.InvokeAsync(() => + { + throw new NotImplementedException(); + }) + ); + } + + [Fact] + public async Task InvokeAsync_Func_CancellationTokenCanceled_ThrowsTaskCanceledException() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + _ = await Assert.ThrowsAsync( + async () => await provider.InvokeAsync(() => 42, new CancellationToken(true)) + ); + } + + [Fact] + public async Task InvokeAsync_Func_Expected() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + var result = await provider.InvokeAsync(() => 42); + + // Assert + Assert.Equal(42, result); + } + + [Fact] + public async Task InvokeAsync_FuncWithInputNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = await Assert.ThrowsAsync( + "action", + async () => await provider.InvokeAsync((Func)null!, 42) + ); + } + + [Fact] + public async Task InvokeAsync_FuncWithInput_ContextNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = await Assert.ThrowsAsync( + "Context", + async () => await provider.InvokeAsync((int input) => input * 2, 21) + ); + } + + [Fact] + public async Task InvokeAsync_FuncWithInputThrows_ExpectedException() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + _ = await Assert.ThrowsAsync( + async () => + await provider.InvokeAsync( + (int input) => + { + throw new NotImplementedException(); + }, + 42 + ) + ); + } + + [Fact] + public async Task InvokeAsync_FuncWithInput_CancellationTokenCanceled_ThrowsTaskCanceledException() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + _ = await Assert.ThrowsAsync( + async () => + await provider.InvokeAsync( + (int input) => input * 2, + 21, + new CancellationToken(true) + ) + ); + } + + [Fact] + public async Task InvokeAsync_FuncWithInput_Expected() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + var result = await provider.InvokeAsync((int input) => input * 2, 21); + + // Assert + Assert.Equal(42, result); + } +}