Skip to content

Commit

Permalink
Update Test method parameter checks to account for CancellationToken
Browse files Browse the repository at this point in the history
  • Loading branch information
manfred-brands committed Jan 20, 2024
1 parent 01364d4 commit 88aeaa8
Show file tree
Hide file tree
Showing 12 changed files with 325 additions and 40 deletions.
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,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)),
Expand Down Expand Up @@ -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)),
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,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<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,60 @@ public void TestWithGenericParameter<T>(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
}
}
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
2 changes: 1 addition & 1 deletion src/nunit.analyzers.tests/nunit.analyzers.tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<RootNamespace>NUnit.Analyzers.Tests</RootNamespace>
<TargetFrameworks>net6.0;net462</TargetFrameworks>
<NUnitVersion Condition="'$(NUnitVersion)'==''">3</NUnitVersion>
<NUnitVersion Condition="'$(NUnitVersion)'==''">4</NUnitVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(NUnitVersion)' == '4'">
Expand Down
5 changes: 5 additions & 0 deletions src/nunit.analyzers/Constants/NUnitFrameworkConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down
Loading

0 comments on commit 88aeaa8

Please sign in to comment.