Skip to content

Commit

Permalink
Merge pull request #678 from manfred-brands/issue663_CancellationToken
Browse files Browse the repository at this point in the history
Update Test method parameter checks to account for CancellationToken
  • Loading branch information
manfred-brands authored Jan 27, 2024
2 parents 01364d4 + 70dd521 commit b0655fe
Show file tree
Hide file tree
Showing 16 changed files with 430 additions and 47 deletions.
4 changes: 2 additions & 2 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
"isRoot": true,
"tools": {
"cake.tool": {
"version": "2.1.0",
"version": "3.2.0",
"commands": [
"dotnet-cake"
]
}
}
}
}
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ tools/

# NUNIT
*.VisualState.xml
TestResult.xml
TestResult*.xml

# Build Results of an ATL Project
[Dd]ebugPS/
Expand Down Expand Up @@ -288,4 +288,4 @@ CommentRemover.ConsoleApplication Output/
CommentRemover.Task Output/

# Generated Assembly info
AssemblyInfo.Generated.cs
AssemblyInfo.Generated.cs
15 changes: 13 additions & 2 deletions build.cake
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});

Expand Down
13 changes: 13 additions & 0 deletions src/nunit.analyzers.tests/Constants/CancelAfterAttribute.cs
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -164,6 +165,8 @@ public sealed class NUnitFrameworkConstantsTests
(nameof(NUnitFrameworkConstants.NameOfSetUpAttribute), nameof(SetUpAttribute)),
(nameof(NUnitFrameworkConstants.NameOfTearDownAttribute), nameof(TearDownAttribute)),

(nameof(NUnitFrameworkConstants.NameOfCancelAfterAttribute), nameof(CancelAfterAttribute)),

(nameof(NUnitFrameworkConstants.NameOfExpectedResult), nameof(TestAttribute.ExpectedResult)),

(nameof(NUnitFrameworkConstants.NameOfConstraintExpressionAnd), nameof(EqualConstraint.And)),
Expand Down Expand Up @@ -201,6 +204,9 @@ public sealed class NUnitFrameworkConstantsTests
(nameof(NUnitFrameworkConstants.FullNameOfFixtureLifeCycleAttribute), typeof(FixtureLifeCycleAttribute)),
(nameof(NUnitFrameworkConstants.FullNameOfLifeCycle), typeof(LifeCycle)),

(nameof(NUnitFrameworkConstants.FullNameOfCancelAfterAttribute), typeof(CancelAfterAttribute)),
(nameof(NUnitFrameworkConstants.FullNameOfCancellationToken), typeof(CancellationToken)),

(nameof(NUnitFrameworkConstants.FullNameOfSameAsConstraint), typeof(SameAsConstraint)),
(nameof(NUnitFrameworkConstants.FullNameOfSomeItemsConstraint), typeof(SomeItemsConstraint)),
(nameof(NUnitFrameworkConstants.FullNameOfEqualToConstraint), typeof(EqualConstraint)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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(() =>
{
Expand All @@ -33,18 +34,45 @@ public void Foo(int a1, int a2, int a3, string b1 = ""b1"", string b2 = ""b2"",
});
}

private static async Task<IMethodSymbol> 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<TypeDeclarationSyntax>().Single()
.DescendantNodes().OfType<MethodDeclarationSyntax>().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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -705,5 +705,90 @@ public void ShortName(int first, int second)

RoslynAssert.Valid(analyzer, testCode);
}

#if NUNIT4
[Test]
public void AnalyzeWhenNumberOfParametersMatchExcludingImplicitSuppliedCancellationTokenDueToCancelAfterOnMethod()
{
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<int> 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 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<int> 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<int> 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -755,5 +755,81 @@ public void TestWithGenericParameter<T>(T arg1) { }
}");
RoslynAssert.Valid(this.analyzer, testCode);
}

#if NUNIT4
[Test]
public void AnalyzeWhenTestMethodHasImplicitlySuppliedCancellationTokenParameterDueToCancelAfterOnMethod()
{
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 AnalyzeWhenTestMethodHasImplicitlySuppliedCancellationTokenParameterDueToCancelAfterOnClass()
{
var testCode = TestUtility.WrapClassInNamespaceAndAddUsing(@"
[TestFixture]
[CancelAfter(50)]
public class TestClass
{
[TestCase(100)]
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
}
}
17 changes: 15 additions & 2 deletions src/nunit.analyzers.tests/TestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
}
}
}
Loading

0 comments on commit b0655fe

Please sign in to comment.