diff --git a/TUnit.Analyzers.CodeFixers/TwoPhase/XUnitTwoPhaseAnalyzer.cs b/TUnit.Analyzers.CodeFixers/TwoPhase/XUnitTwoPhaseAnalyzer.cs index 47f149e8b6..0f8e90feb2 100644 --- a/TUnit.Analyzers.CodeFixers/TwoPhase/XUnitTwoPhaseAnalyzer.cs +++ b/TUnit.Analyzers.CodeFixers/TwoPhase/XUnitTwoPhaseAnalyzer.cs @@ -149,7 +149,7 @@ private bool IsXUnitAssertion(InvocationExpressionSyntax invocation) var (kind, replacementCode, introducesAwait, todoComment) = methodName switch { - "Equal" => ConvertEqual(arguments), + "Equal" => ConvertEqual(memberAccess, arguments), "NotEqual" => ConvertNotEqual(arguments), "True" => ConvertTrue(arguments), "False" => ConvertFalse(arguments), @@ -203,13 +203,44 @@ private bool IsXUnitAssertion(InvocationExpressionSyntax invocation) #region Assertion Conversions - private (AssertionConversionKind, string?, bool, string?) ConvertEqual(SeparatedSyntaxList args) + private (AssertionConversionKind, string?, bool, string?) ConvertEqual(MemberAccessExpressionSyntax memberAccess, SeparatedSyntaxList args) { if (args.Count < 2) return (AssertionConversionKind.Equal, null, false, null); var expected = args[0].Expression.ToString(); var actual = args[1].Expression.ToString(); + // Check if there's a third argument (tolerance/precision) + if (args.Count >= 3) + { + // Check the actual method signature to see if it's a tolerance/precision parameter + var symbolInfo = SemanticModel.GetSymbolInfo(memberAccess); + if (symbolInfo.Symbol is IMethodSymbol { Parameters.Length: >= 3 } methodSymbol) + { + var thirdParam = methodSymbol.Parameters[2]; + + if (thirdParam is + { Name: "tolerance" } or + { Name: "precision", Type.Name: "TimeSpan" }) + { + var thirdArgText = args[2].Expression.ToString(); + return (AssertionConversionKind.Equal, $"await Assert.That({actual}).IsEqualTo({expected}).Within({thirdArgText})", true, null); + } + + // Third argument and beyond exist but are not convertible (e.g., int precision for rounding) + var extraArgs = new List(); + for (int i = 2; i < args.Count && i < methodSymbol.Parameters.Length; i++) + { + var param = methodSymbol.Parameters[i]; + var argText = args[i].Expression.ToString(); + extraArgs.Add($"{param.Name}: {argText}"); + } + + var todoComment = $"// TODO: TUnit migration - xUnit Assert.Equal had additional argument(s) ({string.Join(", ", extraArgs)}) that could not be converted."; + return (AssertionConversionKind.Equal, $"await Assert.That({actual}).IsEqualTo({expected})", true, todoComment); + } + } + return (AssertionConversionKind.Equal, $"await Assert.That({actual}).IsEqualTo({expected})", true, null); } diff --git a/TUnit.Analyzers.Tests/XUnitMigrationAnalyzerTests.cs b/TUnit.Analyzers.Tests/XUnitMigrationAnalyzerTests.cs index b231a4dad9..0c290032f7 100644 --- a/TUnit.Analyzers.Tests/XUnitMigrationAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/XUnitMigrationAnalyzerTests.cs @@ -660,6 +660,218 @@ public async Task MyTest() ); } + [Test] + public async Task Assert_Equal_DateTime_With_Precision_Can_Be_Converted() + { + await CodeFixer + .VerifyCodeFixAsync( + """ + {|#0:using System; + using Xunit; + + public class MyClass + { + [Fact] + public void MyTest() + { + Assert.Equal(DateTime.Now, DateTime.Now, TimeSpan.FromSeconds(1)); + } + }|} + """, + Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0), + """ + using System; + using System.Threading.Tasks; + + public class MyClass + { + [Test] + public async Task MyTest() + { + await Assert.That(DateTime.Now).IsEqualTo(DateTime.Now).Within(TimeSpan.FromSeconds(1)); + } + } + """, + ConfigureXUnitTest + ); + } + + [Test] + public async Task Assert_Equal_DateTimeOffset_With_Precision_Can_Be_Converted() + { + await CodeFixer + .VerifyCodeFixAsync( + """ + {|#0:using System; + using Xunit; + + public class MyClass + { + [Fact] + public void MyTest() + { + Assert.Equal(DateTimeOffset.Now, DateTimeOffset.Now, TimeSpan.FromMilliseconds(100)); + } + }|} + """, + Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0), + """ + using System; + using System.Threading.Tasks; + + public class MyClass + { + [Test] + public async Task MyTest() + { + await Assert.That(DateTimeOffset.Now).IsEqualTo(DateTimeOffset.Now).Within(TimeSpan.FromMilliseconds(100)); + } + } + """, + ConfigureXUnitTest + ); + } + + [Test] + public async Task Assert_Equal_Double_With_Tolerance_Can_Be_Converted() + { + await CodeFixer + .VerifyCodeFixAsync( + """ + {|#0:using Xunit; + + public class MyClass + { + [Fact] + public void MyTest() + { + Assert.Equal(1.5, 1.50001, 0.001); + } + }|} + """, + Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0), + """ + using System.Threading.Tasks; + + public class MyClass + { + [Test] + public async Task MyTest() + { + await Assert.That(1.50001).IsEqualTo(1.5).Within(0.001); + } + } + """, + ConfigureXUnitTest + ); + } + + [Test] + public async Task Assert_Equal_Float_With_Tolerance_Can_Be_Converted() + { + await CodeFixer + .VerifyCodeFixAsync( + """ + {|#0:using Xunit; + + public class MyClass + { + [Fact] + public void MyTest() + { + Assert.Equal(1.5f, 1.50001f, 0.001f); + } + }|} + """, + Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0), + """ + using System.Threading.Tasks; + + public class MyClass + { + [Test] + public async Task MyTest() + { + await Assert.That(1.50001f).IsEqualTo(1.5f).Within(0.001f); + } + } + """, + ConfigureXUnitTest + ); + } + + [Test] + public async Task Assert_Equal_Double_With_Precision_Not_Converted() + { + await CodeFixer + .VerifyCodeFixAsync( + """ + {|#0:using Xunit; + + public class MyClass + { + [Fact] + public void MyTest() + { + Assert.Equal(1.12345, 1.12346, 3); + } + }|} + """, + Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0), + """ + using System.Threading.Tasks; + + public class MyClass + { + [Test] + public async Task MyTest() + { + // TODO: TUnit migration - xUnit Assert.Equal had additional argument(s) (precision: 3) that could not be converted. + await Assert.That(1.12346).IsEqualTo(1.12345); + } + } + """, + ConfigureXUnitTest + ); + } + + [Test] + public async Task Assert_Equal_Float_With_Precision_And_Rounding_Not_Converted() + { + await CodeFixer + .VerifyCodeFixAsync( + """ + {|#0:using System; + using Xunit; + + public class MyClass + { + [Fact] + public void MyTest() + { + Assert.Equal(1.5f, 1.6f, 0, MidpointRounding.ToEven); + } + }|} + """, + Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0), + """ + using System; + using System.Threading.Tasks; + + public class MyClass + { + [Test] + public async Task MyTest() + { + // TODO: TUnit migration - xUnit Assert.Equal had additional argument(s) (precision: 0, rounding: MidpointRounding.ToEven) that could not be converted. + await Assert.That(1.6f).IsEqualTo(1.5f); + } + } + """, + ConfigureXUnitTest + ); + } + [Test] public async Task Assert_Matches_Can_Be_Converted() {