From 88aeaa8d9fe2cae60fb54dd40e72bb087f3bd8ae Mon Sep 17 00:00:00 2001 From: Manfred Brands Date: Sat, 20 Jan 2024 22:37:40 +0100 Subject: [PATCH 1/5] Update Test method parameter checks to account for CancellationToken --- .../Constants/NUnitFrameworkConstantsTests.cs | 10 +++ .../IMethodSymbolExtensionsTests.cs | 42 +++++++++-- .../TestCaseSourceUsesStringAnalyzerTests.cs | 60 ++++++++++++++- .../TestCaseUsageAnalyzerTests.cs | 55 ++++++++++++++ src/nunit.analyzers.tests/TestHelpers.cs | 17 ++++- .../TestMethodUsageAnalyzerTests.cs | 73 +++++++++++++++++++ .../nunit.analyzers.tests.csproj | 2 +- .../Constants/NUnitFrameworkConstants.cs | 5 ++ .../Extensions/IMethodSymbolExtensions.cs | 13 +++- .../TestCaseSourceUsesStringAnalyzer.cs | 17 ++++- .../TestCaseUsage/TestCaseUsageAnalyzer.cs | 19 +++-- .../TestMethodUsageAnalyzer.cs | 52 ++++++++----- 12 files changed, 325 insertions(+), 40 deletions(-) diff --git a/src/nunit.analyzers.tests/Constants/NUnitFrameworkConstantsTests.cs b/src/nunit.analyzers.tests/Constants/NUnitFrameworkConstantsTests.cs index 8b45e271..83d22e8b 100644 --- a/src/nunit.analyzers.tests/Constants/NUnitFrameworkConstantsTests.cs +++ b/src/nunit.analyzers.tests/Constants/NUnitFrameworkConstantsTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Threading; using NUnit.Analyzers.Constants; using NUnit.Framework; using NUnit.Framework.Constraints; @@ -164,6 +165,10 @@ public sealed class NUnitFrameworkConstantsTests (nameof(NUnitFrameworkConstants.NameOfSetUpAttribute), nameof(SetUpAttribute)), (nameof(NUnitFrameworkConstants.NameOfTearDownAttribute), nameof(TearDownAttribute)), +#if NUNIT4 + (nameof(NUnitFrameworkConstants.NameOfCancelAfterAttribute), nameof(CancelAfterAttribute)), +#endif + (nameof(NUnitFrameworkConstants.NameOfExpectedResult), nameof(TestAttribute.ExpectedResult)), (nameof(NUnitFrameworkConstants.NameOfConstraintExpressionAnd), nameof(EqualConstraint.And)), @@ -201,6 +206,11 @@ public sealed class NUnitFrameworkConstantsTests (nameof(NUnitFrameworkConstants.FullNameOfFixtureLifeCycleAttribute), typeof(FixtureLifeCycleAttribute)), (nameof(NUnitFrameworkConstants.FullNameOfLifeCycle), typeof(LifeCycle)), +#if NUNIT4 + (nameof(NUnitFrameworkConstants.FullNameOfCancelAfterAttribute), typeof(CancelAfterAttribute)), + (nameof(NUnitFrameworkConstants.FullNameOfCancellationToken), typeof(CancellationToken)), +#endif + (nameof(NUnitFrameworkConstants.FullNameOfSameAsConstraint), typeof(SameAsConstraint)), (nameof(NUnitFrameworkConstants.FullNameOfSomeItemsConstraint), typeof(SomeItemsConstraint)), (nameof(NUnitFrameworkConstants.FullNameOfEqualToConstraint), typeof(EqualConstraint)), diff --git a/src/nunit.analyzers.tests/Extensions/IMethodSymbolExtensionsTests.cs b/src/nunit.analyzers.tests/Extensions/IMethodSymbolExtensionsTests.cs index 66794356..c742cf2d 100644 --- a/src/nunit.analyzers.tests/Extensions/IMethodSymbolExtensionsTests.cs +++ b/src/nunit.analyzers.tests/Extensions/IMethodSymbolExtensionsTests.cs @@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using NUnit.Analyzers.Constants; using NUnit.Analyzers.Extensions; using NUnit.Framework; @@ -22,8 +23,8 @@ public sealed class IMethodSymbolExtensionsTestsGetParameterCounts public void Foo(int a1, int a2, int a3, string b1 = ""b1"", string b2 = ""b2"", params char[] c) { } } }"; - var method = await GetMethodSymbolAsync(testCode).ConfigureAwait(false); - var (requiredParameters, optionalParameters, paramsCount) = method.GetParameterCounts(); + var (method, _) = await GetMethodSymbolAsync(testCode).ConfigureAwait(false); + var (requiredParameters, optionalParameters, paramsCount) = method.GetParameterCounts(false, null); Assert.Multiple(() => { @@ -33,18 +34,45 @@ public void Foo(int a1, int a2, int a3, string b1 = ""b1"", string b2 = ""b2"", }); } - private static async Task GetMethodSymbolAsync(string code) + [Test] + public async Task GetParameterCountsWithCancellationToken([Values] bool hasCancelAfter) + { + var testCode = @" +using System.Threading; + +namespace NUnit.Analyzers.Tests.Targets.Extensions +{ + public sealed class IMethodSymbolExtensionsTestsGetParameterCounts + { + public void Foo(int a1, int a2, int a3, CancellationToken cancellationToken) { } + } +}"; + var (method, compilation) = await GetMethodSymbolAsync(testCode).ConfigureAwait(false); + INamedTypeSymbol? cancellationTokenType = compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfCancellationToken); + + var (requiredParameters, optionalParameters, paramsCount) = method.GetParameterCounts(hasCancelAfter, cancellationTokenType); + int adjustment = hasCancelAfter ? 0 : 1; + + Assert.Multiple(() => + { + Assert.That(requiredParameters, Is.EqualTo(3 + adjustment), nameof(requiredParameters)); + Assert.That(optionalParameters, Is.EqualTo(1 - adjustment), nameof(optionalParameters)); + Assert.That(paramsCount, Is.EqualTo(0), nameof(paramsCount)); + }); + } + + private static async Task<(IMethodSymbol MethodSymbol, Compilation Compilation)> GetMethodSymbolAsync(string code) { - var rootAndModel = await TestHelpers.GetRootAndModel(code).ConfigureAwait(false); + var rootCompilationAndModel = await TestHelpers.GetRootCompilationAndModel(code).ConfigureAwait(false); - MethodDeclarationSyntax methodDeclaration = rootAndModel.Node + MethodDeclarationSyntax methodDeclaration = rootCompilationAndModel.Node .DescendantNodes().OfType().Single() .DescendantNodes().OfType().Single(); - IMethodSymbol? methodSymbol = rootAndModel.Model.GetDeclaredSymbol(methodDeclaration); + IMethodSymbol? methodSymbol = rootCompilationAndModel.Model.GetDeclaredSymbol(methodDeclaration); Assert.That(methodSymbol, Is.Not.Null, $"Cannot find symbol for {methodDeclaration.Identifier}"); - return methodSymbol; + return (methodSymbol!, rootCompilationAndModel.Compilation); } } } diff --git a/src/nunit.analyzers.tests/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzerTests.cs b/src/nunit.analyzers.tests/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzerTests.cs index b046e8d0..a3611ce1 100644 --- a/src/nunit.analyzers.tests/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzerTests.cs +++ b/src/nunit.analyzers.tests/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzerTests.cs @@ -640,7 +640,7 @@ public void AnalyzeWhenNumberOfParametersOfTestIsNotEvidentFromTestSource() [TestFixture] public class AnalyzeWhenNumberOfParametersOfTestIsNotEvidentFromTestSource { - [Explicit(""The code is wrong, but it is too complext for the analyzer to detect this."")] + [Explicit(""The code is wrong, but it is too complex for the analyzer to detect this."")] [TestCaseSource(nameof(TestData))] public void ShortName(int n) { @@ -705,5 +705,63 @@ public void ShortName(int first, int second) RoslynAssert.Valid(analyzer, testCode); } + +#if NUNIT4 + [Test] + public void AnalyzeWhenNumberOfParametersMatchExcludingImplicitSuppliedCancellationToken() + { + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing(@" + [TestFixture] + public class AnalyzeWhenNumberOfParametersMatch + { + [TestCaseSource(nameof(TestData), new object[] { 1, 3, 5 })] + [CancelAfter(10)] + public void ShortName(int number, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + Assert.Ignore(""Cancelled""); + Assert.That(number, Is.GreaterThanOrEqualTo(0)); + } + + static IEnumerable TestData(int first, int second, int third) + { + yield return first; + yield return second; + yield return third; + } + }", additionalUsings: "using System.Collections.Generic;using System.Threading;"); + + RoslynAssert.Valid(analyzer, testCode); + } + + [Test] + public void AnalyzeWhenNumberOfParametersDoesNotMatchNoParametersExpectedNoImplicitSuppliedCancellationToken() + { + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing(@" + [TestFixture] + public class AnalyzeWhenNumberOfParametersDoesNotMatchNoParametersExpected + { + [TestCaseSource(↓nameof(TestData), new object[] { 1 })] + public void ShortName(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + Assert.Ignore(""Cancelled""); + } + + static IEnumerable TestData() + { + yield return 1; + yield return 2; + yield return 3; + } + }", additionalUsings: "using System.Collections.Generic;using System.Threading;"); + + var expectedDiagnostic = ExpectedDiagnostic + .Create(AnalyzerIdentifiers.TestCaseSourceMismatchInNumberOfParameters) + .WithMessage("The TestCaseSource provides '1' parameter(s), but the target method expects '0' parameter(s)"); + RoslynAssert.Diagnostics(analyzer, expectedDiagnostic, testCode); + } + +#endif } } diff --git a/src/nunit.analyzers.tests/TestCaseUsage/TestCaseUsageAnalyzerTests.cs b/src/nunit.analyzers.tests/TestCaseUsage/TestCaseUsageAnalyzerTests.cs index 3daab0fb..d41a5fbb 100644 --- a/src/nunit.analyzers.tests/TestCaseUsage/TestCaseUsageAnalyzerTests.cs +++ b/src/nunit.analyzers.tests/TestCaseUsage/TestCaseUsageAnalyzerTests.cs @@ -755,5 +755,60 @@ public void TestWithGenericParameter(T arg1) { } }"); RoslynAssert.Valid(this.analyzer, testCode); } + +#if NUNIT4 + [Test] + public void AnalyzeWhenTestMethodHasImplicitlySuppliedCancellationTokenParameter() + { + var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@" + [TestCase(100)] + [CancelAfter(50)] + public async Task InfiniteLoopWithCancelAfter(int delayInMs, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(delayInMs, cancellationToken).ConfigureAwait(false); + } + }", "using System.Threading;"); + + RoslynAssert.Valid(this.analyzer, testCode); + } + + [Test] + public void AnalyzeWhenTestMethodHasNoImplicitlySuppliedCancellationTokenParameter() + { + var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@" + [TestCase(100)] + [CancelAfter(50)] + public async Task InfiniteLoopWith50msCancelAfter(int delayInMs) + { + CancellationToken cancellationToken = TestContext.CurrentContext.CancellationToken; + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(delayInMs, cancellationToken).ConfigureAwait(false); + } + }", "using System.Threading;"); + + RoslynAssert.Valid(this.analyzer, testCode); + } + + [Test] + public void WhenTestMethodHasCancellationTokenParameter() + { + var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@" + [↓TestCase(100)] + public async Task InfiniteLoopWith50msCancelAfter(int delayInMs, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(delayInMs, cancellationToken).ConfigureAwait(false); + } + }", "using System.Threading;"); + + RoslynAssert.Diagnostics(this.analyzer, + ExpectedDiagnostic.Create(AnalyzerIdentifiers.TestCaseNotEnoughArgumentsUsage), + testCode); + } +#endif } } diff --git a/src/nunit.analyzers.tests/TestHelpers.cs b/src/nunit.analyzers.tests/TestHelpers.cs index 13b7bf31..82d83bc9 100644 --- a/src/nunit.analyzers.tests/TestHelpers.cs +++ b/src/nunit.analyzers.tests/TestHelpers.cs @@ -56,7 +56,7 @@ internal static Task NotSuppressed(DiagnosticAnalyzer analyzer, DiagnosticSuppre internal static Task Suppressed(DiagnosticAnalyzer analyzer, DiagnosticSuppressor suppressor, string code, Settings? settings = null) => SuppressedOrNot(analyzer, suppressor, code, true, settings); - internal static async Task<(SyntaxNode Node, SemanticModel Model)> GetRootAndModel(string code) + internal static (SyntaxTree Tree, Compilation Compilation) GetTreeAndCompilation(string code) { var tree = CSharpSyntaxTree.ParseText(code); @@ -65,10 +65,23 @@ internal static Task Suppressed(DiagnosticAnalyzer analyzer, DiagnosticSuppresso references: Settings.Default.MetadataReferences, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + return (tree, compilation); + } + + internal static async Task<(SyntaxNode Node, Compilation Compilation, SemanticModel Model)> GetRootCompilationAndModel(string code) + { + (SyntaxTree tree, Compilation compilation) = GetTreeAndCompilation(code); var model = compilation.GetSemanticModel(tree); var root = await tree.GetRootAsync().ConfigureAwait(false); - return (root, model); + return (root, compilation, model); + } + + internal static async Task<(SyntaxNode Node, SemanticModel Model)> GetRootAndModel(string code) + { + (SyntaxNode node, _, SemanticModel model) = await GetRootCompilationAndModel(code).ConfigureAwait(false); + + return (node, model); } } } diff --git a/src/nunit.analyzers.tests/TestMethodUsage/TestMethodUsageAnalyzerTests.cs b/src/nunit.analyzers.tests/TestMethodUsage/TestMethodUsageAnalyzerTests.cs index 02fabcd5..84cc758c 100644 --- a/src/nunit.analyzers.tests/TestMethodUsage/TestMethodUsageAnalyzerTests.cs +++ b/src/nunit.analyzers.tests/TestMethodUsage/TestMethodUsageAnalyzerTests.cs @@ -536,6 +536,79 @@ public void M(int p) RoslynAssert.Diagnostics(analyzer, expectedDiagnostic.WithMessage(message), testCode); } +#if NUNIT4 + [Test] + public void WhenTestMethodHasImplicitlySuppliedCancellationTokenParameter() + { + var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@" + [Test] + [CancelAfter(50)] + public async Task InfiniteLoopWith50msCancelAfter(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + }", "using System.Threading;"); + + RoslynAssert.Valid(analyzer, testCode); + } + + [Test] + public void AnalyzeWhenSimpleTestMethodHasParameterSuppliedByValuesAndCancelAfterAttributes() + { + var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@" + [Test] + [CancelAfter(10)] + public void M([Values(1, 2, 3)] int p, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + Assert.Ignore(""Cancelled""); + Assert.That(p, Is.EqualTo(42)); + }", "using System.Threading;"); + + RoslynAssert.Valid(analyzer, testCode); + } + + [Test] + public void WhenTestMethodHasNoImplicitlySuppliedCancellationTokenParameter() + { + var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@" + [Test] + [CancelAfter(50)] + public async Task InfiniteLoopWith50msCancelAfter() + { + CancellationToken cancellationToken = TestContext.CurrentContext.CancellationToken; + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + }", "using System.Threading;"); + + RoslynAssert.Valid(analyzer, testCode); + } + + [Test] + public void WhenTestMethodHasCancellationTokenParameter() + { + var expectedDiagnostic = ExpectedDiagnostic.Create( + AnalyzerIdentifiers.SimpleTestMethodHasParameters); + + var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@" + [↓Test] + public async Task InfiniteLoopWith50msCancelAfter(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + }", "using System.Threading;"); + + var message = "The test method has '1' parameter(s), but only '0' argument(s) are supplied by attributes"; + RoslynAssert.Diagnostics(analyzer, expectedDiagnostic.WithMessage(message), testCode); + } +#endif + [Test] public void AnalyzeWhenSimpleTestMethodHasParameterWithOutSuppliedValues() { diff --git a/src/nunit.analyzers.tests/nunit.analyzers.tests.csproj b/src/nunit.analyzers.tests/nunit.analyzers.tests.csproj index 05a3809e..52759a12 100644 --- a/src/nunit.analyzers.tests/nunit.analyzers.tests.csproj +++ b/src/nunit.analyzers.tests/nunit.analyzers.tests.csproj @@ -2,7 +2,7 @@ NUnit.Analyzers.Tests net6.0;net462 - 3 + 4 diff --git a/src/nunit.analyzers/Constants/NUnitFrameworkConstants.cs b/src/nunit.analyzers/Constants/NUnitFrameworkConstants.cs index 9e7999fe..b029f4cd 100644 --- a/src/nunit.analyzers/Constants/NUnitFrameworkConstants.cs +++ b/src/nunit.analyzers/Constants/NUnitFrameworkConstants.cs @@ -149,6 +149,9 @@ public static class NUnitFrameworkConstants public const string FullNameOfFixtureLifeCycleAttribute = "NUnit.Framework.FixtureLifeCycleAttribute"; public const string FullNameOfLifeCycle = "NUnit.Framework.LifeCycle"; + public const string FullNameOfCancelAfterAttribute = "NUnit.Framework.CancelAfterAttribute"; + public const string FullNameOfCancellationToken = "System.Threading.CancellationToken"; + public const string NameOfConstraint = "Constraint"; public const string FullNameOfSameAsConstraint = "NUnit.Framework.Constraints.SameAsConstraint"; @@ -180,6 +183,8 @@ public static class NUnitFrameworkConstants public const string NameOfSetUpAttribute = "SetUpAttribute"; public const string NameOfTearDownAttribute = "TearDownAttribute"; + public const string NameOfCancelAfterAttribute = "CancelAfterAttribute"; + public const string NameOfExpectedResult = "ExpectedResult"; public const string NameOfActualParameter = "actual"; diff --git a/src/nunit.analyzers/Extensions/IMethodSymbolExtensions.cs b/src/nunit.analyzers/Extensions/IMethodSymbolExtensions.cs index c49c4755..6d35c273 100644 --- a/src/nunit.analyzers/Extensions/IMethodSymbolExtensions.cs +++ b/src/nunit.analyzers/Extensions/IMethodSymbolExtensions.cs @@ -15,7 +15,9 @@ internal static class IMethodSymbolExtensions /// and the last is the count. /// internal static (uint requiredParameters, uint optionalParameters, uint paramsCount) GetParameterCounts( - this IMethodSymbol @this) + this IMethodSymbol @this, + bool hasCancelAfterAttribute, + INamedTypeSymbol? cancellationTokenType) { var parameters = @this.Parameters; @@ -39,6 +41,15 @@ internal static (uint requiredParameters, uint optionalParameters, uint paramsCo } } + var hasCancellationToken = parameters.Length > 0 && + SymbolEqualityComparer.Default.Equals(parameters[parameters.Length - 1].Type, cancellationTokenType); + if (hasCancelAfterAttribute && hasCancellationToken) + { + // This parameter is optional, it not specified it will be supplied by NUnit. + optionalParameters++; + requiredParameters--; + } + return (requiredParameters, optionalParameters, paramsParameters); } diff --git a/src/nunit.analyzers/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzer.cs b/src/nunit.analyzers/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzer.cs index 6a0b554d..a76d54ef 100644 --- a/src/nunit.analyzers/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzer.cs +++ b/src/nunit.analyzers/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzer.cs @@ -121,10 +121,17 @@ private static void AnalyzeCompilationStart(CompilationStartAnalysisContext cont return; } - context.RegisterSyntaxNodeAction(syntaxContext => AnalyzeAttribute(syntaxContext, testCaseSourceAttribute), SyntaxKind.Attribute); + INamedTypeSymbol? cancelAfterType = context.Compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfCancelAfterAttribute); + INamedTypeSymbol? cancellationTokenType = context.Compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfCancellationToken); + + context.RegisterSyntaxNodeAction(syntaxContext => AnalyzeAttribute(syntaxContext, testCaseSourceAttribute, cancelAfterType, cancellationTokenType), SyntaxKind.Attribute); } - private static void AnalyzeAttribute(SyntaxNodeAnalysisContext context, INamedTypeSymbol testCaseSourceAttribute) + private static void AnalyzeAttribute( + SyntaxNodeAnalysisContext context, + INamedTypeSymbol testCaseSourceAttribute, + INamedTypeSymbol? cancelAfterType, + INamedTypeSymbol? cancellationTokenType) { var attributeInfo = SourceHelpers.GetSourceAttributeInformation( context, @@ -238,7 +245,9 @@ private static void AnalyzeAttribute(SyntaxNodeAnalysisContext context, INamedTy IMethodSymbol? testMethod = context.SemanticModel.GetDeclaredSymbol(testMethodDeclaration); if (testMethod is not null) { - var (methodRequiredParameters, methodOptionalParameters, methodParamsParameters) = testMethod.GetParameterCounts(); + var hasCancelAfterAttribute = testMethod.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, cancelAfterType)); + + var (methodRequiredParameters, methodOptionalParameters, methodParamsParameters) = testMethod.GetParameterCounts(true, cancellationTokenType); if (elementType.SpecialType != SpecialType.System_String && (elementType.SpecialType == SpecialType.System_Object || elementType.IsIEnumerable(out _) || IsOrDerivesFrom(elementType, context.SemanticModel.Compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfTypeTestCaseParameters)))) @@ -258,7 +267,7 @@ private static void AnalyzeAttribute(SyntaxNodeAnalysisContext context, INamedTy } else { - if (methodRequiredParameters + methodOptionalParameters != 1) + if (methodRequiredParameters + methodOptionalParameters != (hasCancelAfterAttribute ? 2 : 1)) { context.ReportDiagnostic(Diagnostic.Create( mismatchInNumberOfTestMethodParameters, diff --git a/src/nunit.analyzers/TestCaseUsage/TestCaseUsageAnalyzer.cs b/src/nunit.analyzers/TestCaseUsage/TestCaseUsageAnalyzer.cs index 9fbb34bf..80cf63c6 100644 --- a/src/nunit.analyzers/TestCaseUsage/TestCaseUsageAnalyzer.cs +++ b/src/nunit.analyzers/TestCaseUsage/TestCaseUsageAnalyzer.cs @@ -52,17 +52,26 @@ private static void AnalyzeCompilationStart(CompilationStartAnalysisContext cont if (testCaseType is null) return; - context.RegisterSymbolAction(symbolContext => AnalyzeMethod(symbolContext, testCaseType), SymbolKind.Method); + INamedTypeSymbol? cancelAfterType = context.Compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfCancelAfterAttribute); + INamedTypeSymbol? cancellationTokenType = context.Compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfCancellationToken); + + context.RegisterSymbolAction(symbolContext => AnalyzeMethod(symbolContext, testCaseType, cancelAfterType, cancellationTokenType), SymbolKind.Method); } - private static void AnalyzeMethod(SymbolAnalysisContext context, INamedTypeSymbol testCaseType) + private static void AnalyzeMethod( + SymbolAnalysisContext context, + INamedTypeSymbol testCaseType, + INamedTypeSymbol? cancelAfterType, + INamedTypeSymbol? cancellationTokenType) { var methodSymbol = (IMethodSymbol)context.Symbol; - var attributes = methodSymbol.GetAttributes(); - if (attributes.Length == 0) + var methodAttributes = methodSymbol.GetAttributes(); + if (methodAttributes.Length == 0) return; + var hasCancelAfterAttribute = methodAttributes.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, cancelAfterType)); + var testCaseAttributes = methodSymbol.GetAttributes() .Where(a => a.ApplicationSyntaxReference is not null && SymbolEqualityComparer.Default.Equals(a.AttributeClass, testCaseType)); @@ -72,7 +81,7 @@ private static void AnalyzeMethod(SymbolAnalysisContext context, INamedTypeSymbo context.CancellationToken.ThrowIfCancellationRequested(); var (methodRequiredParameters, methodOptionalParameters, methodParamsParameters) = - methodSymbol.GetParameterCounts(); + methodSymbol.GetParameterCounts(hasCancelAfterAttribute, cancellationTokenType); var attributePositionalArguments = attribute.ConstructorArguments.AdjustArguments(); diff --git a/src/nunit.analyzers/TestMethodUsage/TestMethodUsageAnalyzer.cs b/src/nunit.analyzers/TestMethodUsage/TestMethodUsageAnalyzer.cs index 18c4ce54..5f1c610d 100644 --- a/src/nunit.analyzers/TestMethodUsage/TestMethodUsageAnalyzer.cs +++ b/src/nunit.analyzers/TestMethodUsage/TestMethodUsageAnalyzer.cs @@ -89,50 +89,64 @@ private static void AnalyzeCompilationStart(CompilationStartAnalysisContext cont if (testCaseType is null || testType is null) return; - context.RegisterSymbolAction(symbolContext => AnalyzeMethod(symbolContext, testCaseType, testType), SymbolKind.Method); + INamedTypeSymbol? cancelAfterType = context.Compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfCancelAfterAttribute); + INamedTypeSymbol? cancellationTokenType = context.Compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfCancellationToken); + + context.RegisterSymbolAction(symbolContext => AnalyzeMethod(symbolContext, testCaseType, testType, cancelAfterType, cancellationTokenType), SymbolKind.Method); } - private static void AnalyzeMethod(SymbolAnalysisContext context, INamedTypeSymbol testCaseType, INamedTypeSymbol testType) + private static void AnalyzeMethod( + SymbolAnalysisContext context, + INamedTypeSymbol testCaseType, + INamedTypeSymbol testType, + INamedTypeSymbol? cancelAfterType, + INamedTypeSymbol? cancellationTokenType) { var methodSymbol = (IMethodSymbol)context.Symbol; var methodAttributes = methodSymbol.GetAttributes(); - foreach (var attribute in methodAttributes) + // Check Expected Result for TestCases (should this be moved to TestCaseUsageAnalyzer) + foreach (var testCaseAttribute in methodAttributes.Where(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, testCaseType))) { - if (attribute.AttributeClass is null) - continue; + context.CancellationToken.ThrowIfCancellationRequested(); + + AnalyzeExpectedResult(context, testCaseAttribute, methodSymbol); + } - var isTestCaseAttribute = SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, testCaseType); - var isTestAttribute = SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, testType); + var hasITestBuilderAttribute = HasITestBuilderAttribute(context.Compilation, methodAttributes); - if (isTestCaseAttribute - || (isTestAttribute && !HasITestBuilderAttribute(context.Compilation, methodAttributes))) + if (!hasITestBuilderAttribute) + { + var testAttribute = methodAttributes.SingleOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, testType)); + var hasCancelAfterAttribute = methodAttributes.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, cancelAfterType)); + + if (testAttribute is not null) { context.CancellationToken.ThrowIfCancellationRequested(); - AnalyzeExpectedResult(context, attribute, methodSymbol); + AnalyzeExpectedResult(context, testAttribute, methodSymbol); } - var isSimpleTestBulderAttribute = attribute.DerivesFromISimpleTestBuilder(context.Compilation); + var simpleTestBuilderAttribute = methodAttributes.FirstOrDefault(a => a.DerivesFromISimpleTestBuilder(context.Compilation)); - if (isSimpleTestBulderAttribute) + if (simpleTestBuilderAttribute is not null) { var parameters = methodSymbol.Parameters; var testMethodParameters = parameters.Length; - var hasITestBuilderAttribute = HasITestBuilderAttribute(context.Compilation, methodAttributes); var parametersMarkedWithIParameterDataSourceAttribute = parameters.Count(p => HasIParameterDataSourceAttribute(context.Compilation, p.GetAttributes())); - - if (testMethodParameters > 0 && - !hasITestBuilderAttribute && - parametersMarkedWithIParameterDataSourceAttribute < testMethodParameters) + var hasCancellationToken = parameters.Length > 0 && + SymbolEqualityComparer.Default.Equals(parameters[parameters.Length - 1].Type, cancellationTokenType); + int implicitParameters = hasCancelAfterAttribute && hasCancellationToken ? 1 : 0; + if (testMethodParameters > implicitParameters && + parametersMarkedWithIParameterDataSourceAttribute + implicitParameters < testMethodParameters) { context.ReportDiagnostic(Diagnostic.Create( simpleTestHasParameters, - attribute.ApplicationSyntaxReference.GetLocation(), + simpleTestBuilderAttribute.ApplicationSyntaxReference.GetLocation(), testMethodParameters, - parametersMarkedWithIParameterDataSourceAttribute)); + parametersMarkedWithIParameterDataSourceAttribute + implicitParameters)); } } } From f2d269cfd7b1d471ff14ee2516203c34d4523313 Mon Sep 17 00:00:00 2001 From: Manfred Brands Date: Sun, 21 Jan 2024 12:12:23 +0100 Subject: [PATCH 2/5] Fix tests for NUnit 3.x build --- .../Constants/CancelAfterAttribute.cs | 13 +++++++++++++ .../Constants/NUnitFrameworkConstantsTests.cs | 4 ---- 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 src/nunit.analyzers.tests/Constants/CancelAfterAttribute.cs diff --git a/src/nunit.analyzers.tests/Constants/CancelAfterAttribute.cs b/src/nunit.analyzers.tests/Constants/CancelAfterAttribute.cs new file mode 100644 index 00000000..cd050390 --- /dev/null +++ b/src/nunit.analyzers.tests/Constants/CancelAfterAttribute.cs @@ -0,0 +1,13 @@ +#if !NUNIT4 + +using System; + +namespace NUnit.Framework +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + internal sealed class CancelAfterAttribute : Attribute + { + } +} + +#endif diff --git a/src/nunit.analyzers.tests/Constants/NUnitFrameworkConstantsTests.cs b/src/nunit.analyzers.tests/Constants/NUnitFrameworkConstantsTests.cs index 83d22e8b..f6d84f7c 100644 --- a/src/nunit.analyzers.tests/Constants/NUnitFrameworkConstantsTests.cs +++ b/src/nunit.analyzers.tests/Constants/NUnitFrameworkConstantsTests.cs @@ -165,9 +165,7 @@ public sealed class NUnitFrameworkConstantsTests (nameof(NUnitFrameworkConstants.NameOfSetUpAttribute), nameof(SetUpAttribute)), (nameof(NUnitFrameworkConstants.NameOfTearDownAttribute), nameof(TearDownAttribute)), -#if NUNIT4 (nameof(NUnitFrameworkConstants.NameOfCancelAfterAttribute), nameof(CancelAfterAttribute)), -#endif (nameof(NUnitFrameworkConstants.NameOfExpectedResult), nameof(TestAttribute.ExpectedResult)), @@ -206,10 +204,8 @@ public sealed class NUnitFrameworkConstantsTests (nameof(NUnitFrameworkConstants.FullNameOfFixtureLifeCycleAttribute), typeof(FixtureLifeCycleAttribute)), (nameof(NUnitFrameworkConstants.FullNameOfLifeCycle), typeof(LifeCycle)), -#if NUNIT4 (nameof(NUnitFrameworkConstants.FullNameOfCancelAfterAttribute), typeof(CancelAfterAttribute)), (nameof(NUnitFrameworkConstants.FullNameOfCancellationToken), typeof(CancellationToken)), -#endif (nameof(NUnitFrameworkConstants.FullNameOfSameAsConstraint), typeof(SameAsConstraint)), (nameof(NUnitFrameworkConstants.FullNameOfSomeItemsConstraint), typeof(SomeItemsConstraint)), From 053e2a1658db558f75e84833eaa74bf67eb7b917 Mon Sep 17 00:00:00 2001 From: Manfred Brands Date: Sun, 21 Jan 2024 12:25:57 +0100 Subject: [PATCH 3/5] Update build.cake to test against both NUnit 3 and 4 --- .config/dotnet-tools.json | 4 ++-- .gitignore | 4 ++-- build.cake | 15 +++++++++++++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index b6cadaf6..678c0add 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,10 +3,10 @@ "isRoot": true, "tools": { "cake.tool": { - "version": "2.1.0", + "version": "3.2.0", "commands": [ "dotnet-cake" ] } } -} \ No newline at end of file +} diff --git a/.gitignore b/.gitignore index c0dabd86..40dc2c8a 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,7 @@ tools/ # NUNIT *.VisualState.xml -TestResult.xml +TestResult*.xml # Build Results of an ATL Project [Dd]ebugPS/ @@ -288,4 +288,4 @@ CommentRemover.ConsoleApplication Output/ CommentRemover.Task Output/ # Generated Assembly info -AssemblyInfo.Generated.cs \ No newline at end of file +AssemblyInfo.Generated.cs diff --git a/build.cake b/build.cake index 2e2e7689..03c11e3b 100644 --- a/build.cake +++ b/build.cake @@ -148,18 +148,29 @@ Task("Test") .IsDependentOn("Build") .Does(() => { + Information("Testing against NUnit 3.xx"); DotNetTest(TEST_PROJECT, new DotNetTestSettings { Configuration = configuration, Loggers = new string[] { "trx" }, - VSTestReportPath = "TestResult.xml", + VSTestReportPath = "TestResult-NUnit3.xml", + MSBuildSettings = new DotNetMSBuildSettings().WithProperty("NUnitVersion", "3") + }); + Information("Testing against NUnit 4.xx"); + DotNetTest(TEST_PROJECT, new DotNetTestSettings + { + Configuration = configuration, + Loggers = new string[] { "trx" }, + VSTestReportPath = "TestResult-NUnit4.xml", + MSBuildSettings = new DotNetMSBuildSettings().WithProperty("NUnitVersion", "4") }); }) .Finally(() => { if (AppVeyor.IsRunningOnAppVeyor) { - AppVeyor.UploadTestResults("TestResult.xml", AppVeyorTestResultsType.MSTest); + AppVeyor.UploadTestResults("TestResult-NUnit3.xml", AppVeyorTestResultsType.MSTest); + AppVeyor.UploadTestResults("TestResult-NUnit4.xml", AppVeyorTestResultsType.MSTest); } }); From bb0263c274d30f360dfd2bcb3f1d76b6fa2c432c Mon Sep 17 00:00:00 2001 From: Manfred Brands Date: Mon, 22 Jan 2024 11:46:52 +0100 Subject: [PATCH 4/5] Code Review changes --- src/nunit.analyzers/Extensions/IMethodSymbolExtensions.cs | 8 +++++++- .../TestCaseSourceUsesStringAnalyzer.cs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/nunit.analyzers/Extensions/IMethodSymbolExtensions.cs b/src/nunit.analyzers/Extensions/IMethodSymbolExtensions.cs index 6d35c273..ec0198d9 100644 --- a/src/nunit.analyzers/Extensions/IMethodSymbolExtensions.cs +++ b/src/nunit.analyzers/Extensions/IMethodSymbolExtensions.cs @@ -10,10 +10,16 @@ internal static class IMethodSymbolExtensions /// Gets the parameters into required, optional, and params counts. /// /// The reference to get parameters from. + /// if has a CancelAfterAttribute. + /// The symbol reference for . /// /// The first count is the required parameters, the second is the optional count, /// and the last is the count. /// + /// + /// When the CancelAfterAttribute is in play and the last parameter has type + /// this parameter is optional, if not supplied by the user it will be supplied by NUnit. + /// internal static (uint requiredParameters, uint optionalParameters, uint paramsCount) GetParameterCounts( this IMethodSymbol @this, bool hasCancelAfterAttribute, @@ -45,7 +51,7 @@ internal static (uint requiredParameters, uint optionalParameters, uint paramsCo SymbolEqualityComparer.Default.Equals(parameters[parameters.Length - 1].Type, cancellationTokenType); if (hasCancelAfterAttribute && hasCancellationToken) { - // This parameter is optional, it not specified it will be supplied by NUnit. + // This parameter is optional, if not specified it will be supplied by NUnit. optionalParameters++; requiredParameters--; } diff --git a/src/nunit.analyzers/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzer.cs b/src/nunit.analyzers/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzer.cs index a76d54ef..40e23b7f 100644 --- a/src/nunit.analyzers/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzer.cs +++ b/src/nunit.analyzers/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzer.cs @@ -247,7 +247,7 @@ private static void AnalyzeAttribute( { var hasCancelAfterAttribute = testMethod.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, cancelAfterType)); - var (methodRequiredParameters, methodOptionalParameters, methodParamsParameters) = testMethod.GetParameterCounts(true, cancellationTokenType); + var (methodRequiredParameters, methodOptionalParameters, methodParamsParameters) = testMethod.GetParameterCounts(hasCancelAfterAttribute, cancellationTokenType); if (elementType.SpecialType != SpecialType.System_String && (elementType.SpecialType == SpecialType.System_Object || elementType.IsIEnumerable(out _) || IsOrDerivesFrom(elementType, context.SemanticModel.Compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfTypeTestCaseParameters)))) From 70dd521e7488daa764a74acdad664b378a3c7573 Mon Sep 17 00:00:00 2001 From: Manfred Brands Date: Mon, 22 Jan 2024 12:52:55 +0100 Subject: [PATCH 5/5] Add support for CancelAfter on TestFixture class --- .../TestCaseSourceUsesStringAnalyzerTests.cs | 29 +++++++++++++++- .../TestCaseUsageAnalyzerTests.cs | 33 +++++++++++++++---- .../TestMethodUsageAnalyzerTests.cs | 23 ++++++++++++- .../TestCaseSourceUsesStringAnalyzer.cs | 3 +- .../TestCaseUsage/TestCaseUsageAnalyzer.cs | 5 +-- .../TestMethodUsageAnalyzer.cs | 3 +- 6 files changed, 84 insertions(+), 12 deletions(-) diff --git a/src/nunit.analyzers.tests/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzerTests.cs b/src/nunit.analyzers.tests/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzerTests.cs index a3611ce1..4a5dee71 100644 --- a/src/nunit.analyzers.tests/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzerTests.cs +++ b/src/nunit.analyzers.tests/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzerTests.cs @@ -708,7 +708,7 @@ public void ShortName(int first, int second) #if NUNIT4 [Test] - public void AnalyzeWhenNumberOfParametersMatchExcludingImplicitSuppliedCancellationToken() + public void AnalyzeWhenNumberOfParametersMatchExcludingImplicitSuppliedCancellationTokenDueToCancelAfterOnMethod() { var testCode = TestUtility.WrapClassInNamespaceAndAddUsing(@" [TestFixture] @@ -734,6 +734,33 @@ static IEnumerable TestData(int first, int second, int third) RoslynAssert.Valid(analyzer, testCode); } + [Test] + public void AnalyzeWhenNumberOfParametersMatchExcludingImplicitSuppliedCancellationTokenDueToCancelAfterOnClass() + { + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing(@" + [TestFixture] + [CancelAfter(100)] + public class AnalyzeWhenNumberOfParametersMatch + { + [TestCaseSource(nameof(TestData), new object[] { 1, 3, 5 })] + public void ShortName(int number, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + Assert.Ignore(""Cancelled""); + Assert.That(number, Is.GreaterThanOrEqualTo(0)); + } + + static IEnumerable TestData(int first, int second, int third) + { + yield return first; + yield return second; + yield return third; + } + }", additionalUsings: "using System.Collections.Generic;using System.Threading;"); + + RoslynAssert.Valid(analyzer, testCode); + } + [Test] public void AnalyzeWhenNumberOfParametersDoesNotMatchNoParametersExpectedNoImplicitSuppliedCancellationToken() { diff --git a/src/nunit.analyzers.tests/TestCaseUsage/TestCaseUsageAnalyzerTests.cs b/src/nunit.analyzers.tests/TestCaseUsage/TestCaseUsageAnalyzerTests.cs index d41a5fbb..c8dc554a 100644 --- a/src/nunit.analyzers.tests/TestCaseUsage/TestCaseUsageAnalyzerTests.cs +++ b/src/nunit.analyzers.tests/TestCaseUsage/TestCaseUsageAnalyzerTests.cs @@ -758,18 +758,39 @@ public void TestWithGenericParameter(T arg1) { } #if NUNIT4 [Test] - public void AnalyzeWhenTestMethodHasImplicitlySuppliedCancellationTokenParameter() + public void AnalyzeWhenTestMethodHasImplicitlySuppliedCancellationTokenParameterDueToCancelAfterOnMethod() { var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@" - [TestCase(100)] + [TestCase(100)] + [CancelAfter(50)] + public async Task InfiniteLoopWithCancelAfter(int delayInMs, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(delayInMs, cancellationToken).ConfigureAwait(false); + } + }", "using System.Threading;"); + + RoslynAssert.Valid(this.analyzer, testCode); + } + + [Test] + public void AnalyzeWhenTestMethodHasImplicitlySuppliedCancellationTokenParameterDueToCancelAfterOnClass() + { + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing(@" + [TestFixture] [CancelAfter(50)] - public async Task InfiniteLoopWithCancelAfter(int delayInMs, CancellationToken cancellationToken) + public class TestClass { - while (!cancellationToken.IsCancellationRequested) + [TestCase(100)] + public async Task InfiniteLoopWithCancelAfter(int delayInMs, CancellationToken cancellationToken) { - await Task.Delay(delayInMs, cancellationToken).ConfigureAwait(false); + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(delayInMs, cancellationToken).ConfigureAwait(false); + } } - }", "using System.Threading;"); + }", "using System.Threading;"); RoslynAssert.Valid(this.analyzer, testCode); } diff --git a/src/nunit.analyzers.tests/TestMethodUsage/TestMethodUsageAnalyzerTests.cs b/src/nunit.analyzers.tests/TestMethodUsage/TestMethodUsageAnalyzerTests.cs index 84cc758c..78c30402 100644 --- a/src/nunit.analyzers.tests/TestMethodUsage/TestMethodUsageAnalyzerTests.cs +++ b/src/nunit.analyzers.tests/TestMethodUsage/TestMethodUsageAnalyzerTests.cs @@ -538,7 +538,7 @@ public void M(int p) #if NUNIT4 [Test] - public void WhenTestMethodHasImplicitlySuppliedCancellationTokenParameter() + public void WhenTestMethodHasImplicitlySuppliedCancellationTokenParameterDueToCancelAfterOnMethod() { var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@" [Test] @@ -554,6 +554,27 @@ public async Task InfiniteLoopWith50msCancelAfter(CancellationToken cancellation RoslynAssert.Valid(analyzer, testCode); } + [Test] + public void WhenTestMethodHasImplicitlySuppliedCancellationTokenParameterDueToCancelAfterOnClass() + { + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing(@" + [TestFixture] + [CancelAfter(50)] + public class TestClass + { + [Test] + public async Task InfiniteLoopWith50msCancelAfter(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + } + }", "using System.Threading;"); + + RoslynAssert.Valid(analyzer, testCode); + } + [Test] public void AnalyzeWhenSimpleTestMethodHasParameterSuppliedByValuesAndCancelAfterAttributes() { diff --git a/src/nunit.analyzers/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzer.cs b/src/nunit.analyzers/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzer.cs index 40e23b7f..8a3cd697 100644 --- a/src/nunit.analyzers/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzer.cs +++ b/src/nunit.analyzers/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzer.cs @@ -245,7 +245,8 @@ private static void AnalyzeAttribute( IMethodSymbol? testMethod = context.SemanticModel.GetDeclaredSymbol(testMethodDeclaration); if (testMethod is not null) { - var hasCancelAfterAttribute = testMethod.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, cancelAfterType)); + var hasCancelAfterAttribute = testMethod.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, cancelAfterType)) || + testMethod.ContainingType.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, cancelAfterType)); var (methodRequiredParameters, methodOptionalParameters, methodParamsParameters) = testMethod.GetParameterCounts(hasCancelAfterAttribute, cancellationTokenType); diff --git a/src/nunit.analyzers/TestCaseUsage/TestCaseUsageAnalyzer.cs b/src/nunit.analyzers/TestCaseUsage/TestCaseUsageAnalyzer.cs index 80cf63c6..4d8d690b 100644 --- a/src/nunit.analyzers/TestCaseUsage/TestCaseUsageAnalyzer.cs +++ b/src/nunit.analyzers/TestCaseUsage/TestCaseUsageAnalyzer.cs @@ -70,9 +70,10 @@ private static void AnalyzeMethod( if (methodAttributes.Length == 0) return; - var hasCancelAfterAttribute = methodAttributes.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, cancelAfterType)); + var hasCancelAfterAttribute = methodAttributes.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, cancelAfterType)) || + methodSymbol.ContainingType.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, cancelAfterType)); - var testCaseAttributes = methodSymbol.GetAttributes() + var testCaseAttributes = methodAttributes .Where(a => a.ApplicationSyntaxReference is not null && SymbolEqualityComparer.Default.Equals(a.AttributeClass, testCaseType)); diff --git a/src/nunit.analyzers/TestMethodUsage/TestMethodUsageAnalyzer.cs b/src/nunit.analyzers/TestMethodUsage/TestMethodUsageAnalyzer.cs index 5f1c610d..281fe3da 100644 --- a/src/nunit.analyzers/TestMethodUsage/TestMethodUsageAnalyzer.cs +++ b/src/nunit.analyzers/TestMethodUsage/TestMethodUsageAnalyzer.cs @@ -119,7 +119,8 @@ private static void AnalyzeMethod( if (!hasITestBuilderAttribute) { var testAttribute = methodAttributes.SingleOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, testType)); - var hasCancelAfterAttribute = methodAttributes.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, cancelAfterType)); + var hasCancelAfterAttribute = methodAttributes.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, cancelAfterType)) || + methodSymbol.ContainingType.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, cancelAfterType)); if (testAttribute is not null) {