diff --git a/TUnit.Analyzers.CodeFixers/Base/AssertionRewriter.cs b/TUnit.Analyzers.CodeFixers/Base/AssertionRewriter.cs index 9cc05fda97..ea964fc8f2 100644 --- a/TUnit.Analyzers.CodeFixers/Base/AssertionRewriter.cs +++ b/TUnit.Analyzers.CodeFixers/Base/AssertionRewriter.cs @@ -34,6 +34,15 @@ protected ExpressionSyntax CreateTUnitAssertion( string methodName, ExpressionSyntax actualValue, params ArgumentSyntax[] additionalArguments) + { + return CreateTUnitAssertionWithMessage(methodName, actualValue, null, additionalArguments); + } + + protected ExpressionSyntax CreateTUnitAssertionWithMessage( + string methodName, + ExpressionSyntax actualValue, + ExpressionSyntax? message, + params ArgumentSyntax[] additionalArguments) { // Create Assert.That(actualValue) var assertThatInvocation = SyntaxFactory.InvocationExpression( @@ -60,11 +69,140 @@ protected ExpressionSyntax CreateTUnitAssertion( ? SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(additionalArguments)) : SyntaxFactory.ArgumentList(); - var fullInvocation = SyntaxFactory.InvocationExpression(methodAccess, arguments); + ExpressionSyntax fullInvocation = SyntaxFactory.InvocationExpression(methodAccess, arguments); + + // Add .Because(message) if message is provided and non-empty + if (message != null && !IsEmptyOrNullMessage(message)) + { + var becauseAccess = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + fullInvocation, + SyntaxFactory.IdentifierName("Because") + ); - // Now wrap the entire thing in await: await Assert.That(actualValue).MethodName(args) + fullInvocation = SyntaxFactory.InvocationExpression( + becauseAccess, + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument(message) + ) + ) + ); + } + + // Now wrap the entire thing in await: await Assert.That(actualValue).MethodName(args).Because(message) return SyntaxFactory.AwaitExpression(fullInvocation); } + + private static bool IsEmptyOrNullMessage(ExpressionSyntax message) + { + // Check for null literal + if (message is LiteralExpressionSyntax literal) + { + if (literal.IsKind(SyntaxKind.NullLiteralExpression)) + { + return true; + } + + // Check for empty string literal + if (literal.IsKind(SyntaxKind.StringLiteralExpression) && + literal.Token.ValueText == "") + { + return true; + } + } + + return false; + } + + /// + /// Extracts the message and any format arguments from an argument list. + /// Format string messages like Assert.AreEqual(5, x, "Expected {0}", x) have args after the message. + /// + protected static (ExpressionSyntax? message, ArgumentSyntax[]? formatArgs) ExtractMessageWithFormatArgs( + SeparatedSyntaxList arguments, + int messageIndex) + { + if (arguments.Count <= messageIndex) + { + return (null, null); + } + + var message = arguments[messageIndex].Expression; + + // Check if there are additional format arguments after the message + if (arguments.Count > messageIndex + 1) + { + var formatArgs = arguments.Skip(messageIndex + 1).ToArray(); + return (message, formatArgs); + } + + return (message, null); + } + + /// + /// Creates a message expression, wrapping in string.Format if format args are present. + /// + protected static ExpressionSyntax CreateMessageExpression( + ExpressionSyntax message, + ArgumentSyntax[]? formatArgs) + { + if (formatArgs == null || formatArgs.Length == 0) + { + return message; + } + + // Create string.Format(message, arg1, arg2, ...) + var allArgs = new List + { + SyntaxFactory.Argument(message) + }; + allArgs.AddRange(formatArgs); + + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword)), + SyntaxFactory.IdentifierName("Format") + ), + SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(allArgs)) + ); + } + + /// + /// Checks if the argument at the given index appears to be a comparer (IComparer, IEqualityComparer). + /// + protected bool IsLikelyComparerArgument(ArgumentSyntax argument) + { + var typeInfo = SemanticModel.GetTypeInfo(argument.Expression); + if (typeInfo.Type == null) return false; + + var typeName = typeInfo.Type.ToDisplayString(); + + // Check for IComparer, IComparer, IEqualityComparer, IEqualityComparer + if (typeName.Contains("IComparer") || typeName.Contains("IEqualityComparer")) + { + return true; + } + + // Check interfaces + if (typeInfo.Type is INamedTypeSymbol namedType) + { + return namedType.AllInterfaces.Any(i => + i.Name == "IComparer" || + i.Name == "IEqualityComparer"); + } + + return false; + } + + /// + /// Creates a TODO comment for unsupported features during migration. + /// + protected static SyntaxTrivia CreateTodoComment(string message) + { + return SyntaxFactory.Comment($"// TODO: TUnit migration - {message}"); + } protected bool IsFrameworkAssertion(InvocationExpressionSyntax invocation) { diff --git a/TUnit.Analyzers.CodeFixers/Base/AsyncMethodSignatureRewriter.cs b/TUnit.Analyzers.CodeFixers/Base/AsyncMethodSignatureRewriter.cs new file mode 100644 index 0000000000..350227a03d --- /dev/null +++ b/TUnit.Analyzers.CodeFixers/Base/AsyncMethodSignatureRewriter.cs @@ -0,0 +1,85 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TUnit.Analyzers.CodeFixers.Base; + +/// +/// Transforms method signatures that contain await expressions but are not marked as async. +/// Converts void methods to async Task and T-returning methods to async Task<T>. +/// +public class AsyncMethodSignatureRewriter : CSharpSyntaxRewriter +{ + public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) + { + // First, visit children to ensure nested content is processed + node = (MethodDeclarationSyntax)base.VisitMethodDeclaration(node)!; + + // Skip if already async or abstract + if (node.Modifiers.Any(SyntaxKind.AsyncKeyword) || + node.Modifiers.Any(SyntaxKind.AbstractKeyword)) + { + return node; + } + + // Check if method contains await expressions + bool hasAwait = node.DescendantNodes().OfType().Any(); + if (!hasAwait) + { + return node; + } + + // Convert the return type + var newReturnType = ConvertReturnType(node.ReturnType); + + // Add async modifier after access modifiers but before other modifiers (like static) + var newModifiers = InsertAsyncModifier(node.Modifiers); + + return node + .WithReturnType(newReturnType) + .WithModifiers(newModifiers); + } + + private static TypeSyntax ConvertReturnType(TypeSyntax returnType) + { + // void -> Task + if (returnType is PredefinedTypeSyntax predefined && predefined.Keyword.IsKind(SyntaxKind.VoidKeyword)) + { + return SyntaxFactory.ParseTypeName("Task") + .WithLeadingTrivia(returnType.GetLeadingTrivia()) + .WithTrailingTrivia(returnType.GetTrailingTrivia()); + } + + // T -> Task + var innerType = returnType.WithoutTrivia(); + return SyntaxFactory.GenericName("Task") + .WithTypeArgumentList( + SyntaxFactory.TypeArgumentList( + SyntaxFactory.SingletonSeparatedList(innerType))) + .WithLeadingTrivia(returnType.GetLeadingTrivia()) + .WithTrailingTrivia(returnType.GetTrailingTrivia()); + } + + private static SyntaxTokenList InsertAsyncModifier(SyntaxTokenList modifiers) + { + // Find the right position for async (after public/private/etc, before static/virtual/etc) + int insertIndex = 0; + + for (int i = 0; i < modifiers.Count; i++) + { + var modifier = modifiers[i]; + if (modifier.IsKind(SyntaxKind.PublicKeyword) || + modifier.IsKind(SyntaxKind.PrivateKeyword) || + modifier.IsKind(SyntaxKind.ProtectedKeyword) || + modifier.IsKind(SyntaxKind.InternalKeyword)) + { + insertIndex = i + 1; + } + } + + var asyncModifier = SyntaxFactory.Token(SyntaxKind.AsyncKeyword) + .WithTrailingTrivia(SyntaxFactory.Space); + + return modifiers.Insert(insertIndex, asyncModifier); + } +} diff --git a/TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs index 5272c1796c..c76aaede50 100644 --- a/TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs +++ b/TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs @@ -57,6 +57,10 @@ protected async Task ConvertCodeAsync(Document document, SyntaxNode? r // Framework-specific conversions (also use semantic model while it still matches) compilationUnit = ApplyFrameworkSpecificConversions(compilationUnit, semanticModel, compilation); + // Fix method signatures that now contain await but aren't marked async + var asyncSignatureRewriter = new AsyncMethodSignatureRewriter(); + compilationUnit = (CompilationUnitSyntax)asyncSignatureRewriter.Visit(compilationUnit); + // Remove unnecessary base classes and interfaces var baseTypeRewriter = CreateBaseTypeRewriter(semanticModel, compilation); compilationUnit = (CompilationUnitSyntax)baseTypeRewriter.Visit(compilationUnit); @@ -69,6 +73,13 @@ protected async Task ConvertCodeAsync(Document document, SyntaxNode? r var attributeRewriter = CreateAttributeRewriter(compilation); compilationUnit = (CompilationUnitSyntax)attributeRewriter.Visit(compilationUnit); + // Ensure [Test] attribute is present when data attributes exist (NUnit-specific) + if (ShouldEnsureTestAttribute()) + { + var testAttributeEnsurer = new TestAttributeEnsurer(); + compilationUnit = (CompilationUnitSyntax)testAttributeEnsurer.Visit(compilationUnit); + } + // Remove framework usings and add TUnit usings (do this LAST) compilationUnit = MigrationHelpers.RemoveFrameworkUsings(compilationUnit, FrameworkName); @@ -106,6 +117,13 @@ protected async Task ConvertCodeAsync(Document document, SyntaxNode? r /// protected virtual bool ShouldAddTUnitUsings() => true; + /// + /// Determines whether to run TestAttributeEnsurer to add [Test] when data attributes exist. + /// Override to return true for NUnit (where [TestCase] alone is valid but TUnit requires [Test] + [Arguments]). + /// Default is false since most frameworks don't need this. + /// + protected virtual bool ShouldEnsureTestAttribute() => false; + /// /// Removes excessive blank lines at the start of class members (after opening brace). /// This can occur after removing members like ITestOutputHelper fields/properties. diff --git a/TUnit.Analyzers.CodeFixers/Base/TestAttributeEnsurer.cs b/TUnit.Analyzers.CodeFixers/Base/TestAttributeEnsurer.cs new file mode 100644 index 0000000000..ba0c546e87 --- /dev/null +++ b/TUnit.Analyzers.CodeFixers/Base/TestAttributeEnsurer.cs @@ -0,0 +1,136 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TUnit.Analyzers.CodeFixers.Base; + +/// +/// Ensures that methods with data attributes (like [Arguments]) also have a [Test] attribute. +/// This handles the case where NUnit allows [TestCase] alone, but TUnit requires [Test] + [Arguments]. +/// +public class TestAttributeEnsurer : CSharpSyntaxRewriter +{ + private static readonly string[] DataAttributeNames = + [ + "Arguments", + "MethodDataSource", + "ClassDataSource", + "MatrixDataSource" + ]; + + public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) + { + // First, visit children + node = (MethodDeclarationSyntax)base.VisitMethodDeclaration(node)!; + + // Check if method has any data attributes + bool hasDataAttribute = HasAnyDataAttribute(node); + if (!hasDataAttribute) + { + return node; + } + + // Check if method already has [Test] attribute + bool hasTestAttribute = HasTestAttribute(node); + if (hasTestAttribute) + { + return node; + } + + // Add [Test] attribute before the first attribute list + return AddTestAttribute(node); + } + + private static bool HasAnyDataAttribute(MethodDeclarationSyntax method) + { + foreach (var attributeList in method.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { + var name = GetAttributeName(attribute); + if (DataAttributeNames.Contains(name)) + { + return true; + } + } + } + return false; + } + + private static bool HasTestAttribute(MethodDeclarationSyntax method) + { + foreach (var attributeList in method.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { + var name = GetAttributeName(attribute); + if (name == "Test") + { + return true; + } + } + } + return false; + } + + private static string GetAttributeName(AttributeSyntax attribute) + { + var name = attribute.Name.ToString(); + + // Remove "Attribute" suffix if present + if (name.EndsWith("Attribute")) + { + name = name[..^9]; + } + + // Handle fully qualified names (take the last part) + var lastDot = name.LastIndexOf('.'); + if (lastDot >= 0) + { + name = name[(lastDot + 1)..]; + } + + return name; + } + + private static MethodDeclarationSyntax AddTestAttribute(MethodDeclarationSyntax method) + { + if (method.AttributeLists.Count == 0) + { + // No existing attributes - add [Test] with proper formatting + var testAttribute = SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("Test")); + var testAttributeList = SyntaxFactory.AttributeList( + SyntaxFactory.SingletonSeparatedList(testAttribute)) + .WithTrailingTrivia(SyntaxFactory.EndOfLine("\n")); + + return method.WithAttributeLists( + SyntaxFactory.SingletonList(testAttributeList)); + } + + // Get the leading trivia (indentation) from the first attribute list + var firstAttributeList = method.AttributeLists[0]; + var leadingTrivia = firstAttributeList.GetLeadingTrivia(); + + // Create [Test] attribute list with same indentation + var testAttr = SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("Test")); + var newTestAttrList = SyntaxFactory.AttributeList( + SyntaxFactory.SingletonSeparatedList(testAttr)) + .WithLeadingTrivia(leadingTrivia) + .WithTrailingTrivia(SyntaxFactory.EndOfLine("\n")); + + // Strip newlines from first attribute's leading trivia, keep only indentation + // This prevents double newlines when we insert [Test] before it + var strippedTrivia = firstAttributeList.GetLeadingTrivia() + .Where(t => !t.IsKind(SyntaxKind.EndOfLineTrivia)) + .ToList(); + var updatedFirstAttr = firstAttributeList.WithLeadingTrivia(strippedTrivia); + + // Build new attribute list: [Test], then updated first attr (with stripped trivia), then rest + var newAttributeLists = new SyntaxList() + .Add(newTestAttrList) + .Add(updatedFirstAttr) + .AddRange(method.AttributeLists.Skip(1)); + + return method.WithAttributeLists(newAttributeLists); + } +} diff --git a/TUnit.Analyzers.CodeFixers/MSTestMigrationCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/MSTestMigrationCodeFixProvider.cs index 72a131101e..508048417d 100644 --- a/TUnit.Analyzers.CodeFixers/MSTestMigrationCodeFixProvider.cs +++ b/TUnit.Analyzers.CodeFixers/MSTestMigrationCodeFixProvider.cs @@ -217,83 +217,260 @@ protected override bool IsFrameworkAssertionNamespace(string namespaceName) private ExpressionSyntax? ConvertMSTestAssertion(InvocationExpressionSyntax invocation, string methodName) { var arguments = invocation.ArgumentList.Arguments; - + + // MSTest assertion message parameter positions: + // - 2-arg assertions (IsTrue, IsFalse, IsNull, IsNotNull): message is 2nd param (index 1) + // - 3-arg assertions (AreEqual, AreSame, etc.): message is 3rd param (index 2) + return methodName switch { - "AreEqual" when arguments.Count >= 2 => + // 2-arg assertions with message as 3rd param + "AreEqual" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("IsEqualTo", arguments[1].Expression, arguments[2].Expression, arguments[0]), + "AreEqual" when arguments.Count >= 2 => CreateTUnitAssertion("IsEqualTo", arguments[1].Expression, arguments[0]), - "AreNotEqual" when arguments.Count >= 2 => + "AreNotEqual" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("IsNotEqualTo", arguments[1].Expression, arguments[2].Expression, arguments[0]), + "AreNotEqual" when arguments.Count >= 2 => CreateTUnitAssertion("IsNotEqualTo", arguments[1].Expression, arguments[0]), - "IsTrue" when arguments.Count >= 1 => + "AreSame" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("IsSameReference", arguments[1].Expression, arguments[2].Expression, arguments[0]), + "AreSame" when arguments.Count >= 2 => + CreateTUnitAssertion("IsSameReference", arguments[1].Expression, arguments[0]), + "AreNotSame" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("IsNotSameReference", arguments[1].Expression, arguments[2].Expression, arguments[0]), + "AreNotSame" when arguments.Count >= 2 => + CreateTUnitAssertion("IsNotSameReference", arguments[1].Expression, arguments[0]), + + // 1-arg assertions with message as 2nd param + "IsTrue" when arguments.Count >= 2 => + CreateTUnitAssertionWithMessage("IsTrue", arguments[0].Expression, arguments[1].Expression), + "IsTrue" when arguments.Count >= 1 => CreateTUnitAssertion("IsTrue", arguments[0].Expression), - "IsFalse" when arguments.Count >= 1 => + "IsFalse" when arguments.Count >= 2 => + CreateTUnitAssertionWithMessage("IsFalse", arguments[0].Expression, arguments[1].Expression), + "IsFalse" when arguments.Count >= 1 => CreateTUnitAssertion("IsFalse", arguments[0].Expression), - "IsNull" when arguments.Count >= 1 => + "IsNull" when arguments.Count >= 2 => + CreateTUnitAssertionWithMessage("IsNull", arguments[0].Expression, arguments[1].Expression), + "IsNull" when arguments.Count >= 1 => CreateTUnitAssertion("IsNull", arguments[0].Expression), - "IsNotNull" when arguments.Count >= 1 => + "IsNotNull" when arguments.Count >= 2 => + CreateTUnitAssertionWithMessage("IsNotNull", arguments[0].Expression, arguments[1].Expression), + "IsNotNull" when arguments.Count >= 1 => CreateTUnitAssertion("IsNotNull", arguments[0].Expression), - "AreSame" when arguments.Count >= 2 => - CreateTUnitAssertion("IsSameReference", arguments[1].Expression, arguments[0]), - "AreNotSame" when arguments.Count >= 2 => - CreateTUnitAssertion("IsNotSameReference", arguments[1].Expression, arguments[0]), - "IsInstanceOfType" when arguments.Count >= 2 => + + // Type assertions with message as 3rd param + "IsInstanceOfType" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("IsAssignableTo", arguments[0].Expression, arguments[2].Expression, arguments[1]), + "IsInstanceOfType" when arguments.Count >= 2 => CreateTUnitAssertion("IsAssignableTo", arguments[0].Expression, arguments[1]), - "IsNotInstanceOfType" when arguments.Count >= 2 => + "IsNotInstanceOfType" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("IsNotAssignableTo", arguments[0].Expression, arguments[2].Expression, arguments[1]), + "IsNotInstanceOfType" when arguments.Count >= 2 => CreateTUnitAssertion("IsNotAssignableTo", arguments[0].Expression, arguments[1]), + + // Special assertions "ThrowsException" when arguments.Count >= 1 => CreateThrowsAssertion(invocation), "ThrowsExceptionAsync" when arguments.Count >= 1 => CreateThrowsAsyncAssertion(invocation), "Fail" => CreateFailAssertion(arguments), + "Inconclusive" => CreateInconclusiveAssertion(arguments), _ => null }; } + + private ExpressionSyntax CreateInconclusiveAssertion(SeparatedSyntaxList arguments) + { + // Convert Assert.Inconclusive(message) to await Assert.Skip(message) + var skipInvocation = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("Assert"), + SyntaxFactory.IdentifierName("Skip") + ), + arguments.Count > 0 + ? SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(arguments[0])) + : SyntaxFactory.ArgumentList() + ); + + return SyntaxFactory.AwaitExpression(skipInvocation); + } private ExpressionSyntax? ConvertCollectionAssertion(InvocationExpressionSyntax invocation, string methodName) { var arguments = invocation.ArgumentList.Arguments; - + + // CollectionAssert message is typically the last parameter after the required args + return methodName switch { - "AreEqual" when arguments.Count >= 2 => + // Equality assertions + "AreEqual" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("IsEquivalentTo", arguments[1].Expression, arguments[2].Expression, arguments[0]), + "AreEqual" when arguments.Count >= 2 => + CreateTUnitAssertion("IsEquivalentTo", arguments[1].Expression, arguments[0]), + "AreNotEqual" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("IsNotEquivalentTo", arguments[1].Expression, arguments[2].Expression, arguments[0]), + "AreNotEqual" when arguments.Count >= 2 => + CreateTUnitAssertion("IsNotEquivalentTo", arguments[1].Expression, arguments[0]), + + // AreEquivalent (order independent) + "AreEquivalent" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("IsEquivalentTo", arguments[1].Expression, arguments[2].Expression, arguments[0]), + "AreEquivalent" when arguments.Count >= 2 => CreateTUnitAssertion("IsEquivalentTo", arguments[1].Expression, arguments[0]), - "AreNotEqual" when arguments.Count >= 2 => + "AreNotEquivalent" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("IsNotEquivalentTo", arguments[1].Expression, arguments[2].Expression, arguments[0]), + "AreNotEquivalent" when arguments.Count >= 2 => CreateTUnitAssertion("IsNotEquivalentTo", arguments[1].Expression, arguments[0]), - "Contains" when arguments.Count >= 2 => + + // Contains assertions + "Contains" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("Contains", arguments[0].Expression, arguments[2].Expression, arguments[1]), + "Contains" when arguments.Count >= 2 => CreateTUnitAssertion("Contains", arguments[0].Expression, arguments[1]), - "DoesNotContain" when arguments.Count >= 2 => + "DoesNotContain" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("DoesNotContain", arguments[0].Expression, arguments[2].Expression, arguments[1]), + "DoesNotContain" when arguments.Count >= 2 => CreateTUnitAssertion("DoesNotContain", arguments[0].Expression, arguments[1]), - "AllItemsAreNotNull" when arguments.Count >= 1 => - CreateTUnitAssertion("AllSatisfy", arguments[0].Expression, - SyntaxFactory.Argument( - SyntaxFactory.SimpleLambdaExpression( - SyntaxFactory.Parameter(SyntaxFactory.Identifier("x")), - SyntaxFactory.BinaryExpression( - SyntaxKind.NotEqualsExpression, - SyntaxFactory.IdentifierName("x"), - SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression) - ) - ) - )), + + // Subset/Superset + "IsSubsetOf" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("IsSubsetOf", arguments[0].Expression, arguments[2].Expression, arguments[1]), + "IsSubsetOf" when arguments.Count >= 2 => + CreateTUnitAssertion("IsSubsetOf", arguments[0].Expression, arguments[1]), + "IsNotSubsetOf" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("IsNotSubsetOf", arguments[0].Expression, arguments[2].Expression, arguments[1]), + "IsNotSubsetOf" when arguments.Count >= 2 => + CreateTUnitAssertion("IsNotSubsetOf", arguments[0].Expression, arguments[1]), + + // Unique items + "AllItemsAreUnique" when arguments.Count >= 2 => + CreateTUnitAssertionWithMessage("HasDistinctItems", arguments[0].Expression, arguments[1].Expression), + "AllItemsAreUnique" when arguments.Count >= 1 => + CreateTUnitAssertion("HasDistinctItems", arguments[0].Expression), + + // AllItemsAreNotNull + "AllItemsAreNotNull" when arguments.Count >= 2 => + CreateAllItemsAreNotNullWithMessage(arguments[0].Expression, arguments[1].Expression), + "AllItemsAreNotNull" when arguments.Count >= 1 => + CreateTUnitAssertion("AllSatisfy", arguments[0].Expression, + SyntaxFactory.Argument(CreateNotNullLambda())), + + // AllItemsAreInstancesOfType + "AllItemsAreInstancesOfType" when arguments.Count >= 3 => + CreateAllItemsAreInstancesOfTypeWithMessage(arguments[0].Expression, arguments[1].Expression, arguments[2].Expression), + "AllItemsAreInstancesOfType" when arguments.Count >= 2 => + CreateAllItemsAreInstancesOfType(arguments[0].Expression, arguments[1].Expression), + _ => null }; } + + private ExpressionSyntax CreateAllItemsAreNotNullWithMessage(ExpressionSyntax collection, ExpressionSyntax message) + { + return CreateTUnitAssertionWithMessage("AllSatisfy", collection, message, + SyntaxFactory.Argument(CreateNotNullLambda())); + } + + private static ExpressionSyntax CreateNotNullLambda() + { + return SyntaxFactory.SimpleLambdaExpression( + SyntaxFactory.Parameter(SyntaxFactory.Identifier("x")), + SyntaxFactory.BinaryExpression( + SyntaxKind.NotEqualsExpression, + SyntaxFactory.IdentifierName("x"), + SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression) + ) + ); + } + + private ExpressionSyntax CreateAllItemsAreInstancesOfType(ExpressionSyntax collection, ExpressionSyntax expectedType) + { + // Create a lambda: x => x.GetType() == expectedType or x is Type + var isExpression = SyntaxFactory.IsPatternExpression( + SyntaxFactory.IdentifierName("x"), + SyntaxFactory.DeclarationPattern( + SyntaxFactory.IdentifierName("_"), + SyntaxFactory.SingleVariableDesignation(SyntaxFactory.Identifier("_")) + ) + ); + + // Simpler approach: use AllSatisfy with type check + // Since we have a Type argument, we'll create a comment explaining manual conversion needed + var result = CreateTUnitAssertion("AllSatisfy", collection, + SyntaxFactory.Argument( + SyntaxFactory.SimpleLambdaExpression( + SyntaxFactory.Parameter(SyntaxFactory.Identifier("x")), + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + expectedType, + SyntaxFactory.IdentifierName("IsInstanceOfType") + ), + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("x")) + ) + ) + ) + ) + )); + return result; + } + + private ExpressionSyntax CreateAllItemsAreInstancesOfTypeWithMessage(ExpressionSyntax collection, ExpressionSyntax expectedType, ExpressionSyntax message) + { + var result = CreateTUnitAssertionWithMessage("AllSatisfy", collection, message, + SyntaxFactory.Argument( + SyntaxFactory.SimpleLambdaExpression( + SyntaxFactory.Parameter(SyntaxFactory.Identifier("x")), + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + expectedType, + SyntaxFactory.IdentifierName("IsInstanceOfType") + ), + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("x")) + ) + ) + ) + ) + )); + return result; + } private ExpressionSyntax? ConvertStringAssertion(InvocationExpressionSyntax invocation, string methodName) { var arguments = invocation.ArgumentList.Arguments; - + + // StringAssert message is typically the last parameter after the required args + return methodName switch { - "Contains" when arguments.Count >= 2 => + "Contains" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("Contains", arguments[0].Expression, arguments[2].Expression, arguments[1]), + "Contains" when arguments.Count >= 2 => CreateTUnitAssertion("Contains", arguments[0].Expression, arguments[1]), - "DoesNotMatch" when arguments.Count >= 2 => + "DoesNotMatch" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("DoesNotMatch", arguments[0].Expression, arguments[2].Expression, arguments[1]), + "DoesNotMatch" when arguments.Count >= 2 => CreateTUnitAssertion("DoesNotMatch", arguments[0].Expression, arguments[1]), - "EndsWith" when arguments.Count >= 2 => + "EndsWith" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("EndsWith", arguments[0].Expression, arguments[2].Expression, arguments[1]), + "EndsWith" when arguments.Count >= 2 => CreateTUnitAssertion("EndsWith", arguments[0].Expression, arguments[1]), - "Matches" when arguments.Count >= 2 => + "Matches" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("Matches", arguments[0].Expression, arguments[2].Expression, arguments[1]), + "Matches" when arguments.Count >= 2 => CreateTUnitAssertion("Matches", arguments[0].Expression, arguments[1]), - "StartsWith" when arguments.Count >= 2 => + "StartsWith" when arguments.Count >= 3 => + CreateTUnitAssertionWithMessage("StartsWith", arguments[0].Expression, arguments[2].Expression, arguments[1]), + "StartsWith" when arguments.Count >= 2 => CreateTUnitAssertion("StartsWith", arguments[0].Expression, arguments[1]), _ => null }; diff --git a/TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs index 9324db3cbb..f96f49210f 100644 --- a/TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs +++ b/TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs @@ -43,6 +43,11 @@ protected override CompilationUnitSyntax ApplyFrameworkSpecificConversions(Compi return compilationUnit; } + + /// + /// NUnit allows [TestCase] alone, but TUnit requires [Test] + [Arguments]. + /// + protected override bool ShouldEnsureTestAttribute() => true; } public class NUnitAttributeRewriter : AttributeRewriter @@ -158,27 +163,170 @@ private ExpressionSyntax ConvertAssertThat(InvocationExpressionSyntax invocation var arguments = invocation.ArgumentList.Arguments; var actualValue = arguments[0].Expression; var constraint = arguments[1].Expression; - + + // Capture the optional message argument (3rd argument) + ExpressionSyntax? message = null; + if (arguments.Count >= 3) + { + message = arguments[2].Expression; + } + // Parse the constraint to determine the TUnit assertion method if (constraint is InvocationExpressionSyntax constraintInvocation) { - return ConvertConstraintToTUnit(actualValue, constraintInvocation); + return ConvertConstraintToTUnitWithMessage(actualValue, constraintInvocation, message); } - + if (constraint is MemberAccessExpressionSyntax constraintMember) { - return ConvertConstraintMemberToTUnit(actualValue, constraintMember); + return ConvertConstraintMemberToTUnitWithMessage(actualValue, constraintMember, message); } - - return CreateTUnitAssertion("IsEqualTo", actualValue, SyntaxFactory.Argument(constraint)); + + return CreateTUnitAssertionWithMessage("IsEqualTo", actualValue, message, SyntaxFactory.Argument(constraint)); } - + + private ExpressionSyntax ConvertConstraintToTUnitWithMessage(ExpressionSyntax actualValue, InvocationExpressionSyntax constraint, ExpressionSyntax? message) + { + if (constraint.Expression is MemberAccessExpressionSyntax memberAccess) + { + var methodName = memberAccess.Name.Identifier.Text; + + // Handle Is.Not.EqualTo, Is.Not.GreaterThan, etc. (invocation patterns) + if (memberAccess.Expression is MemberAccessExpressionSyntax chainedAccess && + chainedAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Is" } && + chainedAccess.Name.Identifier.Text == "Not") + { + return methodName switch + { + "EqualTo" => CreateTUnitAssertionWithMessage("IsNotEqualTo", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "GreaterThan" => CreateTUnitAssertionWithMessage("IsLessThanOrEqualTo", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "LessThan" => CreateTUnitAssertionWithMessage("IsGreaterThanOrEqualTo", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "GreaterThanOrEqualTo" => CreateTUnitAssertionWithMessage("IsLessThan", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "LessThanOrEqualTo" => CreateTUnitAssertionWithMessage("IsGreaterThan", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "SameAs" => CreateTUnitAssertionWithMessage("IsNotSameReferenceAs", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "InstanceOf" => CreateTUnitAssertionWithMessage("IsNotTypeOf", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + _ => CreateTUnitAssertionWithMessage("IsNotEqualTo", actualValue, message, constraint.ArgumentList.Arguments.ToArray()) + }; + } + + // Handle Does.Not.StartWith, Does.Not.EndWith, Does.Not.Contain + if (memberAccess.Expression is MemberAccessExpressionSyntax doesNotAccess && + doesNotAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Does" } && + doesNotAccess.Name.Identifier.Text == "Not") + { + return methodName switch + { + "StartWith" => CreateTUnitAssertionWithMessage("DoesNotStartWith", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "EndWith" => CreateTUnitAssertionWithMessage("DoesNotEndWith", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "Contain" => CreateTUnitAssertionWithMessage("DoesNotContain", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + _ => CreateTUnitAssertionWithMessage("DoesNotContain", actualValue, message, constraint.ArgumentList.Arguments.ToArray()) + }; + } + + // Handle Does.StartWith, Does.EndWith, Contains.Substring + if (memberAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Does" or "Contains" }) + { + return methodName switch + { + "StartWith" => CreateTUnitAssertionWithMessage("StartsWith", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "EndWith" => CreateTUnitAssertionWithMessage("EndsWith", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "Substring" => CreateTUnitAssertionWithMessage("Contains", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + _ => CreateTUnitAssertionWithMessage("IsEqualTo", actualValue, message, SyntaxFactory.Argument(constraint)) + }; + } + + return methodName switch + { + "EqualTo" => CreateTUnitAssertionWithMessage("IsEqualTo", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "GreaterThan" => CreateTUnitAssertionWithMessage("IsGreaterThan", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "LessThan" => CreateTUnitAssertionWithMessage("IsLessThan", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "GreaterThanOrEqualTo" => CreateTUnitAssertionWithMessage("IsGreaterThanOrEqualTo", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "LessThanOrEqualTo" => CreateTUnitAssertionWithMessage("IsLessThanOrEqualTo", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "Contains" => CreateTUnitAssertionWithMessage("Contains", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "StartsWith" => CreateTUnitAssertionWithMessage("StartsWith", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "EndsWith" => CreateTUnitAssertionWithMessage("EndsWith", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "SameAs" => CreateTUnitAssertionWithMessage("IsSameReferenceAs", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + "InstanceOf" => CreateTUnitAssertionWithMessage("IsTypeOf", actualValue, message, constraint.ArgumentList.Arguments.ToArray()), + _ => CreateTUnitAssertionWithMessage("IsEqualTo", actualValue, message, SyntaxFactory.Argument(constraint)) + }; + } + + return CreateTUnitAssertionWithMessage("IsEqualTo", actualValue, message, SyntaxFactory.Argument(constraint)); + } + + private ExpressionSyntax ConvertConstraintMemberToTUnitWithMessage(ExpressionSyntax actualValue, MemberAccessExpressionSyntax constraint, ExpressionSyntax? message) + { + var memberName = constraint.Name.Identifier.Text; + + // Handle Is.Not.X patterns (member access, not invocation) + if (constraint.Expression is MemberAccessExpressionSyntax innerMemberAccess && + innerMemberAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Is" } && + innerMemberAccess.Name.Identifier.Text == "Not") + { + return memberName switch + { + "Null" => CreateTUnitAssertionWithMessage("IsNotNull", actualValue, message), + "Empty" => CreateTUnitAssertionWithMessage("IsNotEmpty", actualValue, message), + "True" => CreateTUnitAssertionWithMessage("IsFalse", actualValue, message), + "False" => CreateTUnitAssertionWithMessage("IsTrue", actualValue, message), + "Positive" => CreateTUnitAssertionWithMessage("IsLessThanOrEqualTo", actualValue, message, SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0)))), + "Negative" => CreateTUnitAssertionWithMessage("IsGreaterThanOrEqualTo", actualValue, message, SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0)))), + "Zero" => CreateTUnitAssertionWithMessage("IsNotEqualTo", actualValue, message, SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0)))), + _ => CreateTUnitAssertionWithMessage("IsNotEqualTo", actualValue, message, SyntaxFactory.Argument(constraint)) + }; + } + + return memberName switch + { + "True" => CreateTUnitAssertionWithMessage("IsTrue", actualValue, message), + "False" => CreateTUnitAssertionWithMessage("IsFalse", actualValue, message), + "Null" => CreateTUnitAssertionWithMessage("IsNull", actualValue, message), + "Empty" => CreateTUnitAssertionWithMessage("IsEmpty", actualValue, message), + "Positive" => CreateTUnitAssertionWithMessage("IsPositive", actualValue, message), + "Negative" => CreateTUnitAssertionWithMessage("IsNegative", actualValue, message), + "Zero" => CreateTUnitAssertionWithMessage("IsZero", actualValue, message), + _ => CreateTUnitAssertionWithMessage("IsEqualTo", actualValue, message, SyntaxFactory.Argument(constraint)) + }; + } + private ExpressionSyntax ConvertConstraintToTUnit(ExpressionSyntax actualValue, InvocationExpressionSyntax constraint) { if (constraint.Expression is MemberAccessExpressionSyntax memberAccess) { var methodName = memberAccess.Name.Identifier.Text; + // Handle Is.Not.EqualTo, Is.Not.GreaterThan, etc. (invocation patterns) + if (memberAccess.Expression is MemberAccessExpressionSyntax chainedAccess && + chainedAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Is" } && + chainedAccess.Name.Identifier.Text == "Not") + { + return methodName switch + { + "EqualTo" => CreateTUnitAssertion("IsNotEqualTo", actualValue, constraint.ArgumentList.Arguments.ToArray()), + "GreaterThan" => CreateTUnitAssertion("IsLessThanOrEqualTo", actualValue, constraint.ArgumentList.Arguments.ToArray()), + "LessThan" => CreateTUnitAssertion("IsGreaterThanOrEqualTo", actualValue, constraint.ArgumentList.Arguments.ToArray()), + "GreaterThanOrEqualTo" => CreateTUnitAssertion("IsLessThan", actualValue, constraint.ArgumentList.Arguments.ToArray()), + "LessThanOrEqualTo" => CreateTUnitAssertion("IsGreaterThan", actualValue, constraint.ArgumentList.Arguments.ToArray()), + "SameAs" => CreateTUnitAssertion("IsNotSameReferenceAs", actualValue, constraint.ArgumentList.Arguments.ToArray()), + "InstanceOf" => CreateTUnitAssertion("IsNotTypeOf", actualValue, constraint.ArgumentList.Arguments.ToArray()), + _ => CreateTUnitAssertion("IsNotEqualTo", actualValue, constraint.ArgumentList.Arguments.ToArray()) + }; + } + + // Handle Does.Not.StartWith, Does.Not.EndWith, Does.Not.Contain + if (memberAccess.Expression is MemberAccessExpressionSyntax doesNotAccess && + doesNotAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Does" } && + doesNotAccess.Name.Identifier.Text == "Not") + { + return methodName switch + { + "StartWith" => CreateTUnitAssertion("DoesNotStartWith", actualValue, constraint.ArgumentList.Arguments.ToArray()), + "EndWith" => CreateTUnitAssertion("DoesNotEndWith", actualValue, constraint.ArgumentList.Arguments.ToArray()), + "Contain" => CreateTUnitAssertion("DoesNotContain", actualValue, constraint.ArgumentList.Arguments.ToArray()), + _ => CreateTUnitAssertion("DoesNotContain", actualValue, constraint.ArgumentList.Arguments.ToArray()) + }; + } + // Handle Does.StartWith, Does.EndWith, Contains.Substring if (memberAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Does" or "Contains" }) { @@ -196,9 +344,18 @@ private ExpressionSyntax ConvertConstraintToTUnit(ExpressionSyntax actualValue, "EqualTo" => CreateTUnitAssertion("IsEqualTo", actualValue, constraint.ArgumentList.Arguments.ToArray()), "GreaterThan" => CreateTUnitAssertion("IsGreaterThan", actualValue, constraint.ArgumentList.Arguments.ToArray()), "LessThan" => CreateTUnitAssertion("IsLessThan", actualValue, constraint.ArgumentList.Arguments.ToArray()), + "GreaterThanOrEqualTo" => CreateTUnitAssertion("IsGreaterThanOrEqualTo", actualValue, constraint.ArgumentList.Arguments.ToArray()), + "LessThanOrEqualTo" => CreateTUnitAssertion("IsLessThanOrEqualTo", actualValue, constraint.ArgumentList.Arguments.ToArray()), "Contains" => CreateTUnitAssertion("Contains", actualValue, constraint.ArgumentList.Arguments.ToArray()), "StartsWith" => CreateTUnitAssertion("StartsWith", actualValue, constraint.ArgumentList.Arguments.ToArray()), "EndsWith" => CreateTUnitAssertion("EndsWith", actualValue, constraint.ArgumentList.Arguments.ToArray()), + "SameAs" => CreateTUnitAssertion("IsSameReferenceAs", actualValue, constraint.ArgumentList.Arguments.ToArray()), + "InstanceOf" => CreateTUnitAssertion("IsTypeOf", actualValue, constraint.ArgumentList.Arguments.ToArray()), + "SubsetOf" => CreateTUnitAssertion("IsSubsetOf", actualValue, constraint.ArgumentList.Arguments.ToArray()), + "SupersetOf" => CreateTUnitAssertion("IsSupersetOf", actualValue, constraint.ArgumentList.Arguments.ToArray()), + "EquivalentTo" => CreateTUnitAssertion("IsEquivalentTo", actualValue, constraint.ArgumentList.Arguments.ToArray()), + "Matches" => CreateTUnitAssertion("Matches", actualValue, constraint.ArgumentList.Arguments.ToArray()), + "InRange" => CreateInRangeAssertion(actualValue, constraint.ArgumentList.Arguments), _ => CreateTUnitAssertion("IsEqualTo", actualValue, SyntaxFactory.Argument(constraint)) }; } @@ -210,7 +367,7 @@ private ExpressionSyntax ConvertConstraintMemberToTUnit(ExpressionSyntax actualV { var memberName = constraint.Name.Identifier.Text; - // Handle Is.Not.X patterns + // Handle Is.Not.X patterns (member access, not invocation) if (constraint.Expression is MemberAccessExpressionSyntax innerMemberAccess && innerMemberAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Is" } && innerMemberAccess.Name.Identifier.Text == "Not") @@ -219,7 +376,12 @@ private ExpressionSyntax ConvertConstraintMemberToTUnit(ExpressionSyntax actualV { "Null" => CreateTUnitAssertion("IsNotNull", actualValue), "Empty" => CreateTUnitAssertion("IsNotEmpty", actualValue), - _ => CreateTUnitAssertion("IsEqualTo", actualValue, SyntaxFactory.Argument(constraint)) + "True" => CreateTUnitAssertion("IsFalse", actualValue), + "False" => CreateTUnitAssertion("IsTrue", actualValue), + "Positive" => CreateTUnitAssertion("IsLessThanOrEqualTo", actualValue, SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0)))), + "Negative" => CreateTUnitAssertion("IsGreaterThanOrEqualTo", actualValue, SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0)))), + "Zero" => CreateTUnitAssertion("IsNotEqualTo", actualValue, SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0)))), + _ => CreateTUnitAssertion("IsNotEqualTo", actualValue, SyntaxFactory.Argument(constraint)) }; } @@ -229,39 +391,228 @@ private ExpressionSyntax ConvertConstraintMemberToTUnit(ExpressionSyntax actualV "False" => CreateTUnitAssertion("IsFalse", actualValue), "Null" => CreateTUnitAssertion("IsNull", actualValue), "Empty" => CreateTUnitAssertion("IsEmpty", actualValue), + "Positive" => CreateTUnitAssertion("IsPositive", actualValue), + "Negative" => CreateTUnitAssertion("IsNegative", actualValue), + "Zero" => CreateTUnitAssertion("IsZero", actualValue), + "Unique" => CreateTUnitAssertion("HasDistinctItems", actualValue), + "Ordered" => CreateTUnitAssertion("IsInAscendingOrder", actualValue), _ => CreateTUnitAssertion("IsEqualTo", actualValue, SyntaxFactory.Argument(constraint)) }; } + + private ExpressionSyntax CreateInRangeAssertion(ExpressionSyntax actualValue, SeparatedSyntaxList arguments) + { + // Is.InRange(low, high) -> IsInRange(low, high) + if (arguments.Count >= 2) + { + return CreateTUnitAssertion("IsInRange", actualValue, arguments[0], arguments[1]); + } + return CreateTUnitAssertion("IsInRange", actualValue); + } private ExpressionSyntax? ConvertClassicAssertion(InvocationExpressionSyntax invocation, string methodName) { var arguments = invocation.ArgumentList.Arguments; - + + // Handle Assert.Throws and Assert.ThrowsAsync first (generic methods) + if (methodName is "Throws" or "ThrowsAsync") + { + return ConvertNUnitThrows(invocation); + } + + // Handle special assertions (Pass, Inconclusive, Fail, Warn) return methodName switch { - "AreEqual" when arguments.Count >= 2 => - CreateTUnitAssertion("IsEqualTo", arguments[1].Expression, arguments[0]), - "AreNotEqual" when arguments.Count >= 2 => - CreateTUnitAssertion("IsNotEqualTo", arguments[1].Expression, arguments[0]), - "IsTrue" when arguments.Count >= 1 => - CreateTUnitAssertion("IsTrue", arguments[0].Expression), - "IsFalse" when arguments.Count >= 1 => - CreateTUnitAssertion("IsFalse", arguments[0].Expression), - "IsNull" when arguments.Count >= 1 => - CreateTUnitAssertion("IsNull", arguments[0].Expression), - "IsNotNull" when arguments.Count >= 1 => - CreateTUnitAssertion("IsNotNull", arguments[0].Expression), - "IsEmpty" when arguments.Count >= 1 => - CreateTUnitAssertion("IsEmpty", arguments[0].Expression), - "IsNotEmpty" when arguments.Count >= 1 => - CreateTUnitAssertion("IsNotEmpty", arguments[0].Expression), - "Greater" when arguments.Count >= 2 => - CreateTUnitAssertion("IsGreaterThan", arguments[0].Expression, arguments[1]), - "Less" when arguments.Count >= 2 => - CreateTUnitAssertion("IsLessThan", arguments[0].Expression, arguments[1]), + // Pass and Fail + "Pass" => CreatePassAssertion(arguments), + "Fail" => CreateFailAssertion(arguments), + "Inconclusive" => CreateSkipAssertion(arguments), + "Ignore" => CreateSkipAssertion(arguments), + + // 2-arg assertions (expected, actual) with optional message at index 2 + "AreEqual" when arguments.Count >= 2 => ConvertAreEqualWithComparer(arguments), + "AreNotEqual" when arguments.Count >= 2 => ConvertAreNotEqualWithMessage(arguments), + "AreSame" when arguments.Count >= 2 => ConvertTwoArgWithMessage("IsSameReferenceAs", arguments), + "AreNotSame" when arguments.Count >= 2 => ConvertTwoArgWithMessage("IsNotSameReferenceAs", arguments), + "Greater" when arguments.Count >= 2 => ConvertTwoArgWithMessage("IsGreaterThan", arguments, swapArgs: false), + "GreaterOrEqual" when arguments.Count >= 2 => ConvertTwoArgWithMessage("IsGreaterThanOrEqualTo", arguments, swapArgs: false), + "Less" when arguments.Count >= 2 => ConvertTwoArgWithMessage("IsLessThan", arguments, swapArgs: false), + "LessOrEqual" when arguments.Count >= 2 => ConvertTwoArgWithMessage("IsLessThanOrEqualTo", arguments, swapArgs: false), + + // 1-arg assertions with optional message at index 1 + "IsTrue" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsTrue", arguments), + "IsFalse" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsFalse", arguments), + "IsNull" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsNull", arguments), + "IsNotNull" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsNotNull", arguments), + "IsEmpty" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsEmpty", arguments), + "IsNotEmpty" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsNotEmpty", arguments), + "IsNaN" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsNaN", arguments), + "IsInstanceOf" when arguments.Count >= 2 => ConvertInstanceOf(arguments, isNegated: false), + "IsNotInstanceOf" when arguments.Count >= 2 => ConvertInstanceOf(arguments, isNegated: true), + + // Collection assertions + "Contains" when arguments.Count >= 2 => ConvertTwoArgWithMessage("Contains", arguments, swapArgs: false), + + // Comparison assertions + "Positive" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsPositive", arguments), + "Negative" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsNegative", arguments), + "Zero" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsZero", arguments), + "NotZero" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsNotZero", arguments), + _ => null }; } + + private ExpressionSyntax ConvertOneArgWithMessage(string methodName, SeparatedSyntaxList arguments) + { + var actualValue = arguments[0].Expression; + var (message, formatArgs) = ExtractMessageWithFormatArgs(arguments, 1); + var messageExpr = message != null ? CreateMessageExpression(message, formatArgs) : null; + return CreateTUnitAssertionWithMessage(methodName, actualValue, messageExpr); + } + + private ExpressionSyntax ConvertTwoArgWithMessage(string methodName, SeparatedSyntaxList arguments, bool swapArgs = true) + { + // For most NUnit assertions: expected is first, actual is second + // For TUnit: actual is first, expected goes in the method call + var actualValue = swapArgs ? arguments[1].Expression : arguments[0].Expression; + var expectedArg = swapArgs ? arguments[0] : arguments[1]; + var (message, formatArgs) = ExtractMessageWithFormatArgs(arguments, 2); + var messageExpr = message != null ? CreateMessageExpression(message, formatArgs) : null; + return CreateTUnitAssertionWithMessage(methodName, actualValue, messageExpr, expectedArg); + } + + private ExpressionSyntax ConvertAreEqualWithComparer(SeparatedSyntaxList arguments) + { + var expected = arguments[0]; + var actual = arguments[1].Expression; + + // Check if 3rd argument is a comparer (not a string message) + if (arguments.Count >= 3 && IsLikelyComparerArgument(arguments[2])) + { + // Add TODO comment and skip the comparer + var result = CreateTUnitAssertion("IsEqualTo", actual, expected); + return result.WithLeadingTrivia( + CreateTodoComment("custom comparer was used - consider using Assert.That(...).IsEquivalentTo() or a custom condition."), + SyntaxFactory.EndOfLine("\n"), + SyntaxFactory.Whitespace(" ")); + } + + var (message, formatArgs) = ExtractMessageWithFormatArgs(arguments, 2); + var messageExpr = message != null ? CreateMessageExpression(message, formatArgs) : null; + return CreateTUnitAssertionWithMessage("IsEqualTo", actual, messageExpr, expected); + } + + private ExpressionSyntax ConvertAreNotEqualWithMessage(SeparatedSyntaxList arguments) + { + var expected = arguments[0]; + var actual = arguments[1].Expression; + + // Check if 3rd argument is a comparer + if (arguments.Count >= 3 && IsLikelyComparerArgument(arguments[2])) + { + var result = CreateTUnitAssertion("IsNotEqualTo", actual, expected); + return result.WithLeadingTrivia( + CreateTodoComment("custom comparer was used - consider using a custom condition."), + SyntaxFactory.EndOfLine("\n"), + SyntaxFactory.Whitespace(" ")); + } + + var (message, formatArgs) = ExtractMessageWithFormatArgs(arguments, 2); + var messageExpr = message != null ? CreateMessageExpression(message, formatArgs) : null; + return CreateTUnitAssertionWithMessage("IsNotEqualTo", actual, messageExpr, expected); + } + + private ExpressionSyntax ConvertInstanceOf(SeparatedSyntaxList arguments, bool isNegated) + { + var actualValue = arguments[0].Expression; + var expectedType = arguments[1]; + var methodName = isNegated ? "IsNotAssignableTo" : "IsAssignableTo"; + var (message, formatArgs) = ExtractMessageWithFormatArgs(arguments, 2); + var messageExpr = message != null ? CreateMessageExpression(message, formatArgs) : null; + return CreateTUnitAssertionWithMessage(methodName, actualValue, messageExpr, expectedType); + } + + private ExpressionSyntax ConvertNUnitThrows(InvocationExpressionSyntax invocation) + { + if (invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name is GenericNameSyntax genericName) + { + var exceptionType = genericName.TypeArgumentList.Arguments[0]; + var action = invocation.ArgumentList.Arguments[0].Expression; + + var throwsAsyncInvocation = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("Assert"), + SyntaxFactory.GenericName("ThrowsAsync") + .WithTypeArgumentList( + SyntaxFactory.TypeArgumentList( + SyntaxFactory.SingletonSeparatedList(exceptionType) + ) + ) + ), + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument(action) + ) + ) + ); + + return SyntaxFactory.AwaitExpression(throwsAsyncInvocation); + } + + // Fallback for non-generic Throws + return CreateTUnitAssertion("Throws", invocation.ArgumentList.Arguments[0].Expression); + } + + private ExpressionSyntax CreatePassAssertion(SeparatedSyntaxList arguments) + { + var passInvocation = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("Assert"), + SyntaxFactory.IdentifierName("Pass") + ), + arguments.Count > 0 + ? SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(arguments[0])) + : SyntaxFactory.ArgumentList() + ); + + return SyntaxFactory.AwaitExpression(passInvocation); + } + + private ExpressionSyntax CreateFailAssertion(SeparatedSyntaxList arguments) + { + var failInvocation = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("Assert"), + SyntaxFactory.IdentifierName("Fail") + ), + arguments.Count > 0 + ? SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(arguments[0])) + : SyntaxFactory.ArgumentList() + ); + + return SyntaxFactory.AwaitExpression(failInvocation); + } + + private ExpressionSyntax CreateSkipAssertion(SeparatedSyntaxList arguments) + { + var skipInvocation = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("Assert"), + SyntaxFactory.IdentifierName("Skip") + ), + arguments.Count > 0 + ? SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(arguments[0])) + : SyntaxFactory.ArgumentList() + ); + + return SyntaxFactory.AwaitExpression(skipInvocation); + } } public class NUnitBaseTypeRewriter : CSharpSyntaxRewriter diff --git a/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs index 352417e2d8..02b2be8bdb 100644 --- a/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs +++ b/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs @@ -539,22 +539,39 @@ protected override bool IsFrameworkAssertionNamespace(string namespaceName) return methodName switch { + // Equality assertions - check for comparer overloads + "Equal" when arguments.Count >= 3 && IsLikelyComparerArgument(arguments[2]) => + CreateEqualWithComparerComment(arguments), "Equal" when arguments.Count >= 2 => CreateTUnitAssertion("IsEqualTo", arguments[1].Expression, arguments[0]), + "NotEqual" when arguments.Count >= 3 && IsLikelyComparerArgument(arguments[2]) => + CreateNotEqualWithComparerComment(arguments), "NotEqual" when arguments.Count >= 2 => CreateTUnitAssertion("IsNotEqualTo", arguments[1].Expression, arguments[0]), + + // Boolean assertions + "True" when arguments.Count >= 2 => + CreateTUnitAssertionWithMessage("IsTrue", arguments[0].Expression, arguments[1].Expression), "True" when arguments.Count >= 1 => CreateTUnitAssertion("IsTrue", arguments[0].Expression), + "False" when arguments.Count >= 2 => + CreateTUnitAssertionWithMessage("IsFalse", arguments[0].Expression, arguments[1].Expression), "False" when arguments.Count >= 1 => CreateTUnitAssertion("IsFalse", arguments[0].Expression), + + // Null assertions "Null" when arguments.Count >= 1 => CreateTUnitAssertion("IsNull", arguments[0].Expression), "NotNull" when arguments.Count >= 1 => CreateTUnitAssertion("IsNotNull", arguments[0].Expression), + + // Reference assertions "Same" when arguments.Count >= 2 => CreateTUnitAssertion("IsSameReference", arguments[1].Expression, arguments[0]), "NotSame" when arguments.Count >= 2 => CreateTUnitAssertion("IsNotSameReference", arguments[1].Expression, arguments[0]), + + // String/Collection contains "Contains" when arguments.Count >= 2 => CreateTUnitAssertion("Contains", arguments[1].Expression, arguments[0]), "DoesNotContain" when arguments.Count >= 2 => @@ -563,26 +580,154 @@ protected override bool IsFrameworkAssertionNamespace(string namespaceName) CreateTUnitAssertion("StartsWith", arguments[1].Expression, arguments[0]), "EndsWith" when arguments.Count >= 2 => CreateTUnitAssertion("EndsWith", arguments[1].Expression, arguments[0]), + + // Empty/Not empty "Empty" when arguments.Count >= 1 => CreateTUnitAssertion("IsEmpty", arguments[0].Expression), "NotEmpty" when arguments.Count >= 1 => CreateTUnitAssertion("IsNotEmpty", arguments[0].Expression), - "Throws" => - ConvertThrows(invocation, nameNode), - "ThrowsAsync" => - ConvertThrowsAsync(invocation, nameNode), - "IsType" => - ConvertIsType(invocation, nameNode), - "IsAssignableFrom" => - ConvertIsAssignableFrom(invocation, nameNode), + + // Exception assertions + "Throws" => ConvertThrows(invocation, nameNode), + "ThrowsAsync" => ConvertThrowsAsync(invocation, nameNode), + "ThrowsAny" => ConvertThrowsAny(invocation, nameNode), + "ThrowsAnyAsync" => ConvertThrowsAnyAsync(invocation, nameNode), + + // Type assertions + "IsType" => ConvertIsType(invocation, nameNode), + "IsNotType" => ConvertIsNotType(invocation, nameNode), + "IsAssignableFrom" => ConvertIsAssignableFrom(invocation, nameNode), + + // Range assertions "InRange" when arguments.Count >= 3 => CreateTUnitAssertion("IsInRange", arguments[0].Expression, arguments[1], arguments[2]), "NotInRange" when arguments.Count >= 3 => CreateTUnitAssertion("IsNotInRange", arguments[0].Expression, arguments[1], arguments[2]), + + // Collection assertions + "Single" when arguments.Count >= 1 => + CreateTUnitAssertion("HasSingleItem", arguments[0].Expression), + "All" when arguments.Count >= 2 => + CreateTUnitAssertion("AllSatisfy", arguments[0].Expression, arguments[1]), + + // Subset/superset + "Subset" when arguments.Count >= 2 => + CreateTUnitAssertion("IsSubsetOf", arguments[0].Expression, arguments[1]), + "Superset" when arguments.Count >= 2 => + CreateTUnitAssertion("IsSupersetOf", arguments[0].Expression, arguments[1]), + "ProperSubset" when arguments.Count >= 2 => + CreateTUnitAssertion("IsSubsetOf", arguments[0].Expression, arguments[1]), + "ProperSuperset" when arguments.Count >= 2 => + CreateTUnitAssertion("IsSupersetOf", arguments[0].Expression, arguments[1]), + + // Unique items + "Distinct" when arguments.Count >= 1 => + CreateTUnitAssertion("HasDistinctItems", arguments[0].Expression), + + // Equivalent (order independent) + "Equivalent" when arguments.Count >= 2 => + CreateTUnitAssertion("IsEquivalentTo", arguments[1].Expression, arguments[0]), + _ => null }; } + private ExpressionSyntax CreateEqualWithComparerComment(SeparatedSyntaxList arguments) + { + var result = CreateTUnitAssertion("IsEqualTo", arguments[1].Expression, arguments[0]); + return result.WithLeadingTrivia( + SyntaxFactory.Comment("// TODO: TUnit migration - custom comparer was used. Consider using Assert.That(...).IsEquivalentTo() or a custom condition."), + SyntaxFactory.EndOfLine("\n"), + SyntaxFactory.Whitespace(" ")); + } + + private ExpressionSyntax CreateNotEqualWithComparerComment(SeparatedSyntaxList arguments) + { + var result = CreateTUnitAssertion("IsNotEqualTo", arguments[1].Expression, arguments[0]); + return result.WithLeadingTrivia( + SyntaxFactory.Comment("// TODO: TUnit migration - custom comparer was used. Consider using a custom condition."), + SyntaxFactory.EndOfLine("\n"), + SyntaxFactory.Whitespace(" ")); + } + + private ExpressionSyntax ConvertThrowsAny(InvocationExpressionSyntax invocation, SimpleNameSyntax nameNode) + { + // Assert.ThrowsAny(action) -> await Assert.ThrowsAsync(action) + // Note: ThrowsAny accepts derived types, ThrowsAsync should work similarly + if (nameNode is GenericNameSyntax genericName) + { + var exceptionType = genericName.TypeArgumentList.Arguments[0]; + var action = invocation.ArgumentList.Arguments[0].Expression; + + var invocationExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("Assert"), + SyntaxFactory.GenericName("ThrowsAsync") + .WithTypeArgumentList( + SyntaxFactory.TypeArgumentList( + SyntaxFactory.SingletonSeparatedList(exceptionType) + ) + ) + ), + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument(action) + ) + ) + ); + + return SyntaxFactory.AwaitExpression(invocationExpression); + } + + return CreateTUnitAssertion("Throws", invocation.ArgumentList.Arguments[0].Expression); + } + + private ExpressionSyntax ConvertThrowsAnyAsync(InvocationExpressionSyntax invocation, SimpleNameSyntax nameNode) + { + // Same as ThrowsAny but for async + return ConvertThrowsAny(invocation, nameNode); + } + + private ExpressionSyntax ConvertIsNotType(InvocationExpressionSyntax invocation, SimpleNameSyntax nameNode) + { + // Assert.IsNotType(value) -> await Assert.That(value).IsNotTypeOf() + if (nameNode is GenericNameSyntax genericName) + { + var expectedType = genericName.TypeArgumentList.Arguments[0]; + var value = invocation.ArgumentList.Arguments[0].Expression; + + var assertThatInvocation = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("Assert"), + SyntaxFactory.IdentifierName("That") + ), + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument(value) + ) + ) + ); + + var methodAccess = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + assertThatInvocation, + SyntaxFactory.GenericName("IsNotTypeOf") + .WithTypeArgumentList( + SyntaxFactory.TypeArgumentList( + SyntaxFactory.SingletonSeparatedList(expectedType) + ) + ) + ); + + var fullInvocation = SyntaxFactory.InvocationExpression(methodAccess, SyntaxFactory.ArgumentList()); + return SyntaxFactory.AwaitExpression(fullInvocation); + } + + return CreateTUnitAssertion("IsNotTypeOf", invocation.ArgumentList.Arguments[0].Expression); + } + private ExpressionSyntax ConvertThrows(InvocationExpressionSyntax invocation, SimpleNameSyntax nameNode) { // Assert.Throws(action) -> await Assert.ThrowsAsync(action) diff --git a/TUnit.Analyzers.Tests/MSTestMigrationAnalyzerTests.cs b/TUnit.Analyzers.Tests/MSTestMigrationAnalyzerTests.cs index 4291101719..acdae641ae 100644 --- a/TUnit.Analyzers.Tests/MSTestMigrationAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/MSTestMigrationAnalyzerTests.cs @@ -122,7 +122,7 @@ public void MyMethod() public class MyClass { [Test] - public void MyMethod() + public async Task MyMethod() { await Assert.That(5).IsEqualTo(5); await Assert.That(true).IsTrue(); @@ -293,7 +293,7 @@ public void MyMethod() public class MyClass { [Test] - public void MyMethod() + public async Task MyMethod() { var list1 = new[] { 1, 2, 3 }; var list2 = new[] { 1, 2, 3 }; @@ -334,7 +334,7 @@ public void StringTests() public class MyClass { [Test] - public void StringTests() + public async Task StringTests() { await Assert.That("hello world").Contains("world"); await Assert.That("hello world").StartsWith("hello"); @@ -383,14 +383,14 @@ public class OuterClass public class InnerTests { [Test] - public void InnerTest() + public async Task InnerTest() { await Assert.That(true).IsTrue(); } } [Test] - public void OuterTest() + public async Task OuterTest() { await Assert.That(false).IsFalse(); } @@ -427,7 +427,7 @@ public void GenericTest() public class GenericTestClass { [Test] - public void GenericTest() + public async Task GenericTest() { var instance = default(T); await Assert.That(instance).IsEqualTo(default(T)); @@ -530,7 +530,7 @@ public void Setup() } [Test] - public void Test1() + public async Task Test1() { await Assert.That(_counter > 0).IsTrue(); await Assert.That(_counter).IsNotNull(); @@ -539,7 +539,7 @@ public void Test1() [Arguments(1, 2, 3)] [Arguments(5, 5, 10)] [Test] - public void AdditionTest(int a, int b, int expected) + public async Task AdditionTest(int a, int b, int expected) { var result = a + b; await Assert.That(result).IsEqualTo(expected); @@ -547,7 +547,7 @@ public void AdditionTest(int a, int b, int expected) [MethodDataSource(nameof(GetTestData))] [Test] - public void DataDrivenTest(string input) + public async Task DataDrivenTest(string input) { await Assert.That(input).IsNotNull(); } @@ -616,7 +616,7 @@ public void TestMultipleAssertionTypes() public class MyClass { [Test] - public void TestMultipleAssertionTypes() + public async Task TestMultipleAssertionTypes() { var value = 42; var list = new[] { 1, 2, 3 }; @@ -672,7 +672,7 @@ public void TestReferences() public class MyClass { [Test] - public void TestReferences() + public async Task TestReferences() { var obj1 = new object(); var obj2 = obj1; @@ -687,6 +687,48 @@ public void TestReferences() ); } + [Test] + public async Task MSTest_Assertion_Messages_Preserved() + { + await CodeFixer.VerifyCodeFixAsync( + """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + {|#0:public class MyClass|} + { + [TestMethod] + public void TestWithMessages() + { + Assert.AreEqual(5, 5, "Values should be equal"); + Assert.IsTrue(true, "Should be true"); + Assert.IsNull(null, "Should be null"); + Assert.AreNotEqual(3, 5, "Values should not be equal"); + } + } + """, + Verifier.Diagnostic(Rules.MSTestMigration).WithLocation(0), + """ + using TUnit.Core; + using TUnit.Assertions; + using static TUnit.Assertions.Assert; + using TUnit.Assertions.Extensions; + + public class MyClass + { + [Test] + public async Task TestWithMessages() + { + await Assert.That(5).IsEqualTo(5).Because("Values should be equal"); + await Assert.That(true).IsTrue().Because("Should be true"); + await Assert.That(null).IsNull().Because("Should be null"); + await Assert.That(5).IsNotEqualTo(3).Because("Values should not be equal"); + } + } + """, + ConfigureMSTestTest + ); + } + private static void ConfigureMSTestTest(Verifier.Test test) { test.TestState.AdditionalReferences.Add(typeof(TestMethodAttribute).Assembly); diff --git a/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs b/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs index 9a28a47fc4..ce0a920c8c 100644 --- a/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs @@ -29,12 +29,10 @@ public void MyMethod() { } [Test] [Arguments("NUnit.Framework.Test", "Test")] - [Arguments("NUnit.Framework.TestCase(1, 2, 3)", "Arguments(1, 2, 3)")] [Arguments("NUnit.Framework.SetUp", "Before(HookType.Test)")] [Arguments("NUnit.Framework.TearDown", "After(HookType.Test)")] [Arguments("NUnit.Framework.OneTimeSetUp", "Before(HookType.Class)")] [Arguments("NUnit.Framework.OneTimeTearDown", "After(HookType.Class)")] - [Arguments("NUnit.Framework.TestCaseSource(\"SomeMethod\")", "MethodDataSource(\"SomeMethod\")")] public async Task NUnit_Attribute_Can_Be_Converted(string attribute, string expected) { await CodeFixer.VerifyCodeFixAsync( @@ -123,7 +121,7 @@ public void MyMethod() public class MyClass { [Test] - public void MyMethod() + public async Task MyMethod() { await Assert.That(5).IsEqualTo(5); await Assert.That(true).IsTrue(); @@ -134,7 +132,7 @@ public void MyMethod() ConfigureNUnitTest ); } - + [Test] public async Task NUnit_Classic_Assertions_Converted() { @@ -302,14 +300,14 @@ public class OuterClass public class InnerTests { [Test] - public void InnerTest() + public async Task InnerTest() { await Assert.That(true).IsTrue(); } } [Test] - public void OuterTest() + public async Task OuterTest() { await Assert.That(false).IsFalse(); } @@ -346,7 +344,7 @@ public void GenericTest() public class GenericTestClass { [Test] - public void GenericTest() + public async Task GenericTest() { var instance = default(T); await Assert.That(instance).IsEqualTo(default(T)); @@ -388,7 +386,7 @@ public void ComplexConstraints() public class MyClass { [Test] - public void ComplexConstraints() + public async Task ComplexConstraints() { await Assert.That(10).IsGreaterThan(5); await Assert.That(3).IsLessThan(10); @@ -492,22 +490,24 @@ public void Setup() } [Test] - public void Test1() + public async Task Test1() { await Assert.That(_counter).IsGreaterThan(0); ClassicAssert.IsTrue(true); } + [Test] [Arguments(1, 2, 3)] [Arguments(5, 5, 10)] - public void AdditionTest(int a, int b, int expected) + public async Task AdditionTest(int a, int b, int expected) { var result = a + b; await Assert.That(result).IsEqualTo(expected); } + [Test] [MethodDataSource(nameof(GetTestData))] - public void DataDrivenTest(string input) + public async Task DataDrivenTest(string input) { await Assert.That(input).IsNotNull(); } @@ -566,7 +566,7 @@ public void TestMultipleAssertions() public class MyClass { [Test] - public void TestMultipleAssertions() + public async Task TestMultipleAssertions() { var value = 42; await Assert.That(value).IsNotNull(); @@ -728,6 +728,335 @@ public async Task ToUpper(string input, string expected) ); } + [Test] + public async Task NUnit_IsNotEqualTo_Converted_Correctly() + { + await CodeFixer.VerifyCodeFixAsync( + """ + using NUnit.Framework; + + {|#0:public class MyClass|} + { + [Test] + public void TestMethod() + { + Assert.That(5, Is.Not.EqualTo(10)); + } + } + """, + Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0), + """ + using TUnit.Core; + using TUnit.Assertions; + using static TUnit.Assertions.Assert; + using TUnit.Assertions.Extensions; + + public class MyClass + { + [Test] + public async Task TestMethod() + { + await Assert.That(5).IsNotEqualTo(10); + } + } + """, + ConfigureNUnitTest + ); + } + + [Test] + public async Task NUnit_IsNotGreaterThan_Converted_To_IsLessThanOrEqualTo() + { + await CodeFixer.VerifyCodeFixAsync( + """ + using NUnit.Framework; + + {|#0:public class MyClass|} + { + [Test] + public void TestMethod() + { + Assert.That(5, Is.Not.GreaterThan(10)); + } + } + """, + Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0), + """ + using TUnit.Core; + using TUnit.Assertions; + using static TUnit.Assertions.Assert; + using TUnit.Assertions.Extensions; + + public class MyClass + { + [Test] + public async Task TestMethod() + { + await Assert.That(5).IsLessThanOrEqualTo(10); + } + } + """, + ConfigureNUnitTest + ); + } + + [Test] + public async Task NUnit_DoesNotContain_Converted_Correctly() + { + await CodeFixer.VerifyCodeFixAsync( + """ + using NUnit.Framework; + + {|#0:public class MyClass|} + { + [Test] + public void TestMethod() + { + Assert.That("hello world", Does.Not.Contain("foo")); + } + } + """, + Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0), + """ + using TUnit.Core; + using TUnit.Assertions; + using static TUnit.Assertions.Assert; + using TUnit.Assertions.Extensions; + + public class MyClass + { + [Test] + public async Task TestMethod() + { + await Assert.That("hello world").DoesNotContain("foo"); + } + } + """, + ConfigureNUnitTest + ); + } + + [Test] + public async Task NUnit_DoesNotStartWith_Converted_Correctly() + { + await CodeFixer.VerifyCodeFixAsync( + """ + using NUnit.Framework; + + {|#0:public class MyClass|} + { + [Test] + public void TestMethod() + { + Assert.That("hello world", Does.Not.StartWith("foo")); + } + } + """, + Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0), + """ + using TUnit.Core; + using TUnit.Assertions; + using static TUnit.Assertions.Assert; + using TUnit.Assertions.Extensions; + + public class MyClass + { + [Test] + public async Task TestMethod() + { + await Assert.That("hello world").DoesNotStartWith("foo"); + } + } + """, + ConfigureNUnitTest + ); + } + + [Test] + public async Task NUnit_AssertionMessage_Converted_To_Because() + { + await CodeFixer.VerifyCodeFixAsync( + """ + using NUnit.Framework; + + {|#0:public class MyClass|} + { + [Test] + public void TestMethod() + { + Assert.That(5, Is.EqualTo(5), "Values should match"); + } + } + """, + Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0), + """ + using TUnit.Core; + using TUnit.Assertions; + using static TUnit.Assertions.Assert; + using TUnit.Assertions.Extensions; + + public class MyClass + { + [Test] + public async Task TestMethod() + { + await Assert.That(5).IsEqualTo(5).Because("Values should match"); + } + } + """, + ConfigureNUnitTest + ); + } + + [Test] + public async Task NUnit_AssertionMessage_WithNegation_Converted_To_Because() + { + await CodeFixer.VerifyCodeFixAsync( + """ + using NUnit.Framework; + + {|#0:public class MyClass|} + { + [Test] + public void TestMethod() + { + Assert.That(5, Is.Not.EqualTo(10), "Hash should differ"); + } + } + """, + Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0), + """ + using TUnit.Core; + using TUnit.Assertions; + using static TUnit.Assertions.Assert; + using TUnit.Assertions.Extensions; + + public class MyClass + { + [Test] + public async Task TestMethod() + { + await Assert.That(5).IsNotEqualTo(10).Because("Hash should differ"); + } + } + """, + ConfigureNUnitTest + ); + } + + [Test] + public async Task NUnit_VoidMethod_ConvertedToAsyncTask_WhenContainsAwait() + { + await CodeFixer.VerifyCodeFixAsync( + """ + using NUnit.Framework; + + {|#0:public class MyClass|} + { + [Test] + public void TestMethod() + { + Assert.That(true, Is.True); + } + } + """, + Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0), + """ + using TUnit.Core; + using TUnit.Assertions; + using static TUnit.Assertions.Assert; + using TUnit.Assertions.Extensions; + + public class MyClass + { + [Test] + public async Task TestMethod() + { + await Assert.That(true).IsTrue(); + } + } + """, + ConfigureNUnitTest + ); + } + + [Test] + public async Task NUnit_TestCaseOnly_AddsTestAttribute() + { + await CodeFixer.VerifyCodeFixAsync( + """ + using NUnit.Framework; + + {|#0:public class MyClass|} + { + [TestCase(1, 2, 3)] + [TestCase(4, 5, 9)] + public void AdditionTest(int a, int b, int expected) + { + Assert.That(a + b, Is.EqualTo(expected)); + } + } + """, + Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0), + """ + using TUnit.Core; + using TUnit.Assertions; + using static TUnit.Assertions.Assert; + using TUnit.Assertions.Extensions; + + public class MyClass + { + [Test] + [Arguments(1, 2, 3)] + [Arguments(4, 5, 9)] + public async Task AdditionTest(int a, int b, int expected) + { + await Assert.That(a + b).IsEqualTo(expected); + } + } + """, + ConfigureNUnitTest + ); + } + + [Test] + public async Task NUnit_TestCaseWithExistingTest_DoesNotDuplicateTestAttribute() + { + await CodeFixer.VerifyCodeFixAsync( + """ + using NUnit.Framework; + + {|#0:public class MyClass|} + { + [Test] + [TestCase(1, 2, 3)] + public void AdditionTest(int a, int b, int expected) + { + Assert.That(a + b, Is.EqualTo(expected)); + } + } + """, + Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0), + """ + using TUnit.Core; + using TUnit.Assertions; + using static TUnit.Assertions.Assert; + using TUnit.Assertions.Extensions; + + public class MyClass + { + [Test] + [Arguments(1, 2, 3)] + public async Task AdditionTest(int a, int b, int expected) + { + await Assert.That(a + b).IsEqualTo(expected); + } + } + """, + ConfigureNUnitTest + ); + } + private static void ConfigureNUnitTest(Verifier.Test test) { test.TestState.AdditionalReferences.Add(typeof(NUnit.Framework.TestAttribute).Assembly);