Skip to content

Commit

Permalink
Get real exception in case of error in AssemblyInitialize (#2571)
Browse files Browse the repository at this point in the history
  • Loading branch information
nohwnd authored Mar 15, 2024
1 parent ffe8fa2 commit 23b31bc
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ public void RunAssemblyInitialize(TestContext testContext)
throw AssemblyInitializationException;
}

var realException = AssemblyInitializationException.InnerException ?? AssemblyInitializationException;
var realException = AssemblyInitializationException.GetRealException();

var outcome = realException is AssertInconclusiveException ? UnitTestOutcome.Inconclusive : UnitTestOutcome.Failed;

Expand Down
21 changes: 14 additions & 7 deletions src/Adapter/MSTest.TestAdapter/Extensions/ExceptionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,22 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions;
internal static class ExceptionExtensions
{
/// <summary>
/// In .NET framework, the exception thrown by the test method invocation is wrapped in a TargetInvocationException, so we
/// need to unwrap it to get the real exception.
/// TargetInvocationException and TypeInitializationException do not carry any useful information
/// to the user. Find the first inner exception that has useful information.
/// </summary>
internal static Exception GetRealException(this Exception exception)
=> exception.GetType() == typeof(TargetInvocationException)
&& exception.Source is "mscorlib" or "System.Private.CoreLib"
&& exception.InnerException is not null
? exception.InnerException
: exception;
{
// TargetInvocationException: Because .NET Framework wraps method.Invoke() into TargetInvocationException.
// TypeInitializationException: Because AssemblyInitialize is static, and often helpers that are also static
// are used to implement it, and they fail in constructor.
while (exception is TargetInvocationException or TypeInitializationException
&& exception.InnerException is not null)
{
exception = exception.InnerException;
}

return exception;
}

/// <summary>
/// Get the exception message if available, empty otherwise.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,30 @@ public void RunAssemblyInitializeShouldThrowTestFailedExceptionWithNonAssertExce
Verify(exception.InnerException.InnerException.GetType() == typeof(InvalidOperationException));
}

public void RunAssemblyInitializeShouldThrowTheInnerMostExceptionWhenThereAreMultipleNestedTypeInitializationExceptions()
{
DummyTestClass.AssemblyInitializeMethodBody = tc =>
{
// This helper calls inner helper, and the inner helper ctor throws.
// We want to see the real exception on screen, and not TypeInitializationException
// which has no info about what failed.
FailingStaticHelper.DoWork();
};
_testAssemblyInfo.AssemblyInitializeMethod = typeof(DummyTestClass).GetMethod("AssemblyInitializeMethod");

var exception = VerifyThrows(() => _testAssemblyInfo.RunAssemblyInitialize(_testContext)) as TestFailedException;

Verify(exception is not null);
Verify(exception.Outcome == UnitTestOutcome.Failed);
Verify(
exception.Message
== "Assembly Initialization method Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.Execution.TestAssemblyInfoTests+DummyTestClass.AssemblyInitializeMethod threw exception. System.InvalidOperationException: I fail.. Aborting test execution.");
Verify(
exception.StackTraceInformation.ErrorStackTrace.StartsWith(
" at Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.Execution.TestAssemblyInfoTests.FailingStaticHelper..cctor()", StringComparison.Ordinal));
Verify(exception.InnerException.GetType() == typeof(InvalidOperationException));
}

public void RunAssemblyInitializeShouldThrowForAlreadyExecutedTestAssemblyInitWithException()
{
DummyTestClass.AssemblyInitializeMethodBody = (tc) => { };
Expand Down Expand Up @@ -303,4 +327,28 @@ public static void AssemblyCleanupMethod()
AssemblyCleanupMethodBody.Invoke();
}
}

private static class FailingStaticHelper
{
static FailingStaticHelper()
{
throw new InvalidOperationException("I fail.");
}

public static void DoWork()
{
}
}

private static class FailingInnerStaticHelper
{
static FailingInnerStaticHelper()
{
throw new InvalidOperationException("I fail.");
}

public static void Initialize()
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Globalization;
using System.Reflection;

using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions;
Expand Down Expand Up @@ -148,4 +149,80 @@ public void IsUnitTestAssertExceptionSetsOutcomeAsFailedIfAssertFailedException(
Verify(exceptionMessage == "Dummy Message");
}
#endregion

#region GetRealException scenarios
public void GetRealExceptionGetsTheTopExceptionWhenThereIsJustOne()
{
var exception = new InvalidOperationException();
var actual = exception.GetRealException();

Verify(actual is InvalidOperationException);
}

public void GetRealExceptionGetsTheInnerExceptionWhenTheExceptionIsTargetInvocation()
{
var exception = new TargetInvocationException(new InvalidOperationException());
var actual = exception.GetRealException();

Verify(actual is InvalidOperationException);
}

public void GetRealExceptionGetsTheTargetInvocationExceptionWhenTargetInvocationIsProvidedWithNullInnerException()
{
var exception = new TargetInvocationException(null);
var actual = exception.GetRealException();

Verify(actual is TargetInvocationException);
}

public void GetRealExceptionGetsTheInnerMostRealException()
{
var exception = new TargetInvocationException(new TargetInvocationException(new TargetInvocationException(new InvalidOperationException())));
var actual = exception.GetRealException();

Verify(actual is InvalidOperationException);
}

public void GetRealExceptionGetsTheInnerMostTargetInvocationException()
{
var exception = new TargetInvocationException(new TargetInvocationException(new TargetInvocationException("inner most", null)));
var actual = exception.GetRealException();

Verify(actual is TargetInvocationException);
Verify(actual.Message == "inner most");
}

public void GetRealExceptionGetsTheInnerExceptionWhenTheExceptionIsTypeInitialization()
{
var exception = new TypeInitializationException("some type", new InvalidOperationException());
var actual = exception.GetRealException();

Verify(actual is InvalidOperationException);
}

public void GetRealExceptionGetsTheTypeInitializationExceptionWhenTypeInitializationIsProvidedWithNullInnerException()
{
var exception = new TypeInitializationException("some type", null);
var actual = exception.GetRealException();

Verify(actual is TypeInitializationException);
}

public void GetRealExceptionGetsTheInnerMostRealExceptionOfTypeInitialization()
{
var exception = new TypeInitializationException("some type", new TypeInitializationException("some type", new TypeInitializationException("some type", new InvalidOperationException())));
var actual = exception.GetRealException();

Verify(actual is InvalidOperationException);
}

public void GetRealExceptionGetsTheInnerMostTypeInitializationException()
{
var exception = new TypeInitializationException("some type", new TypeInitializationException("some type", new TypeInitializationException("inner most", null)));
var actual = exception.GetRealException();

Verify(actual is TypeInitializationException);
Verify(actual.Message == "The type initializer for 'inner most' threw an exception.");
}
#endregion
}

0 comments on commit 23b31bc

Please sign in to comment.