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!