Skip to content
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

Move ExpectedException attribute resolution out of TypeCache/ReflectHelper #4505

Merged
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
56 changes: 52 additions & 4 deletions src/Adapter/MSTest.TestAdapter/Execution/TestMethodInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ internal TestMethodInfo(
TestMethod = testMethod;
Parent = parent;
TestMethodOptions = testMethodOptions;
ExpectedException = ResolveExpectedException();
}

/// <summary>
Expand Down Expand Up @@ -89,6 +90,8 @@ internal TestMethodInfo(
/// </summary>
internal TestMethodOptions TestMethodOptions { get; }

internal ExpectedExceptionBaseAttribute? ExpectedException { get; set; /*set for testing only*/ }

public Attribute[]? GetAllAttributes(bool inherit) => ReflectHelper.Instance.GetDerivedAttributes<Attribute>(TestMethod, inherit).ToArray();

public TAttributeType[] GetAttributes<TAttributeType>(bool inherit)
Expand Down Expand Up @@ -212,6 +215,51 @@ public virtual TestResult Invoke(object?[]? arguments)
return newParameters;
}

/// <summary>
/// Resolves the expected exception attribute. The function will try to
/// get all the expected exception attributes defined for a testMethod.
/// </summary>
/// <returns>
/// The expected exception attribute found for this test. Null if not found.
/// </returns>
private ExpectedExceptionBaseAttribute? ResolveExpectedException()
{
IEnumerable<ExpectedExceptionBaseAttribute> expectedExceptions;

try
{
expectedExceptions = ReflectHelper.Instance.GetDerivedAttributes<ExpectedExceptionBaseAttribute>(TestMethod, inherit: true);
}
catch (Exception ex)
{
// If construction of the attribute throws an exception, indicate that there was an
// error when trying to run the test
string errorMessage = string.Format(
CultureInfo.CurrentCulture,
Resource.UTA_ExpectedExceptionAttributeConstructionException,
Parent.ClassType.FullName,
TestMethod.Name,
ex.GetFormattedExceptionMessage());
throw new TypeInspectionException(errorMessage);
}

// Verify that there is only one attribute (multiple attributes derived from
// ExpectedExceptionBaseAttribute are not allowed on a test method)
// This is needed EVEN IF the attribute doesn't allow multiple.
// See https://github.com/microsoft/testfx/issues/4331
if (expectedExceptions.Count() > 1)
{
string errorMessage = string.Format(
CultureInfo.CurrentCulture,
Resource.UTA_MultipleExpectedExceptionsOnTestMethod,
Parent.ClassType.FullName,
TestMethod.Name);
throw new TypeInspectionException(errorMessage);
}

return expectedExceptions.FirstOrDefault();
}

/// <summary>
/// Execute test without timeout.
/// </summary>
Expand Down Expand Up @@ -303,11 +351,11 @@ private TestResult ExecuteInternal(object?[]? arguments, CancellationTokenSource
// if the user specified that the test was going to throw an exception, and
// it did not, we should fail the test
// We only perform this check if the test initialize passes and the test method is actually run.
if (hasTestInitializePassed && !isExceptionThrown && TestMethodOptions.ExpectedException != null)
if (hasTestInitializePassed && !isExceptionThrown && ExpectedException is { } expectedException)
{
result.TestFailureException = new TestFailedException(
ObjectModelUnitTestOutcome.Failed,
TestMethodOptions.ExpectedException.NoExceptionMessage);
expectedException.NoExceptionMessage);
result.Outcome = UTF.UnitTestOutcome.Failed;
}
}
Expand Down Expand Up @@ -342,7 +390,7 @@ private bool IsExpectedException(Exception ex, TestResult result)
// if the user specified an expected exception, we need to check if this
// exception was thrown. If it was thrown, we should pass the test. In
// case a different exception was thrown, the test is seen as failure
if (TestMethodOptions.ExpectedException == null)
if (ExpectedException == null)
{
return false;
}
Expand All @@ -352,7 +400,7 @@ private bool IsExpectedException(Exception ex, TestResult result)
{
// If the expected exception attribute's Verify method returns, then it
// considers this exception as expected, so the test passed
TestMethodOptions.ExpectedException.Verify(ex);
ExpectedException.Verify(ex);
return true;
}
catch (Exception verifyEx)
Expand Down
5 changes: 2 additions & 3 deletions src/Adapter/MSTest.TestAdapter/Execution/TypeCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -729,8 +729,7 @@ private TestMethodInfo ResolveTestMethodInfo(TestMethod testMethod, TestClassInf
MethodInfo methodInfo = GetMethodInfoForTestMethod(testMethod, testClassInfo);

TimeoutInfo timeout = GetTestTimeout(methodInfo, testMethod);
ExpectedExceptionBaseAttribute? expectedExceptionAttribute = _reflectionHelper.ResolveExpectedExceptionHelper(methodInfo, testMethod);
var testMethodOptions = new TestMethodOptions(timeout, expectedExceptionAttribute, testContext, captureDebugTraces, GetTestMethodAttribute(methodInfo, testClassInfo));
var testMethodOptions = new TestMethodOptions(timeout, testContext, captureDebugTraces, GetTestMethodAttribute(methodInfo, testClassInfo));
var testMethodInfo = new TestMethodInfo(methodInfo, testClassInfo, testMethodOptions);

SetCustomProperties(testMethodInfo, testContext);
Expand All @@ -743,7 +742,7 @@ private TestMethodInfo ResolveTestMethodInfoForDiscovery(TestMethod testMethod,
MethodInfo methodInfo = GetMethodInfoForTestMethod(testMethod, testClassInfo);

// Let's build a fake options type as it won't be used.
return new TestMethodInfo(methodInfo, testClassInfo, new(TimeoutInfo.FromTimeout(-1), null, null, false, null!));
return new TestMethodInfo(methodInfo, testClassInfo, new(TimeoutInfo.FromTimeout(-1), null, false, null!));
}

/// <summary>
Expand Down
50 changes: 0 additions & 50 deletions src/Adapter/MSTest.TestAdapter/Helpers/ReflectHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Security;

using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;

Expand Down Expand Up @@ -87,55 +86,6 @@ public virtual bool IsNonDerivedAttributeDefined<TAttribute>(MemberInfo memberIn
return false;
}

/// <summary>
/// Resolves the expected exception attribute. The function will try to
/// get all the expected exception attributes defined for a testMethod.
/// </summary>
/// <param name="methodInfo">The MethodInfo instance.</param>
/// <param name="testMethod">The test method.</param>
/// <returns>
/// The expected exception attribute found for this test. Null if not found.
/// </returns>
public virtual ExpectedExceptionBaseAttribute? ResolveExpectedExceptionHelper(MethodInfo methodInfo, TestMethod testMethod)
{
DebugEx.Assert(methodInfo != null, "MethodInfo should be non-null");

IEnumerable<ExpectedExceptionBaseAttribute> expectedExceptions;

try
{
expectedExceptions = GetDerivedAttributes<ExpectedExceptionBaseAttribute>(methodInfo, inherit: true);
}
catch (Exception ex)
{
// If construction of the attribute throws an exception, indicate that there was an
// error when trying to run the test
string errorMessage = string.Format(
CultureInfo.CurrentCulture,
Resource.UTA_ExpectedExceptionAttributeConstructionException,
testMethod.FullClassName,
testMethod.Name,
ex.GetFormattedExceptionMessage());
throw new TypeInspectionException(errorMessage);
}

// Verify that there is only one attribute (multiple attributes derived from
// ExpectedExceptionBaseAttribute are not allowed on a test method)
// This is needed EVEN IF the attribute doesn't allow multiple.
// See https://github.com/microsoft/testfx/issues/4331
if (expectedExceptions.Count() > 1)
{
string errorMessage = string.Format(
CultureInfo.CurrentCulture,
Resource.UTA_MultipleExpectedExceptionsOnTestMethod,
testMethod.FullClassName,
testMethod.Name);
throw new TypeInspectionException(errorMessage);
}

return expectedExceptions.FirstOrDefault();
}

/// <summary>
/// Returns object to be used for controlling lifetime, null means infinite lifetime.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
/// <summary>
/// A facade service for options passed to a test method.
/// </summary>
internal sealed record TestMethodOptions(TimeoutInfo TimeoutInfo, ExpectedExceptionBaseAttribute? ExpectedException, ITestContext? TestContext, bool CaptureDebugTraces, TestMethodAttribute Executor);
internal sealed record TestMethodOptions(TimeoutInfo TimeoutInfo, ITestContext? TestContext, bool CaptureDebugTraces, TestMethodAttribute Executor);
Loading