diff --git a/Documentation/Diagnostics/PH2152.md b/Documentation/Diagnostics/PH2152.md new file mode 100644 index 000000000..5125830e5 --- /dev/null +++ b/Documentation/Diagnostics/PH2152.md @@ -0,0 +1,69 @@ +# PH2152: Order DataRow attribute above TestMethod for unit tests + +| Property | Value | +|--|--| +| Package | [Philips.CodeAnalysis.MsTestAnalyzers](https://www.nuget.org/packages/Philips.CodeAnalysis.MsTestAnalyzers) | +| Diagnostic ID | PH2152 | +| Category | [MsTest](../MsTest.md) | +| Analyzer | [DataRowOrderAnalyzer](https://github.com/philips-software/roslyn-analyzers/blob/main/Philips.CodeAnalysis.MsTestAnalyzers/DataRowOrderAnalyzer.cs) +| CodeFix | Yes | +| Severity | Warning | +| Enabled By Default | No | + +## Introduction + +DataRow attributes should be consistently ordered above TestMethod/DataTestMethod attributes on test methods to improve readability and maintain consistency across the codebase. + +## How to solve + +Move all DataRow attributes to appear before TestMethod or DataTestMethod attributes on the method. + +## Example + +Code that triggers a diagnostic: +``` cs +[TestClass] +public class Tests +{ + [TestMethod] + [DataRow(1, 2)] + [DataRow(3, 4)] + public void TestMethod1(int x, int y) + { + // Test implementation + } + + [DataTestMethod] + [DataRow("test")] + public void TestMethod2(string value) + { + // Test implementation + } +} +``` + +And the replacement code: +``` cs +[TestClass] +public class Tests +{ + [DataRow(1, 2)] + [DataRow(3, 4)] + [TestMethod] + public void TestMethod1(int x, int y) + { + // Test implementation + } + + [DataRow("test")] + [DataTestMethod] + public void TestMethod2(string value) + { + // Test implementation + } +} +``` + +## Configuration + +This analyzer does not offer any special configuration. The general ways of [suppressing](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/suppress-warnings) diagnostics apply. \ No newline at end of file diff --git a/Philips.CodeAnalysis.Common/AttributeHelper.cs b/Philips.CodeAnalysis.Common/AttributeHelper.cs index f1d3b46a4..0e2e7a43c 100644 --- a/Philips.CodeAnalysis.Common/AttributeHelper.cs +++ b/Philips.CodeAnalysis.Common/AttributeHelper.cs @@ -1,6 +1,7 @@ // © 2019 Koninklijke Philips N.V. See License.md in the project root for license information. using System; +using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -122,6 +123,88 @@ public bool IsDataRowAttribute(AttributeSyntax attribute, SyntaxNodeAnalysisCont return IsAttribute(attribute, context, MsTestFrameworkDefinitions.DataRowAttribute, out _, out _); } + /// + /// Checks if any attribute of the specified types appears after any attribute of the other specified types. + /// + /// The attribute lists to examine + /// The syntax node analysis context + /// The attributes to look for that should appear first + /// The attributes that should not appear before attributesToFind + /// True if any attributesToFind appears after any attributesToCheckAfter + public bool HasAttributeAfterOther(SyntaxList attributeLists, SyntaxNodeAnalysisContext context, INamedTypeSymbol[] attributesToFind, INamedTypeSymbol[] attributesToCheckAfter) + { + var allAttributes = attributeLists + .SelectMany((list, listIndex) => + list.Attributes.Select((attr, attrIndex) => new + { + Attribute = attr, + Position = (listIndex, attrIndex), + Symbol = context.SemanticModel.GetSymbolInfo(attr).Symbol?.ContainingType + })) + .Where(x => x.Symbol != null) + .ToArray(); + + (int listIndex, int attrIndex)? firstCheckAfterPosition = allAttributes + .Where(x => attributesToCheckAfter.Any(attr => AttributeMatches(x.Symbol, attr))) + .Select(x => x.Position) + .FirstOrDefault(); + + if (firstCheckAfterPosition == null) + { + return false; + } + + return allAttributes + .Where(x => attributesToFind.Any(attr => AttributeMatches(x.Symbol, attr))) + .Any(x => x.Position.CompareTo(firstCheckAfterPosition.Value) > 0); + } + + /// + /// Categorizes attributes into groups based on the provided type predicates. + /// + /// The attribute lists to categorize + /// The syntax node analysis context + /// Functions that determine which category an attribute belongs to + /// A dictionary mapping category names to lists of attributes + public Dictionary> CategorizeAttributes(SyntaxList attributeLists, SyntaxNodeAnalysisContext context, Dictionary> categorizers) + { + var result = new Dictionary>(); + foreach (var category in categorizers.Keys) + { + result[category] = []; + } + + foreach (AttributeListSyntax attributeList in attributeLists) + { + foreach (AttributeSyntax attribute in attributeList.Attributes) + { + INamedTypeSymbol attributeSymbol = context.SemanticModel.GetSymbolInfo(attribute).Symbol?.ContainingType; + if (attributeSymbol != null) + { + KeyValuePair>? matchingCategorizer = categorizers.Where(c => c.Value(attributeSymbol)).FirstOrDefault(); + if (matchingCategorizer.HasValue) + { + result[matchingCategorizer.Value.Key].Add(attribute); + } + } + } + } + + return result; + } + + /// + /// Checks if an attribute symbol matches a target symbol (including inheritance). + /// + /// The attribute symbol to check + /// The target symbol to match against + /// True if the symbols match or if attributeSymbol is derived from targetSymbol + private static bool AttributeMatches(INamedTypeSymbol attributeSymbol, INamedTypeSymbol targetSymbol) + { + return SymbolEqualityComparer.Default.Equals(attributeSymbol, targetSymbol) || + attributeSymbol.IsDerivedFrom(targetSymbol); + } + public bool TryExtractAttributeArgument(AttributeArgumentSyntax argumentSyntax, SyntaxNodeAnalysisContext context, out string argumentString, out T value) { argumentString = argumentSyntax.Expression.ToString(); diff --git a/Philips.CodeAnalysis.Common/DiagnosticId.cs b/Philips.CodeAnalysis.Common/DiagnosticId.cs index 890ae1fa7..b959282da 100644 --- a/Philips.CodeAnalysis.Common/DiagnosticId.cs +++ b/Philips.CodeAnalysis.Common/DiagnosticId.cs @@ -141,6 +141,7 @@ public enum DiagnosticId AvoidVariableNamedUnderscore = 2147, AvoidProblematicUsingPatterns = 2149, AvoidTodoComments = 2151, + DataRowOrderInTestMethod = 2152, AvoidUnusedToString = 2153, AvoidUnlicensedPackages = 2155, AvoidPkcsPaddingWithRsaEncryption = 2158, diff --git a/Philips.CodeAnalysis.Common/Helper.cs b/Philips.CodeAnalysis.Common/Helper.cs index 82ffa45f2..8cdd5f639 100644 --- a/Philips.CodeAnalysis.Common/Helper.cs +++ b/Philips.CodeAnalysis.Common/Helper.cs @@ -10,5 +10,7 @@ public class Helper(AnalyzerOptions options, Compilation compilation) : CodeFixH public AllowedSymbols ForAllowedSymbols { get; } = new AllowedSymbols(compilation); public AdditionalFilesHelper ForAdditionalFiles { get; } = new AdditionalFilesHelper(options, compilation); + + public AttributeHelper AttributeHelper { get; } = new AttributeHelper(); } } diff --git a/Philips.CodeAnalysis.MsTestAnalyzers/DataRowOrderAnalyzer.cs b/Philips.CodeAnalysis.MsTestAnalyzers/DataRowOrderAnalyzer.cs new file mode 100644 index 000000000..b32be6ddd --- /dev/null +++ b/Philips.CodeAnalysis.MsTestAnalyzers/DataRowOrderAnalyzer.cs @@ -0,0 +1,68 @@ +// © 2025 Koninklijke Philips N.V. See License.md in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Philips.CodeAnalysis.Common; + +namespace Philips.CodeAnalysis.MsTestAnalyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class DataRowOrderAnalyzer : TestAttributeDiagnosticAnalyzer + { + private const string Title = @"Order DataRow attribute above TestMethod for unit tests"; + public static readonly string MessageFormat = @"DataRow attributes should be placed above TestMethod/DataTestMethod attributes"; + private const string Description = @"DataRow attributes should be consistently ordered above TestMethod/DataTestMethod attributes on test methods"; + private const string Category = Categories.MsTest; + + private static readonly DiagnosticDescriptor Rule = new(DiagnosticId.DataRowOrderInTestMethod.ToId(), + Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: false, description: Description); + + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + protected override Implementation OnInitializeAnalyzer(AnalyzerOptions options, Compilation compilation, MsTestAttributeDefinitions definitions) + { + return new DataRowOrderImplementation(definitions, Helper); + } + + private sealed class DataRowOrderImplementation : Implementation + { + private readonly MsTestAttributeDefinitions _definitions; + + public DataRowOrderImplementation(MsTestAttributeDefinitions definitions, Helper helper) : base(helper) + { + _definitions = definitions; + } + + public override void OnTestAttributeMethod(SyntaxNodeAnalysisContext context, MethodDeclarationSyntax methodDeclaration, IMethodSymbol methodSymbol, HashSet presentAttributes) + { + if (!HasRequiredAttributes(presentAttributes)) + { + return; + } + + // Check if DataRow comes after TestMethod using the generic AttributeHelper method + INamedTypeSymbol[] dataRowAttributes = { _definitions.DataRowSymbol }; + INamedTypeSymbol[] testMethodAttributes = { _definitions.TestMethodSymbol, _definitions.DataTestMethodSymbol }; + + if (Helper.AttributeHelper.HasAttributeAfterOther(methodDeclaration.AttributeLists, context, dataRowAttributes, testMethodAttributes)) + { + Location location = methodDeclaration.Identifier.GetLocation(); + context.ReportDiagnostic(Diagnostic.Create(Rule, location, methodDeclaration.Identifier)); + } + } + + private bool HasRequiredAttributes(HashSet presentAttributes) + { + return presentAttributes.Contains(_definitions.DataRowSymbol) && + presentAttributes.Any(attr => + attr.IsDerivedFrom(_definitions.TestMethodSymbol) || + attr.IsDerivedFrom(_definitions.DataTestMethodSymbol)); + } + } + } +} diff --git a/Philips.CodeAnalysis.MsTestAnalyzers/DataRowOrderCodeFixProvider.cs b/Philips.CodeAnalysis.MsTestAnalyzers/DataRowOrderCodeFixProvider.cs new file mode 100644 index 000000000..f05374678 --- /dev/null +++ b/Philips.CodeAnalysis.MsTestAnalyzers/DataRowOrderCodeFixProvider.cs @@ -0,0 +1,100 @@ +// © 2025 Koninklijke Philips N.V. See License.md in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Philips.CodeAnalysis.Common; + +namespace Philips.CodeAnalysis.MsTestAnalyzers +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DataRowOrderCodeFixProvider)), Shared] + public class DataRowOrderCodeFixProvider : SingleDiagnosticCodeFixProvider + { + protected override string Title => "Move DataRow attributes above TestMethod"; + + protected override DiagnosticId DiagnosticId => DiagnosticId.DataRowOrderInTestMethod; + + protected override async Task ApplyFix(Document document, MethodDeclarationSyntax node, ImmutableDictionary properties, CancellationToken cancellationToken) + { + SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken); + SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken); + + // Get the MsTest definitions to identify attributes + var definitions = MsTestAttributeDefinitions.FromCompilation(semanticModel.Compilation); + + // Reorder the attributes + MethodDeclarationSyntax reorderedMethod = ReorderAttributes(node, semanticModel, definitions); + + SyntaxNode newRoot = root.ReplaceNode(node, reorderedMethod); + return document.WithSyntaxRoot(newRoot); + } + + private static MethodDeclarationSyntax ReorderAttributes(MethodDeclarationSyntax method, SemanticModel semanticModel, MsTestAttributeDefinitions definitions) + { + var dataRowAttributes = new List(); + var testMethodAttributes = new List(); + var otherAttributes = new List(); + + // Categorize all attributes + foreach (AttributeListSyntax attributeList in method.AttributeLists) + { + foreach (AttributeSyntax attribute in attributeList.Attributes) + { + INamedTypeSymbol attributeSymbol = semanticModel.GetSymbolInfo(attribute).Symbol?.ContainingType; + if (attributeSymbol != null) + { + if (SymbolEqualityComparer.Default.Equals(attributeSymbol, definitions.DataRowSymbol)) + { + dataRowAttributes.Add(attribute); + } + else if (attributeSymbol.IsDerivedFrom(definitions.TestMethodSymbol) || + attributeSymbol.IsDerivedFrom(definitions.DataTestMethodSymbol)) + { + testMethodAttributes.Add(attribute); + } + else + { + otherAttributes.Add(attribute); + } + } + else + { + otherAttributes.Add(attribute); + } + } + } + + // Create new attribute lists in the correct order: DataRow, Other, TestMethod + SyntaxList newAttributeLists = SyntaxFactory.List(); + + // Add DataRow attributes first + foreach (AttributeSyntax attr in dataRowAttributes) + { + newAttributeLists = newAttributeLists.Add( + SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(attr))); + } + + // Add other attributes (like TestCategory, Timeout, etc.) + foreach (AttributeSyntax attr in otherAttributes) + { + newAttributeLists = newAttributeLists.Add( + SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(attr))); + } + + // Add TestMethod/DataTestMethod attributes last + foreach (AttributeSyntax attr in testMethodAttributes) + { + newAttributeLists = newAttributeLists.Add( + SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(attr))); + } + + return method.WithAttributeLists(newAttributeLists); + } + } +} \ No newline at end of file diff --git a/Philips.CodeAnalysis.MsTestAnalyzers/Philips.CodeAnalysis.MsTestAnalyzers.md b/Philips.CodeAnalysis.MsTestAnalyzers/Philips.CodeAnalysis.MsTestAnalyzers.md index e56f13613..1d6ec3c98 100644 --- a/Philips.CodeAnalysis.MsTestAnalyzers/Philips.CodeAnalysis.MsTestAnalyzers.md +++ b/Philips.CodeAnalysis.MsTestAnalyzers/Philips.CodeAnalysis.MsTestAnalyzers.md @@ -38,5 +38,5 @@ | [PH2059](../Documentation/Diagnostics/PH2059.md) | Public Method should be TestMethod | Public methods inside a TestClass should either be a test method or non-public. | ✅ **Use [MSTEST0029](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0029)** | | [PH2076](../Documentation/Diagnostics/PH2076.md) | Assert.Fail alternatives | Assert.Fail should not be used if an alternative is more appropriate | ⚠️ **Consider [MSTEST0025](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0025)** | | [PH2095](../Documentation/Diagnostics/PH2095.md) | TestMethods must return void/Task for async methods | TestMethods must return Task if they are async methods, or void if not | ⚠️ **Consider [MSTEST0003](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0003)** | - +| [PH2152](../Documentation/Diagnostics/PH2152.md) | Order DataRow attribute above TestMethod for unit tests | DataRow attributes should be consistently ordered above TestMethod/DataTestMethod attributes on test methods | diff --git a/Philips.CodeAnalysis.Test/MsTest/DataRowOrderAnalyzerTest.cs b/Philips.CodeAnalysis.Test/MsTest/DataRowOrderAnalyzerTest.cs new file mode 100644 index 000000000..92764a76f --- /dev/null +++ b/Philips.CodeAnalysis.Test/MsTest/DataRowOrderAnalyzerTest.cs @@ -0,0 +1,190 @@ +// © 2025 Koninklijke Philips N.V. See License.md in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Philips.CodeAnalysis.Common; +using Philips.CodeAnalysis.MsTestAnalyzers; +using Philips.CodeAnalysis.Test.Helpers; +using Philips.CodeAnalysis.Test.Verifiers; + +namespace Philips.CodeAnalysis.Test.MsTest +{ + [TestClass] + public class DataRowOrderAnalyzerTest : DiagnosticVerifier + { + protected override DiagnosticAnalyzer GetDiagnosticAnalyzer() + { + return new DataRowOrderAnalyzer(); + } + + private static DiagnosticResult CreateExpectedResult(int line, int column) + { + return new DiagnosticResult + { + Id = DiagnosticId.DataRowOrderInTestMethod.ToId(), + Severity = DiagnosticSeverity.Warning, + Location = new DiagnosticResultLocation(null, line, column), + Message = new System.Text.RegularExpressions.Regex(".*") + }; + } + + [TestMethod] + [TestCategory(TestDefinitions.UnitTests)] + public async Task GoodOrderNoDiagnosticAsync() + { + const string code = @" +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Tests +{ + [DataRow(1, 2)] + [DataRow(3, 4)] + [TestMethod] + public void TestMethod1(int x, int y) { } + + [DataRow(""test"")] + [DataTestMethod] + public void TestMethod2(string value) { } +}"; + + await VerifySuccessfulCompilation(code).ConfigureAwait(false); + } + + [TestMethod] + [TestCategory(TestDefinitions.UnitTests)] + public async Task BadOrderReportsDiagnosticAsync() + { + const string code = @" +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Tests +{ + [TestMethod] + [DataRow(1, 2)] + public void TestMethod1(int x, int y) { } +}"; + + await VerifyDiagnostic(code, CreateExpectedResult(9, 14)).ConfigureAwait(false); + } + + [TestMethod] + [TestCategory(TestDefinitions.UnitTests)] + public async Task NoDataRowNoDiagnosticAsync() + { + const string code = @" +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Tests +{ + [TestMethod] + public void TestMethod1() { } + + [DataTestMethod] + public void TestMethod2() { } +}"; + + await VerifySuccessfulCompilation(code).ConfigureAwait(false); + } + + [TestMethod] + [TestCategory(TestDefinitions.UnitTests)] + public async Task NoTestMethodNoDiagnosticAsync() + { + const string code = @" +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Tests +{ + [DataRow(1, 2)] + public void TestMethod1(int x, int y) { } +}"; + + await VerifySuccessfulCompilation(code).ConfigureAwait(false); + } + + [TestMethod] + [TestCategory(TestDefinitions.UnitTests)] + public async Task MixedWithOtherAttributesGoodOrderNoDiagnosticAsync() + { + const string code = @" +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Tests +{ + [DataRow(1, 2)] + [TestCategory(""Unit"")] + [TestMethod] + [Timeout(1000)] + public void TestMethod1(int x, int y) { } +}"; + + await VerifySuccessfulCompilation(code).ConfigureAwait(false); + } + + [TestMethod] + [TestCategory(TestDefinitions.UnitTests)] + public async Task MixedWithOtherAttributesBadOrderReportsDiagnosticAsync() + { + const string code = @" +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Tests +{ + [TestCategory(""Unit"")] + [TestMethod] + [DataRow(1, 2)] + [Timeout(1000)] + public void TestMethod1(int x, int y) { } +}"; + + await VerifyDiagnostic(code, CreateExpectedResult(11, 14)).ConfigureAwait(false); + } + + [TestMethod] + [TestCategory(TestDefinitions.UnitTests)] + public async Task MultipleDataRowsGoodOrderNoDiagnosticAsync() + { + const string code = @" +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Tests +{ + [DataRow(1, 2)] + [DataRow(3, 4)] + [DataRow(5, 6)] + [TestMethod] + public void TestMethod1(int x, int y) { } +}"; + + await VerifySuccessfulCompilation(code).ConfigureAwait(false); + } + + [TestMethod] + [TestCategory(TestDefinitions.UnitTests)] + public async Task MultipleDataRowsBadOrderReportsDiagnosticAsync() + { + const string code = @" +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Tests +{ + [DataRow(1, 2)] + [TestMethod] + [DataRow(3, 4)] + public void TestMethod1(int x, int y) { } +}"; + + await VerifyDiagnostic(code, CreateExpectedResult(10, 14)).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/Philips.CodeAnalysis.Test/MsTest/DataRowOrderCodeFixProviderTest.cs b/Philips.CodeAnalysis.Test/MsTest/DataRowOrderCodeFixProviderTest.cs new file mode 100644 index 000000000..0df4fce1a --- /dev/null +++ b/Philips.CodeAnalysis.Test/MsTest/DataRowOrderCodeFixProviderTest.cs @@ -0,0 +1,201 @@ +// © 2025 Koninklijke Philips N.V. See License.md in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Philips.CodeAnalysis.Common; +using Philips.CodeAnalysis.MsTestAnalyzers; +using Philips.CodeAnalysis.Test.Helpers; +using Philips.CodeAnalysis.Test.Verifiers; + +namespace Philips.CodeAnalysis.Test.MsTest +{ + [TestClass] + public class DataRowOrderCodeFixProviderTest : CodeFixVerifier + { + protected override DiagnosticAnalyzer GetDiagnosticAnalyzer() + { + return new DataRowOrderAnalyzer(); + } + + protected override CodeFixProvider GetCodeFixProvider() + { + return new DataRowOrderCodeFixProvider(); + } + + private static DiagnosticResult CreateExpectedResult(int line, int column) + { + return new DiagnosticResult + { + Id = DiagnosticId.DataRowOrderInTestMethod.ToId(), + Severity = DiagnosticSeverity.Warning, + Location = new DiagnosticResultLocation(null, line, column), + Message = new System.Text.RegularExpressions.Regex(".*") + }; + } + + [TestMethod] + [TestCategory(TestDefinitions.UnitTests)] + public async Task SimpleReorderingCodeFixAsync() + { + const string given = @" +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Tests +{ + [TestMethod] + [DataRow(1, 2)] + public void TestMethod1(int x, int y) { } +}"; + + const string expected = @" +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Tests +{ + [DataRow(1, 2)] + [TestMethod] + public void TestMethod1(int x, int y) { } +}"; + + await VerifyDiagnostic(given, CreateExpectedResult(9, 14)).ConfigureAwait(false); + await VerifyFix(given, expected).ConfigureAwait(false); + } + + [TestMethod] + [TestCategory(TestDefinitions.UnitTests)] + public async Task MultipleDataRowsCodeFixAsync() + { + const string given = @" +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Tests +{ + [TestMethod] + [DataRow(1, 2)] + [DataRow(3, 4)] + public void TestMethod1(int x, int y) { } +}"; + + const string expected = @" +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Tests +{ + [DataRow(1, 2)] + [DataRow(3, 4)] + [TestMethod] + public void TestMethod1(int x, int y) { } +}"; + + await VerifyDiagnostic(given, CreateExpectedResult(10, 14)).ConfigureAwait(false); + await VerifyFix(given, expected).ConfigureAwait(false); + } + + [TestMethod] + [TestCategory(TestDefinitions.UnitTests)] + public async Task WithOtherAttributesCodeFixAsync() + { + const string given = @" +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Tests +{ + [TestCategory(""Unit"")] + [TestMethod] + [DataRow(1, 2)] + [Timeout(1000)] + public void TestMethod1(int x, int y) { } +}"; + + const string expected = @" +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Tests +{ + [DataRow(1, 2)] + [TestCategory(""Unit"")] + [Timeout(1000)] + [TestMethod] + public void TestMethod1(int x, int y) { } +}"; + + await VerifyDiagnostic(given, CreateExpectedResult(11, 14)).ConfigureAwait(false); + await VerifyFix(given, expected).ConfigureAwait(false); + } + + [TestMethod] + [TestCategory(TestDefinitions.UnitTests)] + public async Task DataTestMethodCodeFixAsync() + { + const string given = @" +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Tests +{ + [DataTestMethod] + [DataRow(""test"")] + public void TestMethod1(string value) { } +}"; + + const string expected = @" +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Tests +{ + [DataRow(""test"")] + [DataTestMethod] + public void TestMethod1(string value) { } +}"; + + await VerifyDiagnostic(given, CreateExpectedResult(9, 14)).ConfigureAwait(false); + await VerifyFix(given, expected).ConfigureAwait(false); + } + + [TestMethod] + [TestCategory(TestDefinitions.UnitTests)] + public async Task ComplexMixedAttributesCodeFixAsync() + { + const string given = @" +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Tests +{ + [TestCategory(""Unit"")] + [DataRow(1, 2)] + [TestMethod] + [DataRow(3, 4)] + [Timeout(1000)] + public void TestMethod1(int x, int y) { } +}"; + + const string expected = @" +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Tests +{ + [DataRow(1, 2)] + [DataRow(3, 4)] + [TestCategory(""Unit"")] + [Timeout(1000)] + [TestMethod] + public void TestMethod1(int x, int y) { } +}"; + + await VerifyDiagnostic(given, CreateExpectedResult(12, 14)).ConfigureAwait(false); + await VerifyFix(given, expected).ConfigureAwait(false); + } + } +} \ No newline at end of file