diff --git a/src/Adapter/MSTest.TestAdapter/Execution/TestAssemblyInfo.cs b/src/Adapter/MSTest.TestAdapter/Execution/TestAssemblyInfo.cs
index 8da4e5c3de..c25bbc9535 100644
--- a/src/Adapter/MSTest.TestAdapter/Execution/TestAssemblyInfo.cs
+++ b/src/Adapter/MSTest.TestAdapter/Execution/TestAssemblyInfo.cs
@@ -51,6 +51,8 @@ internal set
}
}
+ internal AssemblyInitializeAttribute? AssemblyInitializeAttribute { get; set; }
+
///
/// Gets or sets the AssemblyInitializeMethod timeout.
///
@@ -80,6 +82,8 @@ internal set
}
}
+ internal AssemblyCleanupAttribute? AssemblyCleanupAttribute { get; set; }
+
///
/// Gets a value indicating whether AssemblyInitialize has been executed.
///
@@ -143,7 +147,7 @@ public void RunAssemblyInitialize(TestContext testContext)
try
{
AssemblyInitializationException = FixtureMethodRunner.RunWithTimeoutAndCancellation(
- () => AssemblyInitializeMethod.InvokeAsSynchronousTask(null, testContext),
+ () => AssemblyInitializeAttribute!.ExecuteAsync(new AssemblyInitializeExecutionContext(() => AssemblyInitializeMethod.InvokeAsync(null, testContext))).GetAwaiter().GetResult(),
testContext.CancellationTokenSource,
AssemblyInitializeMethodTimeoutMilliseconds,
AssemblyInitializeMethod,
@@ -216,7 +220,7 @@ public void RunAssemblyInitialize(TestContext testContext)
try
{
AssemblyCleanupException = FixtureMethodRunner.RunWithTimeoutAndCancellation(
- () => AssemblyCleanupMethod.InvokeAsSynchronousTask(null),
+ () => AssemblyCleanupAttribute!.ExecuteAsync(new AssemblyCleanupExecutionContext(() => AssemblyCleanupMethod.InvokeAsync(null))).GetAwaiter().GetResult(),
new CancellationTokenSource(),
AssemblyCleanupMethodTimeoutMilliseconds,
AssemblyCleanupMethod,
@@ -276,11 +280,11 @@ internal void ExecuteAssemblyCleanup(TestContext testContext)
{
if (AssemblyCleanupMethod.GetParameters().Length == 0)
{
- AssemblyCleanupMethod.InvokeAsSynchronousTask(null);
+ AssemblyCleanupAttribute!.ExecuteAsync(new AssemblyCleanupExecutionContext(() => AssemblyCleanupMethod.InvokeAsync(null))).GetAwaiter().GetResult();
}
else
{
- AssemblyCleanupMethod.InvokeAsSynchronousTask(null, testContext);
+ AssemblyCleanupAttribute!.ExecuteAsync(new AssemblyCleanupExecutionContext(() => AssemblyCleanupMethod.InvokeAsync(null, testContext))).GetAwaiter().GetResult();
}
},
testContext.CancellationTokenSource,
diff --git a/src/Adapter/MSTest.TestAdapter/Execution/TypeCache.cs b/src/Adapter/MSTest.TestAdapter/Execution/TypeCache.cs
index 2b13c8384d..558acf81e0 100644
--- a/src/Adapter/MSTest.TestAdapter/Execution/TypeCache.cs
+++ b/src/Adapter/MSTest.TestAdapter/Execution/TypeCache.cs
@@ -416,14 +416,16 @@ private TestAssemblyInfo GetAssemblyInfo(Type type)
// Enumerate through all methods and identify the Assembly Init and cleanup methods.
foreach (MethodInfo methodInfo in PlatformServiceProvider.Instance.ReflectionOperations.GetDeclaredMethods(t))
{
- if (IsAssemblyOrClassInitializeMethod(methodInfo))
+ if (GetAssemblyOrClassInitializeMethod(methodInfo) is { } assemblyInitializeAttribute)
{
assemblyInfo.AssemblyInitializeMethod = methodInfo;
+ assemblyInfo.AssemblyInitializeAttribute = assemblyInitializeAttribute;
assemblyInfo.AssemblyInitializeMethodTimeoutMilliseconds = TryGetTimeoutInfo(methodInfo, FixtureKind.AssemblyInitialize);
}
- else if (IsAssemblyOrClassCleanupMethod(methodInfo))
+ else if (GetAssemblyOrClassCleanupMethod(methodInfo) is { } assemblyCleanupAttribute)
{
assemblyInfo.AssemblyCleanupMethod = methodInfo;
+ assemblyInfo.AssemblyCleanupAttribute = assemblyCleanupAttribute;
assemblyInfo.AssemblyCleanupMethodTimeoutMilliseconds = TryGetTimeoutInfo(methodInfo, FixtureKind.AssemblyCleanup);
}
}
@@ -440,7 +442,7 @@ private TestAssemblyInfo GetAssemblyInfo(Type type)
/// The initialization attribute type.
/// The method info.
/// True if its an initialization method.
- private bool IsAssemblyOrClassInitializeMethod(MethodInfo methodInfo)
+ private TInitializeAttribute? GetAssemblyOrClassInitializeMethod(MethodInfo methodInfo)
where TInitializeAttribute : Attribute
{
// TODO: this would be inconsistent with the codebase, but potential perf gain, issue: https://github.com/microsoft/testfx/issues/2999
@@ -448,9 +450,20 @@ private bool IsAssemblyOrClassInitializeMethod(MethodInfo
// {
// return false;
// }
- if (!_reflectionHelper.IsNonDerivedAttributeDefined(methodInfo, false))
+ IEnumerable attributes = _reflectionHelper.GetDerivedAttributes(methodInfo, inherit: false);
+ using IEnumerator enumerator = attributes.GetEnumerator();
+ if (!enumerator.MoveNext())
{
- return false;
+ // No attribute found.
+ return null;
+ }
+
+ TInitializeAttribute attribute = enumerator.Current;
+ if (enumerator.MoveNext())
+ {
+ // More than one attribute found.
+ string message = string.Format(CultureInfo.CurrentCulture, Resource.UTA_MultipleAttributesOnTestMethod, methodInfo.DeclaringType!.FullName, methodInfo.Name);
+ throw new TypeInspectionException(message);
}
if (!methodInfo.HasCorrectClassOrAssemblyInitializeSignature())
@@ -459,7 +472,7 @@ private bool IsAssemblyOrClassInitializeMethod(MethodInfo
throw new TypeInspectionException(message);
}
- return true;
+ return attribute;
}
///
@@ -468,7 +481,7 @@ private bool IsAssemblyOrClassInitializeMethod(MethodInfo
/// The cleanup attribute type.
/// The method info.
/// True if its a cleanup method.
- private bool IsAssemblyOrClassCleanupMethod(MethodInfo methodInfo)
+ private TCleanupAttribute? GetAssemblyOrClassCleanupMethod(MethodInfo methodInfo)
where TCleanupAttribute : Attribute
{
// TODO: this would be inconsistent with the codebase, but potential perf gain, issue: https://github.com/microsoft/testfx/issues/2999
@@ -476,9 +489,20 @@ private bool IsAssemblyOrClassCleanupMethod(MethodInfo method
// {
// return false;
// }
- if (!_reflectionHelper.IsNonDerivedAttributeDefined(methodInfo, false))
+ IEnumerable attributes = _reflectionHelper.GetDerivedAttributes(methodInfo, inherit: false);
+ using IEnumerator enumerator = attributes.GetEnumerator();
+ if (!enumerator.MoveNext())
{
- return false;
+ // No attribute found.
+ return null;
+ }
+
+ TCleanupAttribute attribute = enumerator.Current;
+ if (enumerator.MoveNext())
+ {
+ // More than one attribute found.
+ string message = string.Format(CultureInfo.CurrentCulture, Resource.UTA_MultipleAttributesOnTestMethod, methodInfo.DeclaringType!.FullName, methodInfo.Name);
+ throw new TypeInspectionException(message);
}
if (!methodInfo.HasCorrectClassOrAssemblyCleanupSignature())
@@ -487,7 +511,7 @@ private bool IsAssemblyOrClassCleanupMethod(MethodInfo method
throw new TypeInspectionException(message);
}
- return true;
+ return attribute;
}
#endregion
@@ -546,10 +570,10 @@ private void UpdateInfoIfClassInitializeOrCleanupMethod(
bool isBase,
ref MethodInfo?[] initAndCleanupMethods)
{
- bool isInitializeMethod = IsAssemblyOrClassInitializeMethod(methodInfo);
- bool isCleanupMethod = IsAssemblyOrClassCleanupMethod(methodInfo);
+ ClassInitializeAttribute? classInitializeAttribute = GetAssemblyOrClassInitializeMethod(methodInfo);
+ ClassCleanupAttribute? classCleanupAttribute = GetAssemblyOrClassCleanupMethod(methodInfo);
- if (isInitializeMethod)
+ if (classInitializeAttribute is not null)
{
if (TryGetTimeoutInfo(methodInfo, FixtureKind.ClassInitialize) is { } timeoutInfo)
{
@@ -571,7 +595,7 @@ private void UpdateInfoIfClassInitializeOrCleanupMethod(
}
}
- if (isCleanupMethod)
+ if (classCleanupAttribute is not null)
{
if (TryGetTimeoutInfo(methodInfo, FixtureKind.ClassCleanup) is { } timeoutInfo)
{
diff --git a/src/Adapter/MSTest.TestAdapter/Extensions/MethodInfoExtensions.cs b/src/Adapter/MSTest.TestAdapter/Extensions/MethodInfoExtensions.cs
index d220cd9470..d269644a41 100644
--- a/src/Adapter/MSTest.TestAdapter/Extensions/MethodInfoExtensions.cs
+++ b/src/Adapter/MSTest.TestAdapter/Extensions/MethodInfoExtensions.cs
@@ -107,7 +107,7 @@ internal static bool IsValidReturnType(this MethodInfo method)
}
///
- /// Invoke a as a synchronous .
+ /// Invoke a as an asynchronous .
///
///
/// instance.
@@ -118,7 +118,7 @@ internal static bool IsValidReturnType(this MethodInfo method)
///
/// Arguments for the methodInfo invoke.
///
- internal static void InvokeAsSynchronousTask(this MethodInfo methodInfo, object? classInstance, params object?[]? arguments)
+ internal static async Task InvokeAsync(this MethodInfo methodInfo, object? classInstance, params object?[]? arguments)
{
ParameterInfo[]? methodParameters = methodInfo.GetParameters();
@@ -189,14 +189,29 @@ internal static void InvokeAsSynchronousTask(this MethodInfo methodInfo, object?
// If methodInfo is an async method, wait for returned task
if (invokeResult is Task task)
{
- task.GetAwaiter().GetResult();
+ await task;
}
else if (invokeResult is ValueTask valueTask)
{
- valueTask.GetAwaiter().GetResult();
+ await valueTask;
}
}
+ ///
+ /// Invoke a as a synchronous .
+ ///
+ ///
+ /// instance.
+ ///
+ ///
+ /// Instance of the on which methodInfo is invoked.
+ ///
+ ///
+ /// Arguments for the methodInfo invoke.
+ ///
+ internal static void InvokeAsSynchronousTask(this MethodInfo methodInfo, object? classInstance, params object?[]? arguments)
+ => InvokeAsync(methodInfo, classInstance, arguments).GetAwaiter().GetResult();
+
// Scenarios to test:
//
// [DataRow(null, "Hello")]
diff --git a/src/TestFramework/TestFramework/Attributes/Lifecycle/Cleanup/AssemblyCleanupAttribute.cs b/src/TestFramework/TestFramework/Attributes/Lifecycle/Cleanup/AssemblyCleanupAttribute.cs
index 5d37d858f7..d65c4c623d 100644
--- a/src/TestFramework/TestFramework/Attributes/Lifecycle/Cleanup/AssemblyCleanupAttribute.cs
+++ b/src/TestFramework/TestFramework/Attributes/Lifecycle/Cleanup/AssemblyCleanupAttribute.cs
@@ -7,4 +7,13 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting;
/// The assembly cleanup attribute.
///
[AttributeUsage(AttributeTargets.Method)]
-public sealed class AssemblyCleanupAttribute : Attribute;
+public class AssemblyCleanupAttribute : Attribute
+{
+ ///
+ /// Executes the assembly cleanup method. Custom implementations may
+ /// override this method to plug in custom logic for executing assembly cleanup.
+ ///
+ /// A struct to hold information for executing the assembly cleanup.
+ public virtual async Task ExecuteAsync(AssemblyCleanupExecutionContext assemblyCleanupContext)
+ => await assemblyCleanupContext.AssemblyCleanupExecutorGetter();
+}
diff --git a/src/TestFramework/TestFramework/Attributes/Lifecycle/Cleanup/AssemblyCleanupExecutionContext.cs b/src/TestFramework/TestFramework/Attributes/Lifecycle/Cleanup/AssemblyCleanupExecutionContext.cs
new file mode 100644
index 0000000000..a6286c27e6
--- /dev/null
+++ b/src/TestFramework/TestFramework/Attributes/Lifecycle/Cleanup/AssemblyCleanupExecutionContext.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.VisualStudio.TestTools.UnitTesting;
+
+///
+/// Provides the information needed for executing assembly cleanup.
+/// This type is passed as a parameter to .
+///
+public readonly struct AssemblyCleanupExecutionContext
+{
+ internal AssemblyCleanupExecutionContext(Func assemblyCleanupExecutorGetter)
+ => AssemblyCleanupExecutorGetter = assemblyCleanupExecutorGetter;
+
+ ///
+ /// Gets the that returns the that executes the AssemblyCleanup method.
+ ///
+ public Func AssemblyCleanupExecutorGetter { get; }
+}
diff --git a/src/TestFramework/TestFramework/Attributes/Lifecycle/Initialization/AssemblyInitializeAttribute.cs b/src/TestFramework/TestFramework/Attributes/Lifecycle/Initialization/AssemblyInitializeAttribute.cs
index c45d0124e5..171f0653d0 100644
--- a/src/TestFramework/TestFramework/Attributes/Lifecycle/Initialization/AssemblyInitializeAttribute.cs
+++ b/src/TestFramework/TestFramework/Attributes/Lifecycle/Initialization/AssemblyInitializeAttribute.cs
@@ -7,4 +7,13 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting;
/// The assembly initialize attribute.
///
[AttributeUsage(AttributeTargets.Method)]
-public sealed class AssemblyInitializeAttribute : Attribute;
+public class AssemblyInitializeAttribute : Attribute
+{
+ ///
+ /// Executes the assembly initialize method. Custom implementations may
+ /// override this method to plug in custom logic for executing assembly initialize.
+ ///
+ /// A struct to hold information for executing the assembly initialize.
+ public virtual async Task ExecuteAsync(AssemblyInitializeExecutionContext assemblyInitializeContext)
+ => await assemblyInitializeContext.AssemblyInitializeExecutorGetter();
+}
diff --git a/src/TestFramework/TestFramework/Attributes/Lifecycle/Initialization/AssemblyInitializeExecutionContext.cs b/src/TestFramework/TestFramework/Attributes/Lifecycle/Initialization/AssemblyInitializeExecutionContext.cs
new file mode 100644
index 0000000000..5bbd63507d
--- /dev/null
+++ b/src/TestFramework/TestFramework/Attributes/Lifecycle/Initialization/AssemblyInitializeExecutionContext.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.VisualStudio.TestTools.UnitTesting;
+
+///
+/// Provides the information needed for executing assembly initialize.
+/// This type is passed as a parameter to .
+///
+public readonly struct AssemblyInitializeExecutionContext
+{
+ internal AssemblyInitializeExecutionContext(Func assemblyInitializeExecutorGetter)
+ => AssemblyInitializeExecutorGetter = assemblyInitializeExecutorGetter;
+
+ ///
+ /// Gets the that returns the that executes the AssemblyInitialize method.
+ ///
+ public Func AssemblyInitializeExecutorGetter { get; }
+}
diff --git a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt
index f3bbda5e6a..23aff85567 100644
--- a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt
+++ b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt
@@ -1,5 +1,11 @@
#nullable enable
abstract Microsoft.VisualStudio.TestTools.UnitTesting.RetryBaseAttribute.ExecuteAsync(Microsoft.VisualStudio.TestTools.UnitTesting.RetryContext retryContext) -> System.Threading.Tasks.Task!
+Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyCleanupExecutionContext
+Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyCleanupExecutionContext.AssemblyCleanupExecutionContext() -> void
+Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyCleanupExecutionContext.AssemblyCleanupExecutorGetter.get -> System.Func!
+Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyInitializeExecutionContext
+Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyInitializeExecutionContext.AssemblyInitializeExecutionContext() -> void
+Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyInitializeExecutionContext.AssemblyInitializeExecutorGetter.get -> System.Func!
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AssertAreEqualInterpolatedStringHandler
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AssertAreEqualInterpolatedStringHandler.AppendFormatted(object? value, int alignment = 0, string? format = null) -> void
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AssertAreEqualInterpolatedStringHandler.AppendFormatted(string? value) -> void
@@ -266,3 +272,5 @@ static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ThrowsExactly(System.Action! action, System.Func! messageBuilder) -> TException!
static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ThrowsExactlyAsync(System.Func! action, string! message = "", params object![]! messageArgs) -> System.Threading.Tasks.Task!
static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ThrowsExactlyAsync(System.Func! action, System.Func! messageBuilder) -> System.Threading.Tasks.Task!
+virtual Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyCleanupAttribute.ExecuteAsync(Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyCleanupExecutionContext assemblyCleanupContext) -> System.Threading.Tasks.Task!
+virtual Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyInitializeAttribute.ExecuteAsync(Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyInitializeExecutionContext assemblyInitializeContext) -> System.Threading.Tasks.Task!