Skip to content

Commit

Permalink
Merge pull request nunit#4883 from nunit/AsyncSingleThread
Browse files Browse the repository at this point in the history
Async Test continuations now also run on the same thread when SIngleThreaded is used.
  • Loading branch information
stevenaw authored Dec 12, 2024
2 parents f597da8 + 18f9331 commit f46c6d8
Show file tree
Hide file tree
Showing 14 changed files with 48 additions and 43 deletions.
2 changes: 1 addition & 1 deletion src/NUnitFramework/framework/Assert.Exceptions.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public partial class Assert
Exception? caughtException = null;
try
{
AsyncToSyncAdapter.Await(code.Invoke);
AsyncToSyncAdapter.Await(TestExecutionContext.CurrentContext, code.Invoke);
}
catch (Exception e)
{
Expand Down
2 changes: 1 addition & 1 deletion src/NUnitFramework/framework/Assert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ public static void Multiple(AsyncTestDelegate testDelegate)
{
using (EnterMultipleScope())
{
AsyncToSyncAdapter.Await(testDelegate.Invoke);
AsyncToSyncAdapter.Await(TestExecutionContext.CurrentContext, testDelegate.Invoke);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/NUnitFramework/framework/Constraints/Constraint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ protected Constraint(params object?[] args)
public virtual ConstraintResult ApplyTo<TActual>(ActualValueDelegate<TActual> del)
{
if (AsyncToSyncAdapter.IsAsyncOperation(del))
return ApplyTo(AsyncToSyncAdapter.Await(() => del.Invoke()));
return ApplyTo(AsyncToSyncAdapter.Await(TestExecutionContext.CurrentContext, () => del.Invoke()));

return ApplyTo(GetTestObject(del));
}
Expand Down
8 changes: 0 additions & 8 deletions src/NUnitFramework/framework/Constraints/DelayedConstraint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -361,14 +361,6 @@ public override async Task<ConstraintResult> ApplyToAsync<TActual>(Func<Task<TAc
return new DelegatingConstraintResult(this, await BaseConstraint.ApplyToAsync(taskDel));
}

private static object? InvokeDelegate<T>(ActualValueDelegate<T> del)
{
if (AsyncToSyncAdapter.IsAsyncOperation(del))
return AsyncToSyncAdapter.Await(() => del.Invoke());

return del();
}

/// <summary>
/// Returns the string representation of the constraint.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ public AsyncEnumeratorWrapper(IAsyncEnumerator<object?> asyncEnumerator)
public object? Current => _asyncEnumerator.Current;

public void Dispose()
=> AsyncToSyncAdapter.Await(() => _asyncEnumerator.DisposeAsync());
=> AsyncToSyncAdapter.Await(context: null, () => _asyncEnumerator.DisposeAsync());

public bool MoveNext()
=> AsyncToSyncAdapter.Await<bool>(() => _asyncEnumerator.MoveNextAsync());
=> AsyncToSyncAdapter.Await<bool>(context: null, () => _asyncEnumerator.MoveNextAsync());

public void Reset()
=> throw new InvalidOperationException("Can not reset an async enumerable.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,10 @@ public AsyncEnumeratorWrapper(AsyncEnumerableShapeInfo shape, object asyncEnumer
public object? Current => _getCurrentMethod.Invoke();

public void Dispose()
=> AsyncToSyncAdapter.Await(() => _disposeAsyncMethod.Invoke(_asyncEnumerator, null));
=> AsyncToSyncAdapter.Await(context: null, () => _disposeAsyncMethod.Invoke(_asyncEnumerator, null));

public bool MoveNext()
=> AsyncToSyncAdapter.Await<bool>(() => _moveNextAsyncMethod.Invoke());
=> AsyncToSyncAdapter.Await<bool>(context: null, () => _moveNextAsyncMethod.Invoke());

public void Reset()
=> throw new InvalidOperationException("Can not reset an async enumerable.");
Expand Down
14 changes: 7 additions & 7 deletions src/NUnitFramework/framework/Internal/AsyncToSyncAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ public static bool IsAsyncOperation(Delegate @delegate)
return IsAsyncOperation(@delegate.GetMethodInfo());
}

public static object? Await(Func<object?> invoke)
=> Await<object?>(invoke);
public static object? Await(TestExecutionContext? context, Func<object?> invoke)
=> Await<object?>(context, invoke);

public static TResult? Await<TResult>(Func<object?> invoke)
public static TResult? Await<TResult>(TestExecutionContext? context, Func<object?> invoke)
{
Guard.ArgumentNotNull(invoke, nameof(invoke));

using (InitializeExecutionEnvironment())
using (InitializeExecutionEnvironment(context))
{
var awaiter = AwaitAdapter.FromAwaitable(invoke.Invoke());

Expand All @@ -41,10 +41,10 @@ public static bool IsAsyncOperation(Delegate @delegate)
return (TResult?)awaiter.GetResult();
}
}

private static IDisposable? InitializeExecutionEnvironment()
private static IDisposable? InitializeExecutionEnvironment(TestExecutionContext? executionContext)
{
if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA)
if (executionContext?.IsSingleThreaded == true ||
Thread.CurrentThread.GetApartmentState() == ApartmentState.STA)
{
var context = SynchronizationContext.Current;
if (context is null || context.GetType() == typeof(SynchronizationContext))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ private void RunSetUpOrTearDownMethod(TestExecutionContext context, IMethodInfo
var methodInfo = MethodInfoCache.Get(method);

if (methodInfo.IsAsyncOperation)
AsyncToSyncAdapter.Await(() => InvokeMethod(method, context));
AsyncToSyncAdapter.Await(context, () => InvokeMethod(method, context));
else
InvokeMethod(method, context);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public override TestResult Execute(TestExecutionContext context)

if (methodInfo.IsAsyncOperation)
{
return AsyncToSyncAdapter.Await(() => InvokeTestMethod(context, lastParameterAcceptsCancellationToken));
return AsyncToSyncAdapter.Await(context, () => InvokeTestMethod(context, lastParameterAcceptsCancellationToken));
}

return InvokeTestMethod(context, lastParameterAcceptsCancellationToken);
Expand Down
4 changes: 2 additions & 2 deletions src/NUnitFramework/framework/Internal/DisposeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ public static void EnsureDisposed(object? value)
#if NETFRAMEWORK
if (TryGetAsyncDispose(value.GetType(), out var method))
{
AsyncToSyncAdapter.Await(() => method.Invoke(value, null));
AsyncToSyncAdapter.Await(context: null, () => method.Invoke(value, null));
}
#else
if (value is IAsyncDisposable asyncDisposable)
{
AsyncToSyncAdapter.Await(() => asyncDisposable.DisposeAsync());
AsyncToSyncAdapter.Await(context: null, () => asyncDisposable.DisposeAsync());
}
#endif
else if (value is IDisposable disposable)
Expand Down
2 changes: 1 addition & 1 deletion src/NUnitFramework/framework/Internal/ExceptionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ private static List<Exception> FlattenExceptionHierarchy(Exception exception)
{
try
{
AsyncToSyncAdapter.Await(parameterlessDelegate.DynamicInvokeWithTransparentExceptions);
AsyncToSyncAdapter.Await(TestExecutionContext.CurrentContext, parameterlessDelegate.DynamicInvokeWithTransparentExceptions);
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal static class MethodInfoExtensions
public static TReturn? InvokeMaybeAwait<TReturn>(this MethodInfo m, object?[]? methodArgs)
{
if (AsyncToSyncAdapter.IsAsyncOperation(m))
return (TReturn?)AsyncToSyncAdapter.Await(() => m.Invoke(null, methodArgs));
return (TReturn?)AsyncToSyncAdapter.Await(context: null, () => m.Invoke(null, methodArgs));
return (TReturn?)m.Invoke(null, methodArgs);
}

Expand Down
41 changes: 27 additions & 14 deletions src/NUnitFramework/tests/Attributes/SingleThreadedFixtureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using NUnit.Framework.Interfaces;
using NUnit.TestData;
using NUnit.Framework.Tests.TestUtilities;
using System.Threading.Tasks;

namespace NUnit.Framework.Tests.Attributes
{
Expand Down Expand Up @@ -37,20 +38,6 @@ public void TestWithDifferentApartmentIsInvalid()
CheckTestIsInvalid<SingleThreadedFixture_TestWithDifferentApartment>("may not specify a different apartment");
}

#if NETCOREAPP
[Platform(Include = "Win, Mono")]
#endif
[SingleThreaded]
public class SingleThreadedFixtureWithApartmentStateTests : ThreadingTests
{
[Test, Apartment(ApartmentState.MTA)]
public void TestWithSameApartmentIsValid()
{
Assert.That(Thread.CurrentThread, Is.EqualTo(ParentThread));
Assert.That(Thread.CurrentThread.GetApartmentState(), Is.EqualTo(ApartmentState.MTA));
}
}

private void CheckTestIsInvalid<TFixture>(string reason)
{
var result = TestBuilder.RunTestFixture(typeof(TFixture));
Expand All @@ -60,6 +47,20 @@ private void CheckTestIsInvalid<TFixture>(string reason)
}
}

#if NETCOREAPP
[Platform(Include = "Win, Mono")]
#endif
[SingleThreaded]
public class SingleThreadedFixtureWithApartmentStateTests : ThreadingTests
{
[Test, Apartment(ApartmentState.MTA)]
public void TestWithSameApartmentIsValid()
{
Assert.That(Thread.CurrentThread, Is.EqualTo(ParentThread));
Assert.That(Thread.CurrentThread.GetApartmentState(), Is.EqualTo(ApartmentState.MTA));
}
}

#if NETCOREAPP
[Platform(Include = "Win")]
#endif
Expand All @@ -80,4 +81,16 @@ public void TestWithSameApartmentStateIsValid()
Assert.That(GetApartmentState(Thread.CurrentThread), Is.EqualTo(ApartmentState.STA));
}
}

[SingleThreaded]
public class SingleThreadedFixtureWithAsyncTests : ThreadingTests
{
[Test]
public async Task AsyncTestRunsOnSameThread()
{
Assert.That(Thread.CurrentThread, Is.EqualTo(ParentThread));
await Task.Yield();
Assert.That(Thread.CurrentThread, Is.EqualTo(ParentThread));
}
}
}
2 changes: 1 addition & 1 deletion src/NUnitFramework/tests/HelperConstraints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public override ConstraintResult ApplyTo<TActual>(TActual actual)
if (AsyncToSyncAdapter.IsAsyncOperation(@delegate))
{
stopwatch.Start();
AsyncToSyncAdapter.Await(() => @delegate.DynamicInvoke());
AsyncToSyncAdapter.Await(TestExecutionContext.CurrentContext, () => @delegate.DynamicInvoke());
stopwatch.Stop();
}
else
Expand Down

0 comments on commit f46c6d8

Please sign in to comment.