diff --git a/src/nunit.analyzers.tests/ParallelizableUsage/ParallelizableUsageAnalyzerTests.cs b/src/nunit.analyzers.tests/ParallelizableUsage/ParallelizableUsageAnalyzerTests.cs new file mode 100644 index 00000000..78d226f2 --- /dev/null +++ b/src/nunit.analyzers.tests/ParallelizableUsage/ParallelizableUsageAnalyzerTests.cs @@ -0,0 +1,243 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Gu.Roslyn.Asserts; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using NUnit.Analyzers.Constants; +using NUnit.Analyzers.ParallelizableUsage; +using NUnit.Framework; + +namespace NUnit.Analyzers.Tests.ParallelizableUsage +{ + [TestFixture] + public sealed class ParallelizableUsageAnalyzerTests + { + private readonly DiagnosticAnalyzer analyzer = new ParallelizableUsageAnalyzer(); + + [Test] + public void VerifySupportedDiagnostics() + { + var diagnostics = analyzer.SupportedDiagnostics; + + var expectedIdentifiers = new List + { + AnalyzerIdentifiers.ParallelScopeSelfNoEffectOnAssemblyUsage, + AnalyzerIdentifiers.ParallelScopeChildrenOnNonParameterizedTestMethodUsage, + AnalyzerIdentifiers.ParallelScopeFixturesOnTestMethodUsage + }; + CollectionAssert.AreEquivalent(expectedIdentifiers, diagnostics.Select(d => d.Id)); + + foreach (var diagnostic in diagnostics) + { + Assert.That(diagnostic.Title.ToString(), Is.EqualTo(ParallelizableUsageAnalyzerConstants.Title), + $"{diagnostic.Id} : {nameof(DiagnosticDescriptor.Title)}"); + Assert.That(diagnostic.Category, Is.EqualTo(Categories.Usage), + $"{diagnostic.Id} : {nameof(DiagnosticDescriptor.Category)}"); + } + + var diagnosticMessage = diagnostics.Select(_ => _.MessageFormat.ToString()).ToImmutableArray(); + + Assert.That(diagnosticMessage, Contains.Item(ParallelizableUsageAnalyzerConstants.ParallelScopeSelfNoEffectOnAssemblyMessage), + $"{ParallelizableUsageAnalyzerConstants.ParallelScopeSelfNoEffectOnAssemblyMessage} is missing."); + Assert.That(diagnosticMessage, Contains.Item(ParallelizableUsageAnalyzerConstants.ParallelScopeChildrenOnNonParameterizedTestMethodMessage), + $"{ParallelizableUsageAnalyzerConstants.ParallelScopeChildrenOnNonParameterizedTestMethodMessage} is missing."); + Assert.That(diagnosticMessage, Contains.Item(ParallelizableUsageAnalyzerConstants.ParallelScopeFixturesOnTestMethodMessage), + $"{ParallelizableUsageAnalyzerConstants.ParallelScopeFixturesOnTestMethodMessage} is missing."); + } + + [TestCase(ParallelizableUsageAnalyzerConstants.ParallelScope.Self, ParallelScope.Self)] + [TestCase(ParallelizableUsageAnalyzerConstants.ParallelScope.Children, ParallelScope.Children)] + [TestCase(ParallelizableUsageAnalyzerConstants.ParallelScope.Fixtures, ParallelScope.Fixtures)] + public void ConstantMatchesValueInNUnit(int enumValue, ParallelScope parallelScope) + { + Assert.That(enumValue, Is.EqualTo((int)parallelScope)); + } + + [TestCase(ParallelScope.All)] + [TestCase(ParallelScope.Children)] + [TestCase(ParallelScope.Fixtures)] + public void AnalyzeWhenAssemblyAttributeIsNotParallelScopeSelf(ParallelScope parallelScope) + { + var enumValue = parallelScope.ToString(); + var testCode = $@" +using NUnit.Framework; +[assembly: Parallelizable(ParallelScope.{enumValue})]"; + AnalyzerAssert.Valid(testCode); + } + + [Test] + public void AnalyzeWhenAssemblyAttributeIsExplicitlyParallelScopeSelf() + { + var expectedDiagnostic = ExpectedDiagnostic.Create( + AnalyzerIdentifiers.ParallelScopeSelfNoEffectOnAssemblyUsage, + ParallelizableUsageAnalyzerConstants.ParallelScopeSelfNoEffectOnAssemblyMessage); + + var testCode = $@" +using NUnit.Framework; +[assembly: ↓Parallelizable(ParallelScope.Self)]"; + AnalyzerAssert.Diagnostics(analyzer, expectedDiagnostic, testCode); + } + + [Test] + public void AnalyzeWhenAssemblyAttributeIsImplicitlyParallelScopeSelf() + { + var expectedDiagnostic = ExpectedDiagnostic.Create( + AnalyzerIdentifiers.ParallelScopeSelfNoEffectOnAssemblyUsage, + ParallelizableUsageAnalyzerConstants.ParallelScopeSelfNoEffectOnAssemblyMessage); + + var testCode = $@" +using NUnit.Framework; +[assembly: ↓Parallelizable()]"; + AnalyzerAssert.Diagnostics(analyzer, expectedDiagnostic, testCode); + } + + [Theory] + public void AnalyzeWhenAttributeIsOnClass(ParallelScope parallelScope) + { + var enumValue = parallelScope.ToString(); + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing($@" + [TestFixture] + [Parallelizable(ParallelScope.{enumValue})] + public sealed class AnalyzeWhenAttributeIsOnClass + {{ + }}"); + AnalyzerAssert.Valid(testCode); + } + + [Test] + public void AnalyzeWhenAttributeIsOnSimpleTestMethodParallelScopeSelf() + { + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing(@" + [TestFixture] + public sealed class AnalyzeWhenAttributeIsOnSimpleTestMethodParallelScopeSelf + { + [Test] + [Parallelizable(ParallelScope.Self)] + public void Test() + { + } + }"); + AnalyzerAssert.Valid(testCode); + } + + [TestCase(ParallelScope.All)] + [TestCase(ParallelScope.Children)] + public void AnalyzeWhenAttributeIsOnSimpleTestMethodContainsParallelScopeChildren(ParallelScope parallelScope) + { + var expectedDiagnostic = ExpectedDiagnostic.Create( + AnalyzerIdentifiers.ParallelScopeChildrenOnNonParameterizedTestMethodUsage, + ParallelizableUsageAnalyzerConstants.ParallelScopeChildrenOnNonParameterizedTestMethodMessage); + + var enumValue = parallelScope.ToString(); + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing($@" + [TestFixture] + public sealed class AnalyzeWhenAttributeIsOnSimpleTestMethodContainsParallelScopeChildren + {{ + [Test] + [↓Parallelizable(ParallelScope.{enumValue})] + public void Test() + {{ + }} + }}"); + AnalyzerAssert.Diagnostics(analyzer, expectedDiagnostic, testCode); + } + + [Test] + public void AnalyzeWhenAttributeIsOnSimpleTestMethodIsParallelScopeFixtures() + { + var expectedDiagnostic = ExpectedDiagnostic.Create( + AnalyzerIdentifiers.ParallelScopeFixturesOnTestMethodUsage, + ParallelizableUsageAnalyzerConstants.ParallelScopeFixturesOnTestMethodMessage); + + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing(@" + [TestFixture] + public sealed class AnalyzeWhenAttributeIsOnSimpleTestMethodIsParallelScopeFixtures + { + [Test] + [↓Parallelizable(ParallelScope.Fixtures)] + public void Test() + { + } + }"); + AnalyzerAssert.Diagnostics(analyzer, expectedDiagnostic, testCode); + } + + [TestCaseSource(nameof(ParallelScopesExceptFixtures))] + public void AnalyzeWhenAttributeIsOnTestCaseTestMethodNotParallelScopeFixtures(ParallelScope parallelScope) + { + var enumValue = parallelScope.ToString(); + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing($@" + [TestFixture] + public sealed class AnalyzeWhenAttributeIsOnTestCaseTestMethodNotParallelScopeFixtures + {{ + [TestCase(1)] + [Parallelizable(ParallelScope.{enumValue})] + public void Test(int data) + {{ + }} + }}"); + AnalyzerAssert.Valid(testCode); + } + + [Test] + public void AnalyzeWhenAttributeIsOnTestCaseTestMethodIsParallelScopeFixtures() + { + var expectedDiagnostic = ExpectedDiagnostic.Create( + AnalyzerIdentifiers.ParallelScopeFixturesOnTestMethodUsage, + ParallelizableUsageAnalyzerConstants.ParallelScopeFixturesOnTestMethodMessage); + + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing(@" + [TestFixture] + public sealed class AnalyzeWhenAttributeIsOnTestCaseTestMethodIsParallelScopeFixtures + { + [TestCase(1)] + [↓Parallelizable(ParallelScope.Fixtures)] + public void Test(int data) + { + } + }"); + AnalyzerAssert.Diagnostics(analyzer, expectedDiagnostic, testCode); + } + + [TestCaseSource(nameof(ParallelScopesExceptFixtures))] + public void AnalyzeWhenAttributeIsOnParametricTestMethodNotParallelScopeFixtures(ParallelScope parallelScope) + { + var enumValue = parallelScope.ToString(); + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing($@" + [TestFixture] + public sealed class AnalyzeWhenAttributeIsOnParametricTestMethodNotParallelScopeFixtures + {{ + [Test] + [Parallelizable(ParallelScope.{enumValue})] + public void Test([Values(1, 2, 3)] int data) + {{ + }} + }}"); + AnalyzerAssert.Valid(testCode); + } + + [Test] + public void AnalyzeWhenAttributeIsOnParametricTestMethodIsParallelScopeFixtures() + { + var expectedDiagnostic = ExpectedDiagnostic.Create( + AnalyzerIdentifiers.ParallelScopeFixturesOnTestMethodUsage, + ParallelizableUsageAnalyzerConstants.ParallelScopeFixturesOnTestMethodMessage); + + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing(@" + [TestFixture] + public sealed class AnalyzeWhenAttributeIsOnParametricTestMethodIsParallelScopeFixtures + { + [Test] + [↓Parallelizable(ParallelScope.Fixtures)] + public void Test([Values(1, 2, 3)] int data) + { + } + }"); + AnalyzerAssert.Diagnostics(analyzer, expectedDiagnostic, testCode); + } + + private static IEnumerable ParallelScopesExceptFixtures => + new ParallelScope[] { ParallelScope.All, ParallelScope.Children, ParallelScope.Self }; + } +} diff --git a/src/nunit.analyzers/Constants/AnalyzerIdentifiers.cs b/src/nunit.analyzers/Constants/AnalyzerIdentifiers.cs index dac5c8f8..ce474d34 100644 --- a/src/nunit.analyzers/Constants/AnalyzerIdentifiers.cs +++ b/src/nunit.analyzers/Constants/AnalyzerIdentifiers.cs @@ -15,6 +15,8 @@ internal static class AnalyzerIdentifiers internal const string TestMethodExpectedResultTypeMismatchUsage = "NUNIT_11"; internal const string TestMethodSpecifiedExpectedResultForVoidUsage = "NUNIT_12"; internal const string TestMethodNoExpectedResultButNonVoidReturnType = "NUNIT_13"; - + internal const string ParallelScopeSelfNoEffectOnAssemblyUsage = "NUNIT_14"; + internal const string ParallelScopeChildrenOnNonParameterizedTestMethodUsage = "NUNIT_15"; + internal const string ParallelScopeFixturesOnTestMethodUsage = "NUNIT_16"; } } diff --git a/src/nunit.analyzers/Constants/NunitFrameworkConstants.cs b/src/nunit.analyzers/Constants/NunitFrameworkConstants.cs index aec116ea..0fe05556 100644 --- a/src/nunit.analyzers/Constants/NunitFrameworkConstants.cs +++ b/src/nunit.analyzers/Constants/NunitFrameworkConstants.cs @@ -27,10 +27,13 @@ public static class NunitFrameworkConstants public const string FullNameOfTypeTestCaseAttribute = "NUnit.Framework.TestCaseAttribute"; public const string FullNameOfTypeTestCaseSourceAttribute = "NUnit.Framework.TestCaseSourceAttribute"; public const string FullNameOfTypeTestAttribute = "NUnit.Framework.TestAttribute"; + public const string FullNameOfTypeParallelizableAttribute = "NUnit.Framework.ParallelizableAttribute"; + public const string FullNameOfTypeITestBuilder = "NUnit.Framework.Interfaces.ITestBuilder"; public const string NameOfTestCaseAttribute = "TestCaseAttribute"; public const string NameOfTestCaseSourceAttribute = "TestCaseSourceAttribute"; public const string NameOfTestAttribute = "TestAttribute"; + public const string NameOfParallelizableAttribute = "ParallelizableAttribute"; public const string NameOfExpectedResult = "ExpectedResult"; diff --git a/src/nunit.analyzers/Constants/ParallelizableUsageAnalyzerConstants.cs b/src/nunit.analyzers/Constants/ParallelizableUsageAnalyzerConstants.cs new file mode 100644 index 00000000..56830f32 --- /dev/null +++ b/src/nunit.analyzers/Constants/ParallelizableUsageAnalyzerConstants.cs @@ -0,0 +1,17 @@ +namespace NUnit.Analyzers.Constants +{ + class ParallelizableUsageAnalyzerConstants + { + internal const string Title = "Find Incorrect ParallelizableAttribute Usage"; + internal const string ParallelScopeSelfNoEffectOnAssemblyMessage = "Specifying ParallelScope.Self on assembly level has no effect"; + internal const string ParallelScopeChildrenOnNonParameterizedTestMethodMessage = "One may not specify ParallelScope.Children on a non-parameterized test method"; + internal const string ParallelScopeFixturesOnTestMethodMessage = "One may not specify ParallelScope.Fixtures on a test method"; + + internal class ParallelScope + { + internal const int Self = 1; + internal const int Children = 256; + internal const int Fixtures = 512; + } + } +} diff --git a/src/nunit.analyzers/ParallelizableUsage/ParallelizableUsageAnalyzer.cs b/src/nunit.analyzers/ParallelizableUsage/ParallelizableUsageAnalyzer.cs new file mode 100644 index 00000000..e355d508 --- /dev/null +++ b/src/nunit.analyzers/ParallelizableUsage/ParallelizableUsageAnalyzer.cs @@ -0,0 +1,164 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using NUnit.Analyzers.Constants; +using NUnit.Analyzers.Extensions; + +namespace NUnit.Analyzers.ParallelizableUsage +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class ParallelizableUsageAnalyzer : DiagnosticAnalyzer + { + internal const string AssemblyAttributeTargetSpecifier = "assembly"; + + private static DiagnosticDescriptor CreateDescriptor(string id, string message, DiagnosticSeverity severity) => + new DiagnosticDescriptor(id, ParallelizableUsageAnalyzerConstants.Title, + message, Categories.Usage, severity, true); + + private static readonly DiagnosticDescriptor scopeSelfNoEffectOnAssemblyUsage = + ParallelizableUsageAnalyzer.CreateDescriptor( + AnalyzerIdentifiers.ParallelScopeSelfNoEffectOnAssemblyUsage, + ParallelizableUsageAnalyzerConstants.ParallelScopeSelfNoEffectOnAssemblyMessage, + DiagnosticSeverity.Warning); + + private static readonly DiagnosticDescriptor scopeChildrenOnNonParameterizedTest = + ParallelizableUsageAnalyzer.CreateDescriptor( + AnalyzerIdentifiers.ParallelScopeChildrenOnNonParameterizedTestMethodUsage, + ParallelizableUsageAnalyzerConstants.ParallelScopeChildrenOnNonParameterizedTestMethodMessage, + DiagnosticSeverity.Error); + + private static readonly DiagnosticDescriptor scopeFixturesOnTest = + ParallelizableUsageAnalyzer.CreateDescriptor( + AnalyzerIdentifiers.ParallelScopeFixturesOnTestMethodUsage, + ParallelizableUsageAnalyzerConstants.ParallelScopeFixturesOnTestMethodMessage, + DiagnosticSeverity.Error); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + scopeSelfNoEffectOnAssemblyUsage, + scopeChildrenOnNonParameterizedTest, + scopeFixturesOnTest); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(ParallelizableUsageAnalyzer.AnalyzeAttribute, SyntaxKind.Attribute); + } + + private static void AnalyzeAttribute(SyntaxNodeAnalysisContext context) + { + var parallelizableAttributeType = context.SemanticModel.Compilation.GetTypeByMetadataName( + NunitFrameworkConstants.FullNameOfTypeParallelizableAttribute); + if (parallelizableAttributeType == null) + return; + + var attributeNode = (AttributeSyntax)context.Node; + var attributeSymbol = context.SemanticModel.GetSymbolInfo(attributeNode).Symbol; + + if (parallelizableAttributeType.ContainingAssembly.Identity != attributeSymbol?.ContainingAssembly.Identity || + NunitFrameworkConstants.NameOfParallelizableAttribute != attributeSymbol?.ContainingType.Name) + return; + + context.CancellationToken.ThrowIfCancellationRequested(); + + var possibleEnumValue = GetOptionalEnumValue(context, attributeNode); + if (possibleEnumValue == null) + return; + + int enumValue = possibleEnumValue.Value; + var attributeListSyntax = attributeNode.Parent as AttributeListSyntax; + if (attributeListSyntax == null) + return; + + if (HasExactFlag(enumValue, ParallelizableUsageAnalyzerConstants.ParallelScope.Self)) + { + // Specifying ParallelScope.Self on an assembly level attribute has no effect + var atAssemblyLevel = attributeListSyntax.Target?.Identifier.ValueText == AssemblyAttributeTargetSpecifier; + if (atAssemblyLevel) + { + context.ReportDiagnostic(Diagnostic.Create(scopeSelfNoEffectOnAssemblyUsage, + attributeNode.GetLocation())); + } + } + else if (HasFlag(enumValue, ParallelizableUsageAnalyzerConstants.ParallelScope.Children)) + { + // One may not specify ParallelScope.Children on a non-parameterized test method + if (IsNonParameterizedTestMethod(context, attributeListSyntax.Parent as MethodDeclarationSyntax)) + { + context.ReportDiagnostic(Diagnostic.Create(scopeChildrenOnNonParameterizedTest, + attributeNode.GetLocation())); + } + } + else if (HasFlag(enumValue, ParallelizableUsageAnalyzerConstants.ParallelScope.Fixtures)) + { + // One may not specify ParallelScope.Fixtures on a test method + if (attributeListSyntax.Parent is MethodDeclarationSyntax) + { + context.ReportDiagnostic(Diagnostic.Create(scopeFixturesOnTest, + attributeNode.GetLocation())); + } + } + } + + private static int? GetOptionalEnumValue(SyntaxNodeAnalysisContext context, AttributeSyntax attributeNode) + { + var attributePositionalAndNamedArguments = attributeNode.GetArguments(); + var attributePositionalArguments = attributePositionalAndNamedArguments.Item1; + var noExplicitEnumArgument = attributePositionalArguments.Length == 0; + if (noExplicitEnumArgument) + { + return ParallelizableUsageAnalyzerConstants.ParallelScope.Self; + } + else + { + var arg = attributePositionalArguments[0]; + var constantValue = context.SemanticModel.GetConstantValue(arg.Expression); + if (constantValue.HasValue) + { + return constantValue.Value as int?; + } + } + + return null; + } + + private static bool IsNonParameterizedTestMethod(SyntaxNodeAnalysisContext context, + MethodDeclarationSyntax methodDeclarationSyntax) + { + if (methodDeclarationSyntax == null) + return false; + + // The method is only a parametric method if (see DefaultTestCaseBuilder.BuildFrom) + // * it has parameters + // * is marked with one or more attributes deriving from ITestBuilder + // * the attributes defines tests (difficult to access without evaluating the code) + bool noParameters = methodDeclarationSyntax.ParameterList.Parameters.Count == 0; + + var allAttributes = methodDeclarationSyntax.AttributeLists.SelectMany(al => al.Attributes); + bool noITestBuilders = !allAttributes.Where(a => DerivesFromITestBuilder(context, a)).Any(); + return noParameters && noITestBuilders; + } + + private static bool DerivesFromITestBuilder(SyntaxNodeAnalysisContext context, AttributeSyntax attribute) + { + var parallelizableAttributeType = context.SemanticModel.Compilation.GetTypeByMetadataName( + NunitFrameworkConstants.FullNameOfTypeITestBuilder); + if (parallelizableAttributeType == null) + return false; + + var attributeType = context.SemanticModel.GetTypeInfo(attribute).Type; + + if (attributeType == null) + return false; + + return attributeType.AllInterfaces.Any(i => i.Equals(parallelizableAttributeType)); + } + + private static bool HasFlag(int enumValue, int flag) + => (enumValue & flag) == flag; + + private static bool HasExactFlag(int enumValue, int flag) + => enumValue == flag; + } +}