Skip to content
Merged
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
3 changes: 2 additions & 1 deletion src/tests/async/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)RuntimeAsyncMethodGenerationAttribute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)RuntimeAsyncMethodGenerationAttribute.cs"
Condition="'$(Language)' == 'C#'" />
</ItemGroup>
</Project>
3 changes: 0 additions & 3 deletions src/tests/async/async-versions/async-versions.ilproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk.IL">
<ItemGroup>
<!-- The runtime-async methods are written directly in IL, so the C# helper
attribute injected by the directory props is not applicable here. -->
<Compile Remove="..\RuntimeAsyncMethodGenerationAttribute.cs" />
<Compile Include="$(MSBuildProjectName).il" />
</ItemGroup>
</Project>
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,8 @@
<Project Sdk="Microsoft.NET.Sdk.IL">
<PropertyGroup>
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<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 thread
VSadov marked this conversation as resolved.
// async1 behavior here is terminal to the process, thus we will not test it:
//
// [Fact]
// public static void CatchAfterYield_NoWrap_Async1()
// => AssertNonExceptionWrapped(NoWrapThrowers.ThrowAfterYieldAsync1);
Comment thread
VSadov marked this conversation as resolved.

Comment thread
VSadov marked this conversation as resolved.
[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,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- On NativeAOT a non-Exception thrown after a suspension point is not surfaced
as a RuntimeWrappedException; it terminates the process instead of faulting the
Task. Blocked on NativeAOT pending https://github.com/dotnet/runtime/issues/69919. -->
<DisableProjectBuild Condition="'$(TestBuildMode)' == 'nativeaot'">true</DisableProjectBuild>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildProjectName).cs" />
<ProjectReference Include="NonExceptionThrower.ilproj" />
<ProjectReference Include="NoWrapThrowers.csproj" />
</ItemGroup>
</Project>
Loading