Skip to content

Commit 6b7aa69

Browse files
committed
Make AssemblyInitialize/AssemblyCleanup inheritable
1 parent f1995d7 commit 6b7aa69

File tree

8 files changed

+131
-24
lines changed

8 files changed

+131
-24
lines changed

src/Adapter/MSTest.TestAdapter/Execution/TestAssemblyInfo.cs

+8-4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ internal set
5151
}
5252
}
5353

54+
internal AssemblyInitializeAttribute? AssemblyInitializeAttribute { get; set; }
55+
5456
/// <summary>
5557
/// Gets or sets the AssemblyInitializeMethod timeout.
5658
/// </summary>
@@ -80,6 +82,8 @@ internal set
8082
}
8183
}
8284

85+
internal AssemblyCleanupAttribute? AssemblyCleanupAttribute { get; set; }
86+
8387
/// <summary>
8488
/// Gets a value indicating whether <c>AssemblyInitialize</c> has been executed.
8589
/// </summary>
@@ -143,7 +147,7 @@ public void RunAssemblyInitialize(TestContext testContext)
143147
try
144148
{
145149
AssemblyInitializationException = FixtureMethodRunner.RunWithTimeoutAndCancellation(
146-
() => AssemblyInitializeMethod.InvokeAsSynchronousTask(null, testContext),
150+
() => AssemblyInitializeAttribute!.ExecuteAsync(new AssemblyInitializeExecutionContext(() => AssemblyInitializeMethod.InvokeAsync(null, testContext))).GetAwaiter().GetResult(),
147151
testContext.CancellationTokenSource,
148152
AssemblyInitializeMethodTimeoutMilliseconds,
149153
AssemblyInitializeMethod,
@@ -216,7 +220,7 @@ public void RunAssemblyInitialize(TestContext testContext)
216220
try
217221
{
218222
AssemblyCleanupException = FixtureMethodRunner.RunWithTimeoutAndCancellation(
219-
() => AssemblyCleanupMethod.InvokeAsSynchronousTask(null),
223+
() => AssemblyCleanupAttribute!.ExecuteAsync(new AssemblyCleanupExecutionContext(() => AssemblyCleanupMethod.InvokeAsync(null))).GetAwaiter().GetResult(),
220224
new CancellationTokenSource(),
221225
AssemblyCleanupMethodTimeoutMilliseconds,
222226
AssemblyCleanupMethod,
@@ -276,11 +280,11 @@ internal void ExecuteAssemblyCleanup(TestContext testContext)
276280
{
277281
if (AssemblyCleanupMethod.GetParameters().Length == 0)
278282
{
279-
AssemblyCleanupMethod.InvokeAsSynchronousTask(null);
283+
AssemblyCleanupAttribute!.ExecuteAsync(new AssemblyCleanupExecutionContext(() => AssemblyCleanupMethod.InvokeAsync(null))).GetAwaiter().GetResult();
280284
}
281285
else
282286
{
283-
AssemblyCleanupMethod.InvokeAsSynchronousTask(null, testContext);
287+
AssemblyCleanupAttribute!.ExecuteAsync(new AssemblyCleanupExecutionContext(() => AssemblyCleanupMethod.InvokeAsync(null, testContext))).GetAwaiter().GetResult();
284288
}
285289
},
286290
testContext.CancellationTokenSource,

src/Adapter/MSTest.TestAdapter/Execution/TypeCache.cs

+38-14
Original file line numberDiff line numberDiff line change
@@ -416,14 +416,16 @@ private TestAssemblyInfo GetAssemblyInfo(Type type)
416416
// Enumerate through all methods and identify the Assembly Init and cleanup methods.
417417
foreach (MethodInfo methodInfo in PlatformServiceProvider.Instance.ReflectionOperations.GetDeclaredMethods(t))
418418
{
419-
if (IsAssemblyOrClassInitializeMethod<AssemblyInitializeAttribute>(methodInfo))
419+
if (GetAssemblyOrClassInitializeMethod<AssemblyInitializeAttribute>(methodInfo) is { } assemblyInitializeAttribute)
420420
{
421421
assemblyInfo.AssemblyInitializeMethod = methodInfo;
422+
assemblyInfo.AssemblyInitializeAttribute = assemblyInitializeAttribute;
422423
assemblyInfo.AssemblyInitializeMethodTimeoutMilliseconds = TryGetTimeoutInfo(methodInfo, FixtureKind.AssemblyInitialize);
423424
}
424-
else if (IsAssemblyOrClassCleanupMethod<AssemblyCleanupAttribute>(methodInfo))
425+
else if (GetAssemblyOrClassCleanupMethod<AssemblyCleanupAttribute>(methodInfo) is { } assemblyCleanupAttribute)
425426
{
426427
assemblyInfo.AssemblyCleanupMethod = methodInfo;
428+
assemblyInfo.AssemblyCleanupAttribute = assemblyCleanupAttribute;
427429
assemblyInfo.AssemblyCleanupMethodTimeoutMilliseconds = TryGetTimeoutInfo(methodInfo, FixtureKind.AssemblyCleanup);
428430
}
429431
}
@@ -440,17 +442,28 @@ private TestAssemblyInfo GetAssemblyInfo(Type type)
440442
/// <typeparam name="TInitializeAttribute">The initialization attribute type. </typeparam>
441443
/// <param name="methodInfo"> The method info. </param>
442444
/// <returns> True if its an initialization method. </returns>
443-
private bool IsAssemblyOrClassInitializeMethod<TInitializeAttribute>(MethodInfo methodInfo)
445+
private TInitializeAttribute? GetAssemblyOrClassInitializeMethod<TInitializeAttribute>(MethodInfo methodInfo)
444446
where TInitializeAttribute : Attribute
445447
{
446448
// TODO: this would be inconsistent with the codebase, but potential perf gain, issue: https://github.com/microsoft/testfx/issues/2999
447449
// if (!methodInfo.IsStatic)
448450
// {
449451
// return false;
450452
// }
451-
if (!_reflectionHelper.IsNonDerivedAttributeDefined<TInitializeAttribute>(methodInfo, false))
453+
IEnumerable<TInitializeAttribute> attributes = _reflectionHelper.GetDerivedAttributes<TInitializeAttribute>(methodInfo, inherit: false);
454+
using IEnumerator<TInitializeAttribute> enumerator = attributes.GetEnumerator();
455+
if (!enumerator.MoveNext())
452456
{
453-
return false;
457+
// No attribute found.
458+
return null;
459+
}
460+
461+
TInitializeAttribute attribute = enumerator.Current;
462+
if (enumerator.MoveNext())
463+
{
464+
// More than one attribute found.
465+
string message = string.Format(CultureInfo.CurrentCulture, Resource.UTA_MultipleAttributesOnTestMethod, methodInfo.DeclaringType!.FullName, methodInfo.Name);
466+
throw new TypeInspectionException(message);
454467
}
455468

456469
if (!methodInfo.HasCorrectClassOrAssemblyInitializeSignature())
@@ -459,7 +472,7 @@ private bool IsAssemblyOrClassInitializeMethod<TInitializeAttribute>(MethodInfo
459472
throw new TypeInspectionException(message);
460473
}
461474

462-
return true;
475+
return attribute;
463476
}
464477

465478
/// <summary>
@@ -468,17 +481,28 @@ private bool IsAssemblyOrClassInitializeMethod<TInitializeAttribute>(MethodInfo
468481
/// <typeparam name="TCleanupAttribute">The cleanup attribute type.</typeparam>
469482
/// <param name="methodInfo"> The method info. </param>
470483
/// <returns> True if its a cleanup method. </returns>
471-
private bool IsAssemblyOrClassCleanupMethod<TCleanupAttribute>(MethodInfo methodInfo)
484+
private TCleanupAttribute? GetAssemblyOrClassCleanupMethod<TCleanupAttribute>(MethodInfo methodInfo)
472485
where TCleanupAttribute : Attribute
473486
{
474487
// TODO: this would be inconsistent with the codebase, but potential perf gain, issue: https://github.com/microsoft/testfx/issues/2999
475488
// if (!methodInfo.IsStatic)
476489
// {
477490
// return false;
478491
// }
479-
if (!_reflectionHelper.IsNonDerivedAttributeDefined<TCleanupAttribute>(methodInfo, false))
492+
IEnumerable<TCleanupAttribute> attributes = _reflectionHelper.GetDerivedAttributes<TCleanupAttribute>(methodInfo, inherit: false);
493+
using IEnumerator<TCleanupAttribute> enumerator = attributes.GetEnumerator();
494+
if (!enumerator.MoveNext())
480495
{
481-
return false;
496+
// No attribute found.
497+
return null;
498+
}
499+
500+
TCleanupAttribute attribute = enumerator.Current;
501+
if (enumerator.MoveNext())
502+
{
503+
// More than one attribute found.
504+
string message = string.Format(CultureInfo.CurrentCulture, Resource.UTA_MultipleAttributesOnTestMethod, methodInfo.DeclaringType!.FullName, methodInfo.Name);
505+
throw new TypeInspectionException(message);
482506
}
483507

484508
if (!methodInfo.HasCorrectClassOrAssemblyCleanupSignature())
@@ -487,7 +511,7 @@ private bool IsAssemblyOrClassCleanupMethod<TCleanupAttribute>(MethodInfo method
487511
throw new TypeInspectionException(message);
488512
}
489513

490-
return true;
514+
return attribute;
491515
}
492516

493517
#endregion
@@ -546,10 +570,10 @@ private void UpdateInfoIfClassInitializeOrCleanupMethod(
546570
bool isBase,
547571
ref MethodInfo?[] initAndCleanupMethods)
548572
{
549-
bool isInitializeMethod = IsAssemblyOrClassInitializeMethod<ClassInitializeAttribute>(methodInfo);
550-
bool isCleanupMethod = IsAssemblyOrClassCleanupMethod<ClassCleanupAttribute>(methodInfo);
573+
ClassInitializeAttribute? classInitializeAttribute = GetAssemblyOrClassInitializeMethod<ClassInitializeAttribute>(methodInfo);
574+
ClassCleanupAttribute? classCleanupAttribute = GetAssemblyOrClassCleanupMethod<ClassCleanupAttribute>(methodInfo);
551575

552-
if (isInitializeMethod)
576+
if (classInitializeAttribute is not null)
553577
{
554578
if (TryGetTimeoutInfo(methodInfo, FixtureKind.ClassInitialize) is { } timeoutInfo)
555579
{
@@ -571,7 +595,7 @@ private void UpdateInfoIfClassInitializeOrCleanupMethod(
571595
}
572596
}
573597

574-
if (isCleanupMethod)
598+
if (classCleanupAttribute is not null)
575599
{
576600
if (TryGetTimeoutInfo(methodInfo, FixtureKind.ClassCleanup) is { } timeoutInfo)
577601
{

src/Adapter/MSTest.TestAdapter/Extensions/MethodInfoExtensions.cs

+19-4
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ internal static bool IsValidReturnType(this MethodInfo method)
107107
}
108108

109109
/// <summary>
110-
/// Invoke a <see cref="MethodInfo"/> as a synchronous <see cref="Task"/>.
110+
/// Invoke a <see cref="MethodInfo"/> as an asynchronous <see cref="Task"/>.
111111
/// </summary>
112112
/// <param name="methodInfo">
113113
/// <see cref="MethodInfo"/> instance.
@@ -118,7 +118,7 @@ internal static bool IsValidReturnType(this MethodInfo method)
118118
/// <param name="arguments">
119119
/// Arguments for the methodInfo invoke.
120120
/// </param>
121-
internal static void InvokeAsSynchronousTask(this MethodInfo methodInfo, object? classInstance, params object?[]? arguments)
121+
internal static async Task InvokeAsync(this MethodInfo methodInfo, object? classInstance, params object?[]? arguments)
122122
{
123123
ParameterInfo[]? methodParameters = methodInfo.GetParameters();
124124

@@ -189,14 +189,29 @@ internal static void InvokeAsSynchronousTask(this MethodInfo methodInfo, object?
189189
// If methodInfo is an async method, wait for returned task
190190
if (invokeResult is Task task)
191191
{
192-
task.GetAwaiter().GetResult();
192+
await task;
193193
}
194194
else if (invokeResult is ValueTask valueTask)
195195
{
196-
valueTask.GetAwaiter().GetResult();
196+
await valueTask;
197197
}
198198
}
199199

200+
/// <summary>
201+
/// Invoke a <see cref="MethodInfo"/> as a synchronous <see cref="Task"/>.
202+
/// </summary>
203+
/// <param name="methodInfo">
204+
/// <see cref="MethodInfo"/> instance.
205+
/// </param>
206+
/// <param name="classInstance">
207+
/// Instance of the on which methodInfo is invoked.
208+
/// </param>
209+
/// <param name="arguments">
210+
/// Arguments for the methodInfo invoke.
211+
/// </param>
212+
internal static void InvokeAsSynchronousTask(this MethodInfo methodInfo, object? classInstance, params object?[]? arguments)
213+
=> InvokeAsync(methodInfo, classInstance, arguments).GetAwaiter().GetResult();
214+
200215
// Scenarios to test:
201216
//
202217
// [DataRow(null, "Hello")]

src/TestFramework/TestFramework/Attributes/Lifecycle/Cleanup/AssemblyCleanupAttribute.cs

+10-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,13 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting;
77
/// The assembly cleanup attribute.
88
/// </summary>
99
[AttributeUsage(AttributeTargets.Method)]
10-
public sealed class AssemblyCleanupAttribute : Attribute;
10+
public class AssemblyCleanupAttribute : Attribute
11+
{
12+
/// <summary>
13+
/// Executes the assembly cleanup method. Custom <see cref="AssemblyCleanupAttribute"/> implementations may
14+
/// override this method to plug in custom logic for executing assembly cleanup.
15+
/// </summary>
16+
/// <param name="assemblyCleanupContext">A struct to hold information for executing the assembly cleanup.</param>
17+
public virtual async Task ExecuteAsync(AssemblyCleanupExecutionContext assemblyCleanupContext)
18+
=> await assemblyCleanupContext.AssemblyCleanupExecutorGetter();
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
namespace Microsoft.VisualStudio.TestTools.UnitTesting;
5+
6+
/// <summary>
7+
/// Provides the information needed for executing assembly cleanup.
8+
/// This type is passed as a parameter to <see cref="AssemblyCleanupAttribute.ExecuteAsync(AssemblyCleanupExecutionContext)"/>.
9+
/// </summary>
10+
public readonly struct AssemblyCleanupExecutionContext
11+
{
12+
internal AssemblyCleanupExecutionContext(Func<Task> assemblyCleanupExecutorGetter)
13+
=> AssemblyCleanupExecutorGetter = assemblyCleanupExecutorGetter;
14+
15+
/// <summary>
16+
/// Gets the <see cref="Func{Task}"/> that returns the <see cref="Task"/> that executes the AssemblyCleanup method.
17+
/// </summary>
18+
public Func<Task> AssemblyCleanupExecutorGetter { get; }
19+
}

src/TestFramework/TestFramework/Attributes/Lifecycle/Initialization/AssemblyInitializeAttribute.cs

+10-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,13 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting;
77
/// The assembly initialize attribute.
88
/// </summary>
99
[AttributeUsage(AttributeTargets.Method)]
10-
public sealed class AssemblyInitializeAttribute : Attribute;
10+
public class AssemblyInitializeAttribute : Attribute
11+
{
12+
/// <summary>
13+
/// Executes the assembly initialize method. Custom <see cref="AssemblyInitializeAttribute"/> implementations may
14+
/// override this method to plug in custom logic for executing assembly initialize.
15+
/// </summary>
16+
/// <param name="assemblyInitializeContext">A struct to hold information for executing the assembly initialize.</param>
17+
public virtual async Task ExecuteAsync(AssemblyInitializeExecutionContext assemblyInitializeContext)
18+
=> await assemblyInitializeContext.AssemblyInitializeExecutorGetter();
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
namespace Microsoft.VisualStudio.TestTools.UnitTesting;
5+
6+
/// <summary>
7+
/// Provides the information needed for executing assembly initialize.
8+
/// This type is passed as a parameter to <see cref="AssemblyInitializeAttribute.ExecuteAsync(AssemblyInitializeExecutionContext)"/>.
9+
/// </summary>
10+
public readonly struct AssemblyInitializeExecutionContext
11+
{
12+
internal AssemblyInitializeExecutionContext(Func<Task> assemblyInitializeExecutorGetter)
13+
=> AssemblyInitializeExecutorGetter = assemblyInitializeExecutorGetter;
14+
15+
/// <summary>
16+
/// Gets the <see cref="Func{Task}"/> that returns the <see cref="Task"/> that executes the AssemblyInitialize method.
17+
/// </summary>
18+
public Func<Task> AssemblyInitializeExecutorGetter { get; }
19+
}

src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
#nullable enable
22
abstract Microsoft.VisualStudio.TestTools.UnitTesting.RetryBaseAttribute.ExecuteAsync(Microsoft.VisualStudio.TestTools.UnitTesting.RetryContext retryContext) -> System.Threading.Tasks.Task<Microsoft.VisualStudio.TestTools.UnitTesting.RetryResult!>!
3+
Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyCleanupExecutionContext
4+
Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyCleanupExecutionContext.AssemblyCleanupExecutionContext() -> void
5+
Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyCleanupExecutionContext.AssemblyCleanupExecutorGetter.get -> System.Func<System.Threading.Tasks.Task!>!
6+
Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyInitializeExecutionContext
7+
Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyInitializeExecutionContext.AssemblyInitializeExecutionContext() -> void
8+
Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyInitializeExecutionContext.AssemblyInitializeExecutorGetter.get -> System.Func<System.Threading.Tasks.Task!>!
39
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AssertAreEqualInterpolatedStringHandler<TArgument>
410
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AssertAreEqualInterpolatedStringHandler<TArgument>.AppendFormatted(object? value, int alignment = 0, string? format = null) -> void
511
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AssertAreEqualInterpolatedStringHandler<TArgument>.AppendFormatted(string? value) -> void
@@ -266,3 +272,5 @@ static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ThrowsExactly<TExcept
266272
static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ThrowsExactly<TException>(System.Action! action, System.Func<System.Exception?, string!>! messageBuilder) -> TException!
267273
static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ThrowsExactlyAsync<TException>(System.Func<System.Threading.Tasks.Task!>! action, string! message = "", params object![]! messageArgs) -> System.Threading.Tasks.Task<TException!>!
268274
static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ThrowsExactlyAsync<TException>(System.Func<System.Threading.Tasks.Task!>! action, System.Func<System.Exception?, string!>! messageBuilder) -> System.Threading.Tasks.Task<TException!>!
275+
virtual Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyCleanupAttribute.ExecuteAsync(Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyCleanupExecutionContext assemblyCleanupContext) -> System.Threading.Tasks.Task!
276+
virtual Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyInitializeAttribute.ExecuteAsync(Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyInitializeExecutionContext assemblyInitializeContext) -> System.Threading.Tasks.Task!

0 commit comments

Comments
 (0)