-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Add runtime async tests for catching non-Exception throws #130506
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Copilot
wants to merge
4
commits into
main
Choose a base branch
from
copilot/runtime-async-tests-for-non-exceptions
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Empty file.
46 changes: 46 additions & 0 deletions
46
src/tests/async/runtime-wrapped-exception/NoWrapThrowers.cs
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| } | ||
| } |
6 changes: 6 additions & 0 deletions
6
src/tests/async/runtime-wrapped-exception/NoWrapThrowers.csproj
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <ItemGroup> | ||
| <Compile Include="NoWrapThrowers.cs" /> | ||
| <ProjectReference Include="NonExceptionThrower.ilproj" /> | ||
| </ItemGroup> | ||
| </Project> |
24 changes: 24 additions & 0 deletions
24
src/tests/async/runtime-wrapped-exception/NonExceptionThrower.il
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
10 changes: 10 additions & 0 deletions
10
src/tests/async/runtime-wrapped-exception/NonExceptionThrower.ilproj
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk.IL"> | ||
| <PropertyGroup> | ||
| <OutputType>Library</OutputType> | ||
| </PropertyGroup> | ||
| <ItemGroup> | ||
| <!-- The C# helper attribute injected by the directory props is not applicable to IL. --> | ||
| <Compile Remove="..\RuntimeAsyncMethodGenerationAttribute.cs" /> | ||
| <Compile Include="NonExceptionThrower.il" /> | ||
| </ItemGroup> | ||
| </Project> |
125 changes: 125 additions & 0 deletions
125
src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.cs
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Task> 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<object> ObserveNonException(Func<Task> 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(); | ||
| } | ||
| } | ||
7 changes: 7 additions & 0 deletions
7
src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.csproj
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <ItemGroup> | ||
| <Compile Include="$(MSBuildProjectName).cs" /> | ||
| <ProjectReference Include="NonExceptionThrower.ilproj" /> | ||
| <ProjectReference Include="NoWrapThrowers.csproj" /> | ||
| </ItemGroup> | ||
| </Project> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
async2 behavior makes more sense.
This is a corner case though. Not a lot of value in changing async1 behavior here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot let's
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 8fef2c4. Kept the async2 expectations and the difference comment as-is, and the async1 divergent case stays blocked — now via the comment "async1 behavior here is terminal to the process, thus we will not test it" instead of the
[ActiveIssue]link.Note
This comment was generated by GitHub Copilot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correction: the change is in commit 362e9c2 (not 8fef2c4).
Note
This comment was generated by GitHub Copilot.