Skip to content

Instantly share code, notes, and snippets.

@jnm2
Last active May 13, 2020 03:29
Show Gist options
  • Save jnm2/9c2b5d74d144852718e5b788d22b7fc0 to your computer and use it in GitHub Desktop.
Save jnm2/9c2b5d74d144852718e5b788d22b7fc0 to your computer and use it in GitHub Desktop.
// See AsyncVoidVerificationScopeTests.cs for more examples and expected behavior.
using (new AsyncVoidVerificationScope())
menuService.GlobalInvoke(UICommands.SaveFile);
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using NUnit.Framework.Internal;
public sealed class AsyncVoidVerificationScope : IDisposable
{
// http://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/TPLETWProvider.cs,30
private static readonly Guid TplEventSourceGuid = new Guid("2e5dba47-a3d2-4d16-8ee0-6671ffdcd7b5");
// http://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/TPLETWProvider.cs,130
private const EventKeywords AsyncCausalityOperationKeyword = (EventKeywords)8;
// http://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/TPLETWProvider.cs,200
private const int TRACEOPERATIONSTART_ID = 14;
private const int TRACEOPERATIONSTOP_ID = 15;
// http://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/AsyncCausalityTracer.cs,42
private const int AsyncCausalityStatusError = 3;
private readonly EventListener eventListener = new EventListener();
private readonly ConcurrentDictionary<int, string> asyncMethodsInFlight = new ConcurrentDictionary<int, string>();
private readonly ConcurrentBag<string> asyncExceptions = new ConcurrentBag<string>();
private static object GetCurrentTestInheritableProperty(string name)
{
for (var test = TestExecutionContext.CurrentContext.CurrentTest; test != null; test = test.Parent as Test)
if (test.Properties.ContainsKey(name))
return test.Properties.Get(name);
return null;
}
public AsyncVoidVerificationScope()
{
Assert.That(GetCurrentTestInheritableProperty("ParallelScope"), Is.EqualTo(ParallelScope.None),
$"{nameof(AsyncVoidVerificationScope)} may only be used by tests that have [NonParallelizable] applied.");
eventListener.EventWritten += OnEventWritten;
eventListener.EventSourceCreated += OnEventSourceCreated;
// Cause the type initializer for System.Threading.Tasks.TplEtwProvider to run.
// Otherwise async method builders starting events will be missed.
Type.GetType("System.Threading.Tasks.TplEtwProvider, mscorlib", true).GetField("Log").GetValue(null);
}
public void Dispose()
{
AssertNoInFlightAsyncVoidMethods();
eventListener.Dispose();
}
private static void OnEventSourceCreated(object sender, EventSourceCreatedEventArgs e)
{
if (e.EventSource.Guid != TplEventSourceGuid) return;
var eventListener = (EventListener)sender;
eventListener.EnableEvents(e.EventSource, EventLevel.Informational, AsyncCausalityOperationKeyword | (EventKeywords)0x20000);
}
private void OnEventWritten(object sender, EventWrittenEventArgs e)
{
switch (e.EventId)
{
case TRACEOPERATIONSTART_ID:
{
// http://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/TPLETWProvider.cs,636
var operationName = (string)e.Payload[1];
// http://referencesource.microsoft.com/#mscorlib/system/runtime/compilerservices/AsyncMethodBuilder.cs,169
var shouldTrack = operationName.StartsWith("Async: ");
if (!shouldTrack) return;
var taskId = (int)e.Payload[0];
if (!asyncMethodsInFlight.TryAdd(taskId, operationName))
throw new NotImplementedException("Task already associated with an in-flight async void method.");
break;
}
case TRACEOPERATIONSTOP_ID:
{
// http://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/TPLETWProvider.cs,674
var taskId = (int)e.Payload[0];
var status = (int)e.Payload[1];
var isTracked = asyncMethodsInFlight.TryRemove(taskId, out var operationName);
if (status == AsyncCausalityStatusError)
{
if (isTracked)
{
asyncExceptions.Add(operationName);
}
else
{
var trace = new StackTrace(skipFrames: 1);
for (var i = 0; i < trace.FrameCount - 1; i++)
{
var method = trace.GetFrame(i).GetMethod();
if (method == null || method.Name != "SetException") continue;
if (method.DeclaringType.FullName != "System.Runtime.CompilerServices.AsyncVoidMethodBuilder") continue;
asyncExceptions.Add("Async: " + trace.GetFrame(i + 1).GetMethod().DeclaringType.Name);
break;
}
}
}
break;
}
}
}
private void AssertNoInFlightAsyncVoidMethods()
{
var inFlightMethods = asyncMethodsInFlight.ToArray();
var asyncExceptionMethods = asyncExceptions.ToArray();
if (inFlightMethods.Length == 0 && asyncExceptionMethods.Length == 0) return;
var foundTypesByName = new Dictionary<string, (Type type, bool wasInFlight, bool wasException)>();
foreach (var (_, message) in inFlightMethods)
{
var typeName = message.Substring(7);
foundTypesByName[typeName] = (type: null, wasInFlight: true, wasException: false);
}
foreach (var message in asyncExceptionMethods)
{
var typeName = message.Substring(7);
var toFind = foundTypesByName.GetValueOrDefault(typeName);
toFind.wasException = true;
foundTypesByName[typeName] = toFind;
}
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var type in assembly.GetTypes())
{
if (foundTypesByName.TryGetValue(type.Name, out var found))
{
if (found.type != null) throw new NotImplementedException($"Multiple async method builder types have the same name: '{found.type}' and '{type}'");
found.type = type;
foundTypesByName[type.Name] = found;
}
}
}
var inFlightMethodTypes = new List<Type>();
var exceptionMethodTypes = new List<Type>();
foreach (var (typeName, (type, wasInFlight, wasException)) in foundTypesByName)
{
if (type == null) throw new NotImplementedException($"Cannot find async method builder type '{typeName}'");
if (type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Any(field => field.FieldType.FullName == "System.Runtime.CompilerServices.AsyncVoidMethodBuilder"))
{
if (wasInFlight) inFlightMethodTypes.Add(type);
if (wasException) exceptionMethodTypes.Add(type);
}
}
void FailIfAny(IReadOnlyCollection<Type> methodStateMachineTypes, string message)
{
if (methodStateMachineTypes.Count == 0) return;
Assert.Fail(message + Environment.NewLine + string.Join(Environment.NewLine, methodStateMachineTypes));
}
FailIfAny(exceptionMethodTypes, "Exceptions were unhandled in async void methods:");
FailIfAny(inFlightMethodTypes, "Not all async void methods have finished running:");
}
}
using System;
using System.Threading.Tasks;
using NUnit.Framework;
[NonParallelizable]
public static class AsyncVoidVerificationScopeTests
{
[Test]
public static async Task Should_succeed_for_unfinished_async_void_started_before_scope()
{
AsyncVoidAwaitDelay(200);
await Task.Delay(100);
using (new AsyncVoidVerificationScope())
{
}
}
[Test]
public static async Task Should_fail_on_dispose_for_unfinished_async_void()
{
var scope = new AsyncVoidVerificationScope();
try
{
AsyncVoidAwaitDelay(200);
await Task.Delay(100);
}
finally
{
Assert.Throws<AssertionException>(scope.Dispose);
}
}
[Test]
public static async Task Should_succeed_for_finished_async_void()
{
using (new AsyncVoidVerificationScope())
{
AsyncVoidAwaitDelay(100);
await Task.Delay(200);
}
}
[Test]
public static async Task Should_succeed_for_unfinished_async_Task()
{
using (new AsyncVoidVerificationScope())
{
_ = AsyncTaskAwaitDelay(200);
await Task.Delay(100);
}
}
[Test]
public static async Task Should_succeed_for_finished_async_Task()
{
using (new AsyncVoidVerificationScope())
{
_ = AsyncTaskAwaitDelay(100);
await Task.Delay(200);
}
}
private static async void AsyncVoidAwaitDelay(int milliseconds)
{
await Task.Delay(milliseconds);
}
private static async Task AsyncTaskAwaitDelay(int milliseconds)
{
await Task.Delay(milliseconds);
}
[Test]
public static void Should_fail_for_async_void_exception_thrown_synchronously_with_message_pump()
{
WindowsFormsUtils.RunWithMessagePump(() =>
{
Assert.Throws<AssertionException>(() =>
{
using (new AsyncVoidVerificationScope())
AsyncVoidThrowException(asynchronously: false);
});
});
}
[Test]
public static void Should_fail_for_async_void_exception_thrown_asynchronously_with_message_pump()
{
WindowsFormsUtils.RunWithMessagePump(async () =>
{
await AssertAsync.Throws<AssertionException>(async () =>
{
using (new AsyncVoidVerificationScope())
{
AsyncVoidThrowException(asynchronously: true);
await Task.Delay(100);
}
});
});
}
private static async void AsyncVoidThrowException(bool asynchronously)
{
if (asynchronously) await Task.Yield();
throw new Exception($"Test exception thrown {(asynchronously ? "asynchronously" : "synchronously")} by async void method");
}
[Test]
public static void Should_succeed_for_async_Task_exception_thrown_synchronously_with_message_pump()
{
WindowsFormsUtils.RunWithMessagePump(() =>
{
using (new AsyncVoidVerificationScope())
_ = AsyncTaskThrowException(asynchronously: false).Exception;
});
}
[Test]
public static void Should_fail_for_async_Task_exception_thrown_asynchronously_with_message_pump()
{
WindowsFormsUtils.RunWithMessagePump(async () =>
{
await AssertAsync.Throws<AssertionException>(async () =>
{
using (new AsyncVoidVerificationScope())
{
_ = AsyncTaskThrowException(asynchronously: true).Exception;
await Task.Delay(100);
}
});
});
}
private static async Task AsyncTaskThrowException(bool asynchronously)
{
if (asynchronously) await Task.Yield();
throw new Exception($"Test exception thrown {(asynchronously ? "asynchronously" : "synchronously")} by async Task method");
}
}
@lsalamon
Copy link

I have trouble with JIT and investigate issue about method not found in production core. It occurs eventually and tries way to get etw events for jit. Your modules can do this?

@jnm2
Copy link
Author

jnm2 commented May 20, 2019

@lsalamon Just checking that I understand what you're saying. Are you seeing the code above cause MissingMethodException on .NET Core? I have not tried to run this on .NET Core yet, only .NET Framework. It's a bit of a hack even on .NET Framework. I'm not sure what's possible on .NET Core without investigating.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment