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

Update Test method parameter checks to account for CancellationToken #678

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
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
manfred-brands marked this conversation as resolved.
Show resolved Hide resolved
[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
manfred-brands marked this conversation as resolved.
Show resolved Hide resolved
[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