diff --git a/reflect_ai.csx b/reflect_ai.csx new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/src/tests/async/runtime-wrapped-exception/NoWrapThrowers.cs b/src/tests/async/runtime-wrapped-exception/NoWrapThrowers.cs new file mode 100644 index 00000000000000..97c9c8631b2622 --- /dev/null +++ b/src/tests/async/runtime-wrapped-exception/NoWrapThrowers.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +// This assembly opts OUT of wrapping non-Exception throws, in contrast to the +// C# default (WrapNonExceptionThrows = true) used by the main test assembly and +// by CoreLib. The async methods below are otherwise identical to the ones in the +// main test assembly; keeping them in a separate assembly is the only way to +// exercise the WrapNonExceptionThrows = false configuration, since the setting is +// assembly-scoped and decided by the frame that catches the exception. +[assembly: RuntimeCompatibility(WrapNonExceptionThrows = false)] + +public static class NoWrapThrowers +{ + // Runtime async (async2) throwing a non-Exception after suspending. + public static async Task ThrowAfterYieldAsync2() + { + await Task.Yield(); + NonExceptionThrower.ThrowNonException(); + } + + // Compiler state machine (async1) throwing a non-Exception after suspending. + [RuntimeAsyncMethodGeneration(false)] + public static async Task ThrowAfterYieldAsync1() + { + await Task.Yield(); + NonExceptionThrower.ThrowNonException(); + } + + // Runtime async (async2) throwing a non-Exception before suspending. + public static async Task ThrowBeforeYieldAsync2() + { + NonExceptionThrower.ThrowNonException(); + await Task.Yield(); + } + + // Compiler state machine (async1) throwing a non-Exception before suspending. + [RuntimeAsyncMethodGeneration(false)] + public static async Task ThrowBeforeYieldAsync1() + { + NonExceptionThrower.ThrowNonException(); + await Task.Yield(); + } +} diff --git a/src/tests/async/runtime-wrapped-exception/NoWrapThrowers.csproj b/src/tests/async/runtime-wrapped-exception/NoWrapThrowers.csproj new file mode 100644 index 00000000000000..6981b089475cc1 --- /dev/null +++ b/src/tests/async/runtime-wrapped-exception/NoWrapThrowers.csproj @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/tests/async/runtime-wrapped-exception/NonExceptionThrower.il b/src/tests/async/runtime-wrapped-exception/NonExceptionThrower.il new file mode 100644 index 00000000000000..3a5cfd42de722c --- /dev/null +++ b/src/tests/async/runtime-wrapped-exception/NonExceptionThrower.il @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Helper that throws an object which does not derive from System.Exception. +// This is only expressible in IL, so it lives in its own IL assembly. The thrown +// object (a string) is what a catch handler in a WrapNonExceptionThrows=true +// assembly observes as RuntimeWrappedException.WrappedException. + +.assembly extern System.Runtime { .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A) .ver 4:0:0:0 } + +.assembly NonExceptionThrower +{ +} + +.class public auto ansi abstract sealed beforefieldinit NonExceptionThrower + extends [System.Runtime]System.Object +{ + .method public hidebysig static void ThrowNonException() cil managed noinlining + { + .maxstack 8 + ldstr "A non-Exception object thrown from IL" + throw + } +} diff --git a/src/tests/async/runtime-wrapped-exception/NonExceptionThrower.ilproj b/src/tests/async/runtime-wrapped-exception/NonExceptionThrower.ilproj new file mode 100644 index 00000000000000..da615aeb006351 --- /dev/null +++ b/src/tests/async/runtime-wrapped-exception/NonExceptionThrower.ilproj @@ -0,0 +1,10 @@ + + + Library + + + + + + + diff --git a/src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.cs b/src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.cs new file mode 100644 index 00000000000000..1de34df3001157 --- /dev/null +++ b/src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Xunit; + +// Regression tests for https://github.com/dotnet/runtime/issues/123194. +// +// IL allows throwing an object that does not derive from System.Exception. When +// such an object propagates into a catch handler, it is (or is not) wrapped in a +// RuntimeWrappedException depending on the RuntimeCompatibilityAttribute +// WrapNonExceptionThrows setting of the assembly that owns the catch handler. +// +// The goal of runtime async (async2) is to match the traditional compiler +// generated state machine (async1) behavior. These tests exercise both async +// forms (via [RuntimeAsyncMethodGeneration]) throwing a non-Exception object, +// once from an assembly with WrapNonExceptionThrows = true (this assembly, which +// matches the C# default and CoreLib) and once from an assembly with +// WrapNonExceptionThrows = false (NoWrapThrowers), for throws both before and +// after a suspension point. +public class RuntimeAsyncNonExceptionThrows +{ + private const string ThrownObject = "A non-Exception object thrown from IL"; + + // WrapNonExceptionThrows = true (this assembly): the non-Exception is observed + // as a RuntimeWrappedException by the awaiting caller for both async forms. + + [Fact] + public static void CatchAfterYield_Async2() + => AssertNonExceptionWrapped(ThrowAfterYieldAsync2); + + [Fact] + public static void CatchAfterYield_Async1() + => AssertNonExceptionWrapped(ThrowAfterYieldAsync1); + + [Fact] + public static void CatchBeforeYield_Async2() + => AssertNonExceptionWrapped(ThrowBeforeYieldAsync2); + + [Fact] + public static void CatchBeforeYield_Async1() + => AssertNonExceptionWrapped(ThrowBeforeYieldAsync1); + + // WrapNonExceptionThrows = false (NoWrapThrowers assembly). + + [Fact] + public static void CatchAfterYield_NoWrap_Async2() + => AssertNonExceptionWrapped(NoWrapThrowers.ThrowAfterYieldAsync2); + + // async2 and async1 behave differently for a non-Exception thrown after a + // suspension point in a WrapNonExceptionThrows = false assembly: + // * async2 faults the returned Task with a RuntimeWrappedException, which the + // caller observes just like the wrap=true case above. + // * async1 lets the raw non-Exception escape the resumed state machine onto + // the thread pool where it becomes an unhandled exception and crashes the + // process, so it can never be observed by the caller. + // async1 behavior here is terminal to the process, thus we will not test it: + // + // [Fact] + // public static void CatchAfterYield_NoWrap_Async1() + // => AssertNonExceptionWrapped(NoWrapThrowers.ThrowAfterYieldAsync1); + + [Fact] + public static void CatchBeforeYield_NoWrap_Async2() + => AssertNonExceptionWrapped(NoWrapThrowers.ThrowBeforeYieldAsync2); + + [Fact] + public static void CatchBeforeYield_NoWrap_Async1() + => AssertNonExceptionWrapped(NoWrapThrowers.ThrowBeforeYieldAsync1); + + private static void AssertNonExceptionWrapped(Func thrower) + { + object wrapped = ObserveNonException(thrower).GetAwaiter().GetResult(); + Assert.Equal(ThrownObject, wrapped); + } + + // Awaits the throwing method and returns the wrapped non-Exception object. + // The catch handler lives in this WrapNonExceptionThrows = true assembly, so a + // propagating non-Exception is caught as a RuntimeWrappedException. Returns null + // if nothing was thrown so the assertion above fails with a clear diff. + private static async Task ObserveNonException(Func thrower) + { + try + { + await thrower(); + return null; + } + catch (RuntimeWrappedException e) + { + return e.WrappedException; + } + } + + // Runtime async (async2) throwing a non-Exception after suspending. + public static async Task ThrowAfterYieldAsync2() + { + await Task.Yield(); + NonExceptionThrower.ThrowNonException(); + } + + // Compiler state machine (async1) throwing a non-Exception after suspending. + [RuntimeAsyncMethodGeneration(false)] + public static async Task ThrowAfterYieldAsync1() + { + await Task.Yield(); + NonExceptionThrower.ThrowNonException(); + } + + // Runtime async (async2) throwing a non-Exception before suspending. + public static async Task ThrowBeforeYieldAsync2() + { + NonExceptionThrower.ThrowNonException(); + await Task.Yield(); + } + + // Compiler state machine (async1) throwing a non-Exception before suspending. + [RuntimeAsyncMethodGeneration(false)] + public static async Task ThrowBeforeYieldAsync1() + { + NonExceptionThrower.ThrowNonException(); + await Task.Yield(); + } +} diff --git a/src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.csproj b/src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.csproj new file mode 100644 index 00000000000000..3849e3f6c3ccf7 --- /dev/null +++ b/src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.csproj @@ -0,0 +1,7 @@ + + + + + + +