Skip to content

Commit 2474f6c

Browse files
committed
Initiate work for conditional tests
1 parent 8cec650 commit 2474f6c

16 files changed

+202
-94
lines changed

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

+2-4
Original file line numberDiff line numberDiff line change
@@ -629,8 +629,7 @@ internal void ExecuteClassCleanup(TestContext testContext)
629629
{
630630
if (classCleanupMethod is not null)
631631
{
632-
if (ClassAttribute.IgnoreMessage is null &&
633-
!ReflectHelper.Instance.IsNonDerivedAttributeDefined<IgnoreAttribute>(classCleanupMethod.DeclaringType!, false))
632+
if (!AttributeHelpers.IsIgnored(classCleanupMethod.DeclaringType!, out _))
634633
{
635634
ClassCleanupException = InvokeCleanupMethod(classCleanupMethod, remainingCleanupCount: BaseClassCleanupMethods.Count, testContext);
636635
}
@@ -641,8 +640,7 @@ internal void ExecuteClassCleanup(TestContext testContext)
641640
for (int i = 0; i < BaseClassCleanupMethods.Count; i++)
642641
{
643642
classCleanupMethod = BaseClassCleanupMethods[i];
644-
if (ClassAttribute.IgnoreMessage is null &&
645-
!ReflectHelper.Instance.IsNonDerivedAttributeDefined<IgnoreAttribute>(classCleanupMethod.DeclaringType!, false))
643+
if (!AttributeHelpers.IsIgnored(classCleanupMethod.DeclaringType!, out _))
646644
{
647645
ClassCleanupException = InvokeCleanupMethod(classCleanupMethod, remainingCleanupCount: BaseClassCleanupMethods.Count - 1 - i, testContext);
648646
if (ClassCleanupException is not null)

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

+7-25
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ private static void RunAssemblyCleanupIfNeeded(ITestContext testContext, ClassCl
344344
/// <param name="testMethodInfo">The testMethodInfo.</param>
345345
/// <param name="notRunnableResult">The results to return if the test method is not runnable.</param>
346346
/// <returns>whether the given testMethod is runnable.</returns>
347-
private bool IsTestMethodRunnable(
347+
private static bool IsTestMethodRunnable(
348348
TestMethod testMethod,
349349
TestMethodInfo? testMethodInfo,
350350
[NotNullWhen(false)] out TestResult[]? notRunnableResult)
@@ -381,34 +381,16 @@ private bool IsTestMethodRunnable(
381381
}
382382
}
383383

384-
// TODO: Executor should never be null. Is it incorrectly annotated?
385-
string? ignoreMessage = testMethodInfo.Parent.ClassAttribute.IgnoreMessage ?? testMethodInfo.TestMethodOptions.Executor?.IgnoreMessage;
386-
if (ignoreMessage is not null)
387-
{
388-
notRunnableResult =
389-
[
390-
new TestResult()
391-
{
392-
Outcome = UTF.UnitTestOutcome.Ignored,
393-
IgnoreReason = ignoreMessage,
394-
}
395-
];
396-
return false;
397-
}
398-
399-
IgnoreAttribute? ignoreAttributeOnClass =
400-
_reflectHelper.GetFirstNonDerivedAttributeOrDefault<IgnoreAttribute>(testMethodInfo.Parent.ClassType, inherit: false);
401-
ignoreMessage = ignoreAttributeOnClass?.IgnoreMessage;
402-
403-
IgnoreAttribute? ignoreAttributeOnMethod =
404-
_reflectHelper.GetFirstNonDerivedAttributeOrDefault<IgnoreAttribute>(testMethodInfo.TestMethod, inherit: false);
384+
bool shouldIgnoreClass = AttributeHelpers.IsIgnored(testMethodInfo.Parent.ClassType, out string? ignoreMessageOnClass);
385+
bool shouldIgnoreMethod = AttributeHelpers.IsIgnored(testMethodInfo.TestMethod, out string? ignoreMessageOnMethod);
405386

406-
if (StringEx.IsNullOrEmpty(ignoreMessage) && ignoreAttributeOnMethod is not null)
387+
string? ignoreMessage = ignoreMessageOnClass;
388+
if (StringEx.IsNullOrEmpty(ignoreMessage) && shouldIgnoreMethod)
407389
{
408-
ignoreMessage = ignoreAttributeOnMethod.IgnoreMessage;
390+
ignoreMessage = ignoreMessageOnMethod;
409391
}
410392

411-
if (ignoreAttributeOnClass is not null || ignoreAttributeOnMethod is not null)
393+
if (shouldIgnoreClass || shouldIgnoreMethod)
412394
{
413395
notRunnableResult =
414396
[
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
using System;
5+
using System.Collections.Generic;
6+
using System.Text;
7+
8+
using Microsoft.VisualStudio.TestTools.UnitTesting;
9+
10+
namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
11+
12+
internal static class AttributeHelpers
13+
{
14+
public static bool IsIgnored(ICustomAttributeProvider type, out string? ignoreMessage)
15+
{
16+
IEnumerable<ConditionalTestBaseAttribute> attributes = ReflectHelper.Instance.GetDerivedAttributes<ConditionalTestBaseAttribute>(type, inherit: false);
17+
foreach (ConditionalTestBaseAttribute attribute in attributes)
18+
{
19+
if (attribute.ShouldIgnore)
20+
{
21+
ignoreMessage = attribute.ConditionalIgnoreMessage;
22+
return true;
23+
}
24+
}
25+
26+
ignoreMessage = null;
27+
return false;
28+
}
29+
}

src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ Rule ID | Category | Severity | Notes
77
MSTEST0038 | Usage | Warning | AvoidAssertAreSameWithValueTypesAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0038)
88
MSTEST0039 | Usage | Info | UseNewerAssertThrowsAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0039)
99
MSTEST0040 | Usage | Warning | AvoidUsingAssertsInAsyncVoidContextAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0040)
10+
MSTEST0041 | Usage | Warning | UseConditionalTestBaseWithTestClassAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0041)

src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs

+1
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,5 @@ internal static class DiagnosticIds
4545
public const string AvoidAssertAreSameWithValueTypesRuleId = "MSTEST0038";
4646
public const string UseNewerAssertThrowsRuleId = "MSTEST0039";
4747
public const string AvoidUsingAssertsInAsyncVoidContextRuleId = "MSTEST0040";
48+
public const string UseConditionalTestBaseWithTestClassRuleId = "MSTEST0041";
4849
}

src/Analyzers/MSTest.Analyzers/Helpers/WellKnownTypeNames.cs

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ internal static class WellKnownTypeNames
1414
public const string MicrosoftVisualStudioTestToolsUnitTestingClassCleanupExecutionAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.ClassCleanupExecutionAttribute";
1515
public const string MicrosoftVisualStudioTestToolsUnitTestingClassInitializeAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.ClassInitializeAttribute";
1616
public const string MicrosoftVisualStudioTestToolsUnitTestingCollectionAssert = "Microsoft.VisualStudio.TestTools.UnitTesting.CollectionAssert";
17+
public const string MicrosoftVisualStudioTestToolsUnitTestingConditionalTestBaseAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.ConditionalTestBaseAttribute";
1718
public const string MicrosoftVisualStudioTestToolsUnitTestingCssIterationAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.CssIterationAttribute";
1819
public const string MicrosoftVisualStudioTestToolsUnitTestingCssProjectStructureAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.CssProjectStructureAttribute";
1920
public const string MicrosoftVisualStudioTestToolsUnitTestingDataRowAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.DataRowAttribute";

src/Analyzers/MSTest.Analyzers/PublicAPI.Unshipped.txt

+4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ MSTest.Analyzers.AvoidAssertAreSameWithValueTypesAnalyzer
33
MSTest.Analyzers.AvoidAssertAreSameWithValueTypesAnalyzer.AvoidAssertAreSameWithValueTypesAnalyzer() -> void
44
MSTest.Analyzers.AvoidUsingAssertsInAsyncVoidContextAnalyzer
55
MSTest.Analyzers.AvoidUsingAssertsInAsyncVoidContextAnalyzer.AvoidUsingAssertsInAsyncVoidContextAnalyzer() -> void
6+
MSTest.Analyzers.UseConditionalTestBaseWithTestClassAnalyzer
7+
MSTest.Analyzers.UseConditionalTestBaseWithTestClassAnalyzer.UseConditionalTestBaseWithTestClassAnalyzer() -> void
68
override MSTest.Analyzers.AvoidAssertAreSameWithValueTypesAnalyzer.Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisContext! context) -> void
79
override MSTest.Analyzers.AvoidAssertAreSameWithValueTypesAnalyzer.SupportedDiagnostics.get -> System.Collections.Immutable.ImmutableArray<Microsoft.CodeAnalysis.DiagnosticDescriptor!>
810
override MSTest.Analyzers.AvoidUsingAssertsInAsyncVoidContextAnalyzer.Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisContext! context) -> void
911
override MSTest.Analyzers.AvoidUsingAssertsInAsyncVoidContextAnalyzer.SupportedDiagnostics.get -> System.Collections.Immutable.ImmutableArray<Microsoft.CodeAnalysis.DiagnosticDescriptor!>
12+
override MSTest.Analyzers.UseConditionalTestBaseWithTestClassAnalyzer.Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisContext! context) -> void
13+
override MSTest.Analyzers.UseConditionalTestBaseWithTestClassAnalyzer.SupportedDiagnostics.get -> System.Collections.Immutable.ImmutableArray<Microsoft.CodeAnalysis.DiagnosticDescriptor!>

src/Analyzers/MSTest.Analyzers/Resources.Designer.cs

+21-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Analyzers/MSTest.Analyzers/Resources.resx

+7-1
Original file line numberDiff line numberDiff line change
@@ -567,4 +567,10 @@ The type declaring these methods should also respect the following rules:
567567
<data name="AvoidUsingAssertsInAsyncVoidContextDescription" xml:space="preserve">
568568
<value>Do not assert inside 'async void' methods, local functions, or lambdas. Exceptions that are thrown in this context will be unhandled exceptions. When using VSTest under .NET Framework, they will be silently swallowed. When using Microsoft.Testing.Platform or VSTest under modern .NET, they may crash the process.</value>
569569
</data>
570-
</root>
570+
<data name="UseConditionalTestBaseWithTestClassTitle" xml:space="preserve">
571+
<value>Use 'ConditionalTestBaseAttribute' on test classes</value>
572+
</data>
573+
<data name="UseConditionalTestBaseWithTestClassMessageFormat" xml:space="preserve">
574+
<value>The attribute '{0}' which derives from 'ConditionalTestBaseAttribute' should be used only on classes marked with `TestClassAttribute`</value>
575+
</data>
576+
</root>

src/Analyzers/MSTest.Analyzers/UseAttributeOnTestMethodAnalyzer.cs

+24-3
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,18 @@ public sealed class UseAttributeOnTestMethodAnalyzer : DiagnosticAnalyzer
114114
DiagnosticSeverity.Info,
115115
isEnabledByDefault: true);
116116

117+
private const string ConditionalTestBaseAttributeShortName = "ConditionalTestBaseAttribute";
118+
internal static readonly DiagnosticDescriptor ConditionalTestBaseRule = DiagnosticDescriptorHelper.Create(
119+
DiagnosticIds.UseAttributeOnTestMethodRuleId,
120+
title: new LocalizableResourceString(
121+
nameof(Resources.UseAttributeOnTestMethodAnalyzerTitle), Resources.ResourceManager, typeof(Resources), ConditionalTestBaseAttributeShortName),
122+
messageFormat: new LocalizableResourceString(
123+
nameof(Resources.UseAttributeOnTestMethodAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources), ConditionalTestBaseAttributeShortName),
124+
description: null,
125+
Category.Usage,
126+
DiagnosticSeverity.Info,
127+
isEnabledByDefault: true);
128+
117129
// IMPORTANT: Remember to add any new rule to the rule tuple.
118130
private static readonly List<(string AttributeFullyQualifiedName, DiagnosticDescriptor Rule)> RuleTuples =
119131
[
@@ -124,11 +136,20 @@ public sealed class UseAttributeOnTestMethodAnalyzer : DiagnosticAnalyzer
124136
(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingDescriptionAttribute, DescriptionRule),
125137
(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingExpectedExceptionBaseAttribute, ExpectedExceptionRule),
126138
(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingCssIterationAttribute, CssIterationRule),
127-
(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingCssProjectStructureAttribute, CssProjectStructureRule)
139+
(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingCssProjectStructureAttribute, CssProjectStructureRule),
140+
(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingConditionalTestBaseAttribute, ConditionalTestBaseRule),
128141
];
129142

130-
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
131-
= ImmutableArray.Create(OwnerRule);
143+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
144+
ImmutableArray.Create(
145+
OwnerRule,
146+
PriorityRule,
147+
TestPropertyRule,
148+
WorkItemRule,
149+
DescriptionRule,
150+
ExpectedExceptionRule,
151+
CssIterationRule,
152+
CssProjectStructureRule);
132153

133154
public override void Initialize(AnalysisContext context)
134155
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
using System.Collections.Immutable;
4+
5+
using Analyzer.Utilities.Extensions;
6+
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.Diagnostics;
9+
10+
using MSTest.Analyzers.Helpers;
11+
12+
namespace MSTest.Analyzers;
13+
14+
/// <summary>
15+
/// MSTEST0041: <inheritdoc cref="Resources.UseConditionalTestBaseWithTestClassTitle"/>.
16+
/// </summary>
17+
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
18+
public sealed class UseConditionalTestBaseWithTestClassAnalyzer : DiagnosticAnalyzer
19+
{
20+
private static readonly LocalizableResourceString Title = new(nameof(Resources.UseConditionalTestBaseWithTestClassTitle), Resources.ResourceManager, typeof(Resources));
21+
private static readonly LocalizableResourceString MessageFormat = new(nameof(Resources.UseConditionalTestBaseWithTestClassMessageFormat), Resources.ResourceManager, typeof(Resources));
22+
23+
internal static readonly DiagnosticDescriptor UseConditionalTestBaseWithTestClassRule = DiagnosticDescriptorHelper.Create(
24+
DiagnosticIds.UseConditionalTestBaseWithTestClassRuleId,
25+
Title,
26+
MessageFormat,
27+
null,
28+
Category.Usage,
29+
DiagnosticSeverity.Warning,
30+
isEnabledByDefault: true);
31+
32+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
33+
= ImmutableArray.Create(UseConditionalTestBaseWithTestClassRule);
34+
35+
public override void Initialize(AnalysisContext context)
36+
{
37+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
38+
context.EnableConcurrentExecution();
39+
40+
context.RegisterCompilationStartAction(context =>
41+
{
42+
if (context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingTestClassAttribute, out INamedTypeSymbol? testClassAttributeSymbol) &&
43+
context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingConditionalTestBaseAttribute, out INamedTypeSymbol? conditionalTestBaseAttributeSymbol))
44+
{
45+
context.RegisterSymbolAction(
46+
context => AnalyzeSymbol(context, testClassAttributeSymbol, conditionalTestBaseAttributeSymbol),
47+
SymbolKind.NamedType);
48+
}
49+
});
50+
}
51+
52+
private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbol testClassAttributeSymbol, INamedTypeSymbol conditionalTestBaseAttributeSymbol)
53+
{
54+
bool hasConditionalTestBaseAttribute = false;
55+
bool isTestClass = false;
56+
foreach (AttributeData attribute in context.Symbol.GetAttributes())
57+
{
58+
if (attribute.AttributeClass.Inherits(testClassAttributeSymbol))
59+
{
60+
isTestClass = true;
61+
}
62+
else if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, conditionalTestBaseAttributeSymbol))
63+
{
64+
hasConditionalTestBaseAttribute = true;
65+
}
66+
}
67+
68+
if (hasConditionalTestBaseAttribute && !isTestClass)
69+
{
70+
context.ReportDiagnostic(context.Symbol.CreateDiagnostic(UseConditionalTestBaseWithTestClassRule));
71+
}
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
/// This attribute is used to ignore a test class or a test method, based on a condition and using an optional message.
8+
/// </summary>
9+
/// <remarks>
10+
/// This attribute isn't inherited. Applying it to a base class will not cause derived classes to be ignored.
11+
/// </remarks>
12+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)]
13+
public abstract class ConditionalTestBaseAttribute : Attribute
14+
{
15+
/// <summary>
16+
/// Gets the ignore message (in case <see cref="ShouldIgnore"/> returns <see langword="true"/>) indicating
17+
/// the reason for ignoring the test method or test class.
18+
/// </summary>
19+
public abstract string? ConditionalIgnoreMessage { get; }
20+
21+
/// <summary>
22+
/// Gets a value indicating whether the test method or test class should be ignored.
23+
/// </summary>
24+
public abstract bool ShouldIgnore { get; }
25+
}

0 commit comments

Comments
 (0)