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

Class Initialize inheritance #143 #577

Merged
merged 24 commits into from
Jul 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
41362e3
Implemented Initialize Inheritance for ClassInitialize attribute
parrainc Oct 17, 2018
8de73ff
Implemented Cleanup inheritance behavior for ClassCleanup attr
parrainc Nov 7, 2018
cd2b1e1
Merge branch 'master' of https://github.com/Microsoft/testfx into iss…
parrainc Nov 7, 2018
5d65c80
Merge branch 'master' of https://github.com/Microsoft/testfx into iss…
parrainc Mar 8, 2019
0042875
Removed OnceBeforeAnyDerivedClasses from ClassInitializeInheritance enum
parrainc Mar 9, 2019
9e2e653
Changed UTF.TestClass for custom testclass attr to avoid test ouput w…
parrainc Mar 10, 2019
5e527d5
Updated RunClassCleanup to prevent running base cleanup without havin…
parrainc Mar 10, 2019
50aae98
Fix name typo
parrainc Mar 10, 2019
b258d16
added doc for BaseClassInitializeMethodsDict prop
parrainc Mar 10, 2019
cf6f872
Updates per review comments
parrainc Mar 14, 2019
61c1c36
Merge branch 'master' of https://github.com/Microsoft/testfx into iss…
parrainc Mar 14, 2019
198b7ac
Merge branch 'master' into issue143-classinitializeinh
jayaranigarg Apr 3, 2019
f951588
Merge branch 'master' of https://github.com/Microsoft/testfx into iss…
parrainc Apr 6, 2019
32783e6
Merge branch 'issue143-classinitializeinh' of https://github.com/parr…
parrainc Apr 6, 2019
54ed1f9
Merge branch 'master' into issue143-classinitializeinh
jayaranigarg Apr 9, 2019
5ffaeb6
Merge branch 'master' into issue143-classinitializeinh
jayaranigarg Apr 9, 2019
63f3884
Merge branch 'master' of https://github.com/Microsoft/testfx into iss…
parrainc Apr 10, 2019
2f9264f
Merge branch 'issue143-classinitializeinh' of https://github.com/parr…
parrainc Apr 10, 2019
a6dd345
making inheritance behavior enum more generic and updating tests
parrainc Apr 10, 2019
a256006
done some refactoring and added/fixed a few tests
parrainc Apr 10, 2019
82aad2c
updates as per review comments
parrainc Apr 13, 2019
987ca72
updated classinfo and tests
parrainc Apr 13, 2019
023d041
updated classcleanup 1-param ctor
parrainc Apr 13, 2019
38d28fe
updating doc message for 1-param ctors
parrainc Apr 13, 2019
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
85 changes: 73 additions & 12 deletions src/Adapter/MSTest.CoreAdapter/Execution/TestClassInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Extensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
Expand Down Expand Up @@ -48,6 +49,8 @@ internal TestClassInfo(
this.ClassType = type;
this.Constructor = constructor;
this.TestContextProperty = testContextProperty;
this.BaseClassCleanupMethodsStack = new Stack<MethodInfo>();
this.BaseClassInitAndCleanupMethods = new Queue<Tuple<MethodInfo, MethodInfo>>();
this.BaseTestInitializeMethodsQueue = new Queue<MethodInfo>();
this.BaseTestCleanupMethodsQueue = new Queue<MethodInfo>();
this.Parent = parent;
Expand Down Expand Up @@ -107,6 +110,11 @@ internal set
/// </summary>
public bool IsClassInitializeExecuted { get; internal set; }

/// <summary>
/// Gets a stack of class cleanup methods to be executed.
/// </summary>
public Stack<MethodInfo> BaseClassCleanupMethodsStack { get; internal set; }

/// <summary>
/// Gets the exception thrown during <see cref="ClassInitializeAttribute"/> method invocation.
/// </summary>
Expand Down Expand Up @@ -157,6 +165,11 @@ public bool HasExecutableCleanupMethod
}
}

/// <summary>
/// Gets a tuples' queue of class initialize/cleanup methods to call for this type.
/// </summary>
public Queue<Tuple<MethodInfo, MethodInfo>> BaseClassInitAndCleanupMethods { get; private set; }

/// <summary>
/// Gets the test initialize method.
/// </summary>
Expand Down Expand Up @@ -219,8 +232,8 @@ internal set
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")]
public void RunClassInitialize(TestContext testContext)
{
// If no class initialize return
if (this.ClassInitializeMethod == null)
// If no class initialize and no base class initialize, return
if (this.ClassInitializeMethod is null && !this.BaseClassInitAndCleanupMethods.Any(p => p.Item1 != null))
{
return;
}
Expand All @@ -230,8 +243,50 @@ public void RunClassInitialize(TestContext testContext)
throw new NullReferenceException(Resource.TestContextIsNull);
}

// If class initialization is not done, then do it.
if (!this.IsClassInitializeExecuted)
MethodInfo initializeMethod = null;
string failedClassInitializeMethodName = string.Empty;

// If class initialization is done, just return
if (this.IsClassInitializeExecuted)
{
return;
}

// Aquiring a lock is usually a costly operation which does not need to be
// performed every time if the class init is already executed.
lock (this.testClassExecuteSyncObject)
{
// Perform a check again.
if (this.IsClassInitializeExecuted)
{
return;
}

try
{
parrainc marked this conversation as resolved.
Show resolved Hide resolved
// ClassInitialize methods for base classes are called in reverse order of discovery
// Base -> Child TestClass
var baseClassInitializeStack = new Stack<Tuple<MethodInfo, MethodInfo>>(
this.BaseClassInitAndCleanupMethods.Where(p => p.Item1 != null));

while (baseClassInitializeStack.Count > 0)
{
var baseInitCleanupMethods = baseClassInitializeStack.Pop();
initializeMethod = baseInitCleanupMethods.Item1;
initializeMethod?.InvokeAsSynchronousTask(null, testContext);
this.BaseClassCleanupMethodsStack.Push(baseInitCleanupMethods.Item2);
}
}
catch (Exception ex)
{
this.ClassInitializationException = ex;
failedClassInitializeMethodName = initializeMethod.Name;
}
}

// If class initialization is not done and class initialize method is not null,
// and class initialization exception is null, then do it.
if (!this.IsClassInitializeExecuted && this.classInitializeMethod != null && this.ClassInitializationException == null)
{
// Aquiring a lock is usually a costly operation which does not need to be
parrainc marked this conversation as resolved.
Show resolved Hide resolved
// performed every time if the class init is already executed.
Expand All @@ -247,6 +302,7 @@ public void RunClassInitialize(TestContext testContext)
catch (Exception ex)
{
this.ClassInitializationException = ex;
failedClassInitializeMethodName = this.ClassInitializeMethod.Name;
}
finally
{
Expand All @@ -271,15 +327,13 @@ public void RunClassInitialize(TestContext testContext)
var realException = this.ClassInitializationException.InnerException ?? this.ClassInitializationException;

var outcome = UnitTestOutcome.Failed;
string errorMessage = null;
StackTraceInformation exceptionStackTraceInfo = null;
if (!realException.TryGetUnitTestAssertException(out outcome, out errorMessage, out exceptionStackTraceInfo))
if (!realException.TryGetUnitTestAssertException(out outcome, out string errorMessage, out StackTraceInformation exceptionStackTraceInfo))
{
errorMessage = string.Format(
CultureInfo.CurrentCulture,
Resource.UTA_ClassInitMethodThrows,
this.ClassType.FullName,
this.ClassInitializeMethod.Name,
failedClassInitializeMethodName,
realException.GetType().ToString(),
StackTraceHelper.GetExceptionMessage(realException));

Expand All @@ -298,19 +352,26 @@ public void RunClassInitialize(TestContext testContext)
/// <returns>
/// Any exception that can be thrown as part of a class cleanup as warning messages.
/// </returns>
[SuppressMessageAttribute("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")]
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")]
parrainc marked this conversation as resolved.
Show resolved Hide resolved
public string RunClassCleanup()
{
if (this.ClassCleanupMethod == null)
if (this.ClassCleanupMethod is null && !this.BaseClassInitAndCleanupMethods.Any(p => p.Item2 != null))
parrainc marked this conversation as resolved.
Show resolved Hide resolved
{
return null;
}

if (this.IsClassInitializeExecuted || this.ClassInitializeMethod == null)
if (this.IsClassInitializeExecuted || this.ClassInitializeMethod is null || this.BaseClassCleanupMethodsStack.Any())
{
var classCleanupMethod = this.ClassCleanupMethod;
try
{
this.ClassCleanupMethod.InvokeAsSynchronousTask(null);
classCleanupMethod?.InvokeAsSynchronousTask(null);
var baseClassCleanupQueue = new Queue<MethodInfo>(this.BaseClassCleanupMethodsStack);
while (baseClassCleanupQueue.Count > 0)
{
classCleanupMethod = baseClassCleanupQueue.Dequeue();
classCleanupMethod?.InvokeAsSynchronousTask(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we be only executing cleanups whose inits have run? This seems to be running all cleanups.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could just push Cleanups that need to be executed into a Stack instead of ExecutedBaseClassInitializeMethods

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, I had a block of code that I removed right before pushing the latest changes, handling the logic for deciding those cleanup method that will be executed. But, now that you mentioned that last part, I think that'd be better. For that, renamed the property you mentioned, now it's called: BaseClassCleanupMethodsStack not sure if the name is 100% ok, and in the RunClassInitialize method, I'll be pushing the cleanups into that stack to then execute them in the RunClassCleanup method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. It doesn't look like we need a Queue here. We could just pop the methods off the stack..

}

return null;
}
Expand Down
99 changes: 89 additions & 10 deletions src/Adapter/MSTest.CoreAdapter/Execution/TypeCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,10 @@ private TestClassInfo CreateClassInfo(Type classType, TestMethod testMethod)
var classInitializeAttributeType = typeof(ClassInitializeAttribute);
var classCleanupAttributeType = typeof(ClassCleanupAttribute);

// List holding the instance of the initialize/cleanup methods
// to be passed into the tuples' queue when updating the class info.
var initAndCleanupMethods = new MethodInfo[2];

// List of instance methods present in the type as well its base type
// which is used to decide whether TestInitialize/TestCleanup methods
// present in the base type should be used or not. They are not used if
Expand All @@ -286,16 +290,8 @@ private TestClassInfo CreateClassInfo(Type classType, TestMethod testMethod)
// Update test initialize/cleanup method
this.UpdateInfoIfTestInitializeOrCleanupMethod(classInfo, methodInfo, isBase: false, instanceMethods: instanceMethods, testInitializeAttributeType: testInitializeAttributeType, testCleanupAttributeType: testCleanupAttributeType);

if (this.IsAssemblyOrClassInitializeMethod(methodInfo, classInitializeAttributeType))
{
// update class initialize method
classInfo.ClassInitializeMethod = methodInfo;
}
else if (this.IsAssemblyOrClassCleanupMethod(methodInfo, classCleanupAttributeType))
{
// update class cleanup method
classInfo.ClassCleanupMethod = methodInfo;
}
// Update class initialize/cleanup method
this.UpdateInfoIfClassInitializeOrCleanupMethod(classInfo, methodInfo, false, ref initAndCleanupMethods, classInitializeAttributeType, classCleanupAttributeType);
}

var baseType = classType.GetTypeInfo().BaseType;
Expand All @@ -308,8 +304,14 @@ private TestClassInfo CreateClassInfo(Type classType, TestMethod testMethod)
// Update test initialize/cleanup method from base type.
this.UpdateInfoIfTestInitializeOrCleanupMethod(classInfo, methodInfo, true, instanceMethods, testInitializeAttributeType, testCleanupAttributeType);
}

if (methodInfo.IsPublic && methodInfo.IsStatic)
{
this.UpdateInfoIfClassInitializeOrCleanupMethod(classInfo, methodInfo, true, ref initAndCleanupMethods, classInitializeAttributeType, classCleanupAttributeType);
}
}

this.UpdateInfoWithInitializeAndCleanupMethods(classInfo, ref initAndCleanupMethods);
baseType = baseType.GetTypeInfo().BaseType;
}

Expand Down Expand Up @@ -474,6 +476,83 @@ private bool IsAssemblyOrClassCleanupMethod(MethodInfo methodInfo, Type cleanupA

#endregion

/// <summary>
/// Update the classInfo with given initialize and cleanup methods.
/// </summary>
/// <param name="classInfo"> The Class Info. </param>
/// <param name="initAndCleanupMethods"> An array with the Initialize and Cleanup Methods Info. </param>
private void UpdateInfoWithInitializeAndCleanupMethods(
TestClassInfo classInfo,
ref MethodInfo[] initAndCleanupMethods)
{
if (initAndCleanupMethods is null)
{
return;
}

classInfo.BaseClassInitAndCleanupMethods.Enqueue(
new Tuple<MethodInfo, MethodInfo>(
initAndCleanupMethods.FirstOrDefault(),
initAndCleanupMethods.LastOrDefault()));

initAndCleanupMethods = null;
}

/// <summary>
/// Update the classInfo if the parameter method is a classInitialize/cleanup method
/// </summary>
/// <param name="classInfo"> The Class Info. </param>
/// <param name="methodInfo"> The Method Info. </param>
/// <param name="isBase"> Flag to check whether base class needs to be validated. </param>
/// <param name="initAndCleanupMethods"> An array with Initialize/Cleanup methods. </param>
/// <param name="classInitializeAttributeType"> The Class Initialize Attribute Type. </param>
/// <param name="classCleanupAttributeType"> The Class Cleanup Attribute Type. </param>
private void UpdateInfoIfClassInitializeOrCleanupMethod(
TestClassInfo classInfo,
MethodInfo methodInfo,
bool isBase,
ref MethodInfo[] initAndCleanupMethods,
Type classInitializeAttributeType,
Type classCleanupAttributeType)
{
var isInitializeMethod = this.IsAssemblyOrClassInitializeMethod(methodInfo, classInitializeAttributeType);
var isCleanupMethod = this.IsAssemblyOrClassCleanupMethod(methodInfo, classCleanupAttributeType);

if (isInitializeMethod)
{
if (isBase)
{
if (((ClassInitializeAttribute)this.reflectionHelper.GetCustomAttribute(methodInfo, classInitializeAttributeType))
.InheritanceBehavior == InheritanceBehavior.BeforeEachDerivedClass)
{
initAndCleanupMethods[0] = methodInfo;
}
}
else
{
// update class initialize method
classInfo.ClassInitializeMethod = methodInfo;
}
}

if (isCleanupMethod)
{
if (isBase)
{
if (((ClassCleanupAttribute)this.reflectionHelper.GetCustomAttribute(methodInfo, classCleanupAttributeType))
.InheritanceBehavior == InheritanceBehavior.BeforeEachDerivedClass)
{
initAndCleanupMethods[1] = methodInfo;
}
}
else
{
// update class cleanup method
classInfo.ClassCleanupMethod = methodInfo;
}
}
}

/// <summary>
/// Update the classInfo if the parameter method is a testInitialize/cleanup method
/// </summary>
Expand Down
71 changes: 69 additions & 2 deletions src/TestFramework/MSTest.Core/Attributes/VSTestAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;

#pragma warning disable SA1402 // FileMayOnlyContainASingleType
#pragma warning disable SA1649 // SA1649FileNameMustMatchTypeName
Expand All @@ -25,6 +23,25 @@ public enum TestTimeout
Infinite = int.MaxValue
}

/// <summary>
/// Enumeration for inheritance behavior, that can be used with both the <see cref="ClassInitializeAttribute"/> class
/// and <see cref="ClassCleanupAttribute"/> class.
/// Defines the behavior of the ClassInitialize and ClassCleanup methods of base classes.
/// The type of the enumeration must match
/// </summary>
public enum InheritanceBehavior
{
/// <summary>
/// None.
/// </summary>
None,

/// <summary>
/// Before each derived class.
/// </summary>
BeforeEachDerivedClass
}

/// <summary>
/// The test class attribute.
/// </summary>
Expand Down Expand Up @@ -156,6 +173,31 @@ public TestPropertyAttribute(string name, string value)
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class ClassInitializeAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="ClassInitializeAttribute"/> class.
/// ClassInitializeAttribute
/// </summary>
public ClassInitializeAttribute()
{
this.InheritanceBehavior = InheritanceBehavior.None;
}

/// <summary>
/// Initializes a new instance of the <see cref="ClassInitializeAttribute"/> class.
/// ClassInitializeAttribute
/// </summary>
/// <param name="inheritanceBehavior">
/// Specifies the ClassInitialize Inheritance Behavior
/// </param>
public ClassInitializeAttribute(InheritanceBehavior inheritanceBehavior)
{
this.InheritanceBehavior = inheritanceBehavior;
}

/// <summary>
/// Gets the Inheritance Behavior
/// </summary>
public InheritanceBehavior InheritanceBehavior { get; private set; }
}

/// <summary>
Expand All @@ -164,6 +206,31 @@ public sealed class ClassInitializeAttribute : Attribute
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class ClassCleanupAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="ClassCleanupAttribute"/> class.
/// ClassCleanupAttribute
/// </summary>
public ClassCleanupAttribute()
{
this.InheritanceBehavior = InheritanceBehavior.None;
}

/// <summary>
/// Initializes a new instance of the <see cref="ClassCleanupAttribute"/> class.
/// ClassCleanupAttribute
/// </summary>
/// <param name="inheritanceBehavior">
/// Specifies the ClassCleanup Inheritance Behavior
/// </param>
public ClassCleanupAttribute(InheritanceBehavior inheritanceBehavior)
{
this.InheritanceBehavior = inheritanceBehavior;
}

/// <summary>
/// Gets the Inheritance Behavior
/// </summary>
public InheritanceBehavior InheritanceBehavior { get; private set; }
}

/// <summary>
Expand Down
Loading