Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added reflect_ai.csx
Empty file.
46 changes: 46 additions & 0 deletions src/tests/async/runtime-wrapped-exception/NoWrapThrowers.cs
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();
}
}
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 src/tests/async/runtime-wrapped-exception/NonExceptionThrower.il
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
}
}
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 src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.cs
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.
Comment on lines +56 to +58

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

async2 behavior makes more sense.
This is a corner case though. Not a lot of value in changing async1 behavior here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot let's

  • keep the async2 behavior expectations as-is,
  • keep the comment on differences,
  • keep the async 1 scenario blocked, but not on an issue since we are not going to fix, block on some short comment like "async1 behavior is terminal to the process, thus we will not test it".

Copy link
Copy Markdown
Contributor Author

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.

Copy link
Copy Markdown
Contributor Author

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.

// 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();
}
}
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>
Loading