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

Support conditional tests #4734

Merged
merged 11 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
6 changes: 2 additions & 4 deletions src/Adapter/MSTest.TestAdapter/Execution/TestClassInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -629,8 +629,7 @@ internal void ExecuteClassCleanup(TestContext testContext)
{
if (classCleanupMethod is not null)
{
if (ClassAttribute.IgnoreMessage is null &&
!ReflectHelper.Instance.IsNonDerivedAttributeDefined<IgnoreAttribute>(classCleanupMethod.DeclaringType!, false))
if (!AttributeHelpers.IsIgnored(classCleanupMethod.DeclaringType!, out _))
{
ClassCleanupException = InvokeCleanupMethod(classCleanupMethod, remainingCleanupCount: BaseClassCleanupMethods.Count, testContext);
}
Expand All @@ -641,8 +640,7 @@ internal void ExecuteClassCleanup(TestContext testContext)
for (int i = 0; i < BaseClassCleanupMethods.Count; i++)
{
classCleanupMethod = BaseClassCleanupMethods[i];
if (ClassAttribute.IgnoreMessage is null &&
!ReflectHelper.Instance.IsNonDerivedAttributeDefined<IgnoreAttribute>(classCleanupMethod.DeclaringType!, false))
if (!AttributeHelpers.IsIgnored(classCleanupMethod.DeclaringType!, out _))
{
ClassCleanupException = InvokeCleanupMethod(classCleanupMethod, remainingCleanupCount: BaseClassCleanupMethods.Count - 1 - i, testContext);
if (ClassCleanupException is not null)
Expand Down
32 changes: 7 additions & 25 deletions src/Adapter/MSTest.TestAdapter/Execution/UnitTestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ private static void RunAssemblyCleanupIfNeeded(ITestContext testContext, ClassCl
/// <param name="testMethodInfo">The testMethodInfo.</param>
/// <param name="notRunnableResult">The results to return if the test method is not runnable.</param>
/// <returns>whether the given testMethod is runnable.</returns>
private bool IsTestMethodRunnable(
private static bool IsTestMethodRunnable(
TestMethod testMethod,
TestMethodInfo? testMethodInfo,
[NotNullWhen(false)] out TestResult[]? notRunnableResult)
Expand Down Expand Up @@ -381,34 +381,16 @@ private bool IsTestMethodRunnable(
}
}

// TODO: Executor should never be null. Is it incorrectly annotated?
string? ignoreMessage = testMethodInfo.Parent.ClassAttribute.IgnoreMessage ?? testMethodInfo.TestMethodOptions.Executor?.IgnoreMessage;
if (ignoreMessage is not null)
{
notRunnableResult =
[
new TestResult()
{
Outcome = UTF.UnitTestOutcome.Ignored,
IgnoreReason = ignoreMessage,
}
];
return false;
}

IgnoreAttribute? ignoreAttributeOnClass =
_reflectHelper.GetFirstNonDerivedAttributeOrDefault<IgnoreAttribute>(testMethodInfo.Parent.ClassType, inherit: false);
ignoreMessage = ignoreAttributeOnClass?.IgnoreMessage;

IgnoreAttribute? ignoreAttributeOnMethod =
_reflectHelper.GetFirstNonDerivedAttributeOrDefault<IgnoreAttribute>(testMethodInfo.TestMethod, inherit: false);
bool shouldIgnoreClass = AttributeHelpers.IsIgnored(testMethodInfo.Parent.ClassType, out string? ignoreMessageOnClass);
bool shouldIgnoreMethod = AttributeHelpers.IsIgnored(testMethodInfo.TestMethod, out string? ignoreMessageOnMethod);

if (StringEx.IsNullOrEmpty(ignoreMessage) && ignoreAttributeOnMethod is not null)
string? ignoreMessage = ignoreMessageOnClass;
if (StringEx.IsNullOrEmpty(ignoreMessage) && shouldIgnoreMethod)
{
ignoreMessage = ignoreAttributeOnMethod.IgnoreMessage;
ignoreMessage = ignoreMessageOnMethod;
}

if (ignoreAttributeOnClass is not null || ignoreAttributeOnMethod is not null)
if (shouldIgnoreClass || shouldIgnoreMethod)
{
notRunnableResult =
[
Expand Down
40 changes: 40 additions & 0 deletions src/Adapter/MSTest.TestAdapter/Helpers/AttributeHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;

internal static class AttributeHelpers
{
public static bool IsIgnored(ICustomAttributeProvider type, out string? ignoreMessage)
Evangelink marked this conversation as resolved.
Show resolved Hide resolved
{
IEnumerable<ConditionBaseAttribute> attributes = ReflectHelper.Instance.GetDerivedAttributes<ConditionBaseAttribute>(type, inherit: false);
IEnumerable<IGrouping<string, ConditionBaseAttribute>> groups = attributes.GroupBy(attr => attr.GroupName);
foreach (IGrouping<string, ConditionBaseAttribute>? group in groups)
{
bool atLeastOneInGroupIsSatisfied = false;
string? firstNonSatisfiedMatch = null;
foreach (ConditionBaseAttribute attribute in group)
{
bool shouldRun = attribute.Mode == Mode.Include ? attribute.ShouldRun : !attribute.ShouldRun;
if (attribute.ShouldRun)
Evangelink marked this conversation as resolved.
Show resolved Hide resolved
{
atLeastOneInGroupIsSatisfied = true;
break;
}

firstNonSatisfiedMatch ??= attribute.ConditionalIgnoreMessage;
}

if (!atLeastOneInGroupIsSatisfied)
{
ignoreMessage = firstNonSatisfiedMatch;
return true;
}
}

ignoreMessage = null;
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Rule ID | Category | Severity | Notes
MSTEST0038 | Usage | Warning | AvoidAssertAreSameWithValueTypesAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0038)
MSTEST0039 | Usage | Info | UseNewerAssertThrowsAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0039)
MSTEST0040 | Usage | Warning | AvoidUsingAssertsInAsyncVoidContextAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0040)
MSTEST0041 | Usage | Warning | UseConditionBaseWithTestClassAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0041)
1 change: 1 addition & 0 deletions src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@ internal static class DiagnosticIds
public const string AvoidAssertAreSameWithValueTypesRuleId = "MSTEST0038";
public const string UseNewerAssertThrowsRuleId = "MSTEST0039";
public const string AvoidUsingAssertsInAsyncVoidContextRuleId = "MSTEST0040";
public const string UseConditionBaseWithTestClassRuleId = "MSTEST0041";
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ internal static class WellKnownTypeNames
public const string MicrosoftVisualStudioTestToolsUnitTestingClassCleanupExecutionAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.ClassCleanupExecutionAttribute";
public const string MicrosoftVisualStudioTestToolsUnitTestingClassInitializeAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.ClassInitializeAttribute";
public const string MicrosoftVisualStudioTestToolsUnitTestingCollectionAssert = "Microsoft.VisualStudio.TestTools.UnitTesting.CollectionAssert";
public const string MicrosoftVisualStudioTestToolsUnitTestingConditionBaseAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.ConditionBaseAttribute";
public const string MicrosoftVisualStudioTestToolsUnitTestingCssIterationAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.CssIterationAttribute";
public const string MicrosoftVisualStudioTestToolsUnitTestingCssProjectStructureAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.CssProjectStructureAttribute";
public const string MicrosoftVisualStudioTestToolsUnitTestingDataRowAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.DataRowAttribute";
Expand Down
4 changes: 4 additions & 0 deletions src/Analyzers/MSTest.Analyzers/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ MSTest.Analyzers.AvoidAssertAreSameWithValueTypesAnalyzer
MSTest.Analyzers.AvoidAssertAreSameWithValueTypesAnalyzer.AvoidAssertAreSameWithValueTypesAnalyzer() -> void
MSTest.Analyzers.AvoidUsingAssertsInAsyncVoidContextAnalyzer
MSTest.Analyzers.AvoidUsingAssertsInAsyncVoidContextAnalyzer.AvoidUsingAssertsInAsyncVoidContextAnalyzer() -> void
MSTest.Analyzers.UseConditionBaseWithTestClassAnalyzer
MSTest.Analyzers.UseConditionBaseWithTestClassAnalyzer.UseConditionBaseWithTestClassAnalyzer() -> void
override MSTest.Analyzers.AvoidAssertAreSameWithValueTypesAnalyzer.Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisContext! context) -> void
override MSTest.Analyzers.AvoidAssertAreSameWithValueTypesAnalyzer.SupportedDiagnostics.get -> System.Collections.Immutable.ImmutableArray<Microsoft.CodeAnalysis.DiagnosticDescriptor!>
override MSTest.Analyzers.AvoidUsingAssertsInAsyncVoidContextAnalyzer.Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisContext! context) -> void
override MSTest.Analyzers.AvoidUsingAssertsInAsyncVoidContextAnalyzer.SupportedDiagnostics.get -> System.Collections.Immutable.ImmutableArray<Microsoft.CodeAnalysis.DiagnosticDescriptor!>
override MSTest.Analyzers.UseConditionBaseWithTestClassAnalyzer.Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisContext! context) -> void
override MSTest.Analyzers.UseConditionBaseWithTestClassAnalyzer.SupportedDiagnostics.get -> System.Collections.Immutable.ImmutableArray<Microsoft.CodeAnalysis.DiagnosticDescriptor!>
24 changes: 21 additions & 3 deletions src/Analyzers/MSTest.Analyzers/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion src/Analyzers/MSTest.Analyzers/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -567,4 +567,10 @@ The type declaring these methods should also respect the following rules:
<data name="AvoidUsingAssertsInAsyncVoidContextDescription" xml:space="preserve">
<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>
</data>
</root>
<data name="UseConditionBaseWithTestClassTitle" xml:space="preserve">
<value>Use 'ConditionBaseAttribute' on test classes</value>
</data>
<data name="UseConditionBaseWithTestClassMessageFormat" xml:space="preserve">
<value>The attribute '{0}' which derives from 'ConditionBaseAttribute' should be used only on classes marked with `TestClassAttribute`</value>
</data>
</root>
27 changes: 24 additions & 3 deletions src/Analyzers/MSTest.Analyzers/UseAttributeOnTestMethodAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,18 @@ public sealed class UseAttributeOnTestMethodAnalyzer : DiagnosticAnalyzer
DiagnosticSeverity.Info,
isEnabledByDefault: true);

private const string ConditionBaseAttributeShortName = "ConditionBaseAttribute";
internal static readonly DiagnosticDescriptor ConditionBaseRule = DiagnosticDescriptorHelper.Create(
DiagnosticIds.UseAttributeOnTestMethodRuleId,
title: new LocalizableResourceString(
nameof(Resources.UseAttributeOnTestMethodAnalyzerTitle), Resources.ResourceManager, typeof(Resources), ConditionBaseAttributeShortName),
messageFormat: new LocalizableResourceString(
nameof(Resources.UseAttributeOnTestMethodAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources), ConditionBaseAttributeShortName),
description: null,
Category.Usage,
DiagnosticSeverity.Info,
isEnabledByDefault: true);

// IMPORTANT: Remember to add any new rule to the rule tuple.
private static readonly List<(string AttributeFullyQualifiedName, DiagnosticDescriptor Rule)> RuleTuples =
[
Expand All @@ -124,11 +136,20 @@ public sealed class UseAttributeOnTestMethodAnalyzer : DiagnosticAnalyzer
(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingDescriptionAttribute, DescriptionRule),
(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingExpectedExceptionBaseAttribute, ExpectedExceptionRule),
(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingCssIterationAttribute, CssIterationRule),
(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingCssProjectStructureAttribute, CssProjectStructureRule)
(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingCssProjectStructureAttribute, CssProjectStructureRule),
(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingConditionBaseAttribute, ConditionBaseRule),
];

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
= ImmutableArray.Create(OwnerRule);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(
OwnerRule,
PriorityRule,
TestPropertyRule,
WorkItemRule,
DescriptionRule,
ExpectedExceptionRule,
CssIterationRule,
CssProjectStructureRule);

public override void Initialize(AnalysisContext context)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Collections.Immutable;

using Analyzer.Utilities.Extensions;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

using MSTest.Analyzers.Helpers;

namespace MSTest.Analyzers;

/// <summary>
/// MSTEST0041: <inheritdoc cref="Resources.UseConditionBaseWithTestClassTitle"/>.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class UseConditionBaseWithTestClassAnalyzer : DiagnosticAnalyzer
{
private static readonly LocalizableResourceString Title = new(nameof(Resources.UseConditionBaseWithTestClassTitle), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableResourceString MessageFormat = new(nameof(Resources.UseConditionBaseWithTestClassMessageFormat), Resources.ResourceManager, typeof(Resources));

internal static readonly DiagnosticDescriptor UseConditionBaseWithTestClassRule = DiagnosticDescriptorHelper.Create(
DiagnosticIds.UseConditionBaseWithTestClassRuleId,
Title,
MessageFormat,
null,
Category.Usage,
DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
= ImmutableArray.Create(UseConditionBaseWithTestClassRule);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterCompilationStartAction(context =>
{
if (context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingTestClassAttribute, out INamedTypeSymbol? testClassAttributeSymbol) &&
context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingConditionBaseAttribute, out INamedTypeSymbol? ConditionBaseAttributeSymbol))
{
context.RegisterSymbolAction(
context => AnalyzeSymbol(context, testClassAttributeSymbol, ConditionBaseAttributeSymbol),
SymbolKind.NamedType);
}
});
}

private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbol testClassAttributeSymbol, INamedTypeSymbol ConditionBaseAttributeSymbol)
{
INamedTypeSymbol? ConditionBaseAttribute = null;
bool isTestClass = false;
foreach (AttributeData attribute in context.Symbol.GetAttributes())
{
if (attribute.AttributeClass.Inherits(testClassAttributeSymbol))
{
isTestClass = true;
}
else if (attribute.AttributeClass.Inherits(ConditionBaseAttributeSymbol))
{
ConditionBaseAttribute = attribute.AttributeClass;
}
}

if (ConditionBaseAttribute is not null && !isTestClass)
{
context.ReportDiagnostic(context.Symbol.CreateDiagnostic(UseConditionBaseWithTestClassRule, ConditionBaseAttribute.Name));
}
}
}
Loading
Loading