Last active
May 13, 2020 03:29
-
-
Save jnm2/9c2b5d74d144852718e5b788d22b7fc0 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// See AsyncVoidVerificationScopeTests.cs for more examples and expected behavior. | |
using (new AsyncVoidVerificationScope()) | |
menuService.GlobalInvoke(UICommands.SaveFile); | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 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
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?