Skip to content

Commit ca35aae

Browse files
Copilotthomhurst
andcommitted
Add support for Within(delta), DoesNotThrow, Assert.Ignore, and Assert.Fail conversions
Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com>
1 parent bf0e38c commit ca35aae

2 files changed

Lines changed: 284 additions & 12 deletions

File tree

TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs

Lines changed: 134 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,16 @@ private ExpressionSyntax ConvertConstraintToTUnitWithMessage(ExpressionSyntax ac
297297
_ => memberAccess.Name.ToString()
298298
};
299299

300+
// Handle chained constraint modifiers like .Within(delta) on Is.EqualTo(x).Within(delta)
301+
if (methodName == "Within" && memberAccess.Expression is InvocationExpressionSyntax innerConstraint)
302+
{
303+
// Get the base assertion (e.g., IsEqualTo(5)) first
304+
var baseAssertion = ConvertConstraintToTUnitWithMessage(actualValue, innerConstraint, message);
305+
306+
// Now chain .Within(delta) to it
307+
return ChainMethodCall(baseAssertion, "Within", constraint.ArgumentList.Arguments.ToArray());
308+
}
309+
300310
// Handle generic type constraints: Is.TypeOf<T>(), Is.InstanceOf<T>(), Is.AssignableFrom<T>()
301311
if (memberAccess.Name is GenericNameSyntax genericMethodName)
302312
{
@@ -453,6 +463,16 @@ private ExpressionSyntax ConvertConstraintToTUnit(ExpressionSyntax actualValue,
453463
_ => memberAccess.Name.ToString()
454464
};
455465

466+
// Handle chained constraint modifiers like .Within(delta) on Is.EqualTo(x).Within(delta)
467+
if (methodName == "Within" && memberAccess.Expression is InvocationExpressionSyntax innerConstraint)
468+
{
469+
// Get the base assertion (e.g., IsEqualTo(5)) first
470+
var baseAssertion = ConvertConstraintToTUnit(actualValue, innerConstraint);
471+
472+
// Now chain .Within(delta) to it
473+
return ChainMethodCall(baseAssertion, "Within", constraint.ArgumentList.Arguments.ToArray());
474+
}
475+
456476
// Handle generic type constraints: Is.TypeOf<T>(), Is.InstanceOf<T>(), Is.AssignableFrom<T>()
457477
if (memberAccess.Name is GenericNameSyntax genericMethodName)
458478
{
@@ -613,6 +633,43 @@ private ExpressionSyntax CreateInRangeAssertion(ExpressionSyntax actualValue, Se
613633
}
614634
return CreateTUnitAssertion("IsInRange", actualValue);
615635
}
636+
637+
/// <summary>
638+
/// Chains a method call onto an existing await expression.
639+
/// For example: await Assert.That(x).IsEqualTo(5) becomes await Assert.That(x).IsEqualTo(5).Within(2)
640+
/// </summary>
641+
private ExpressionSyntax ChainMethodCall(ExpressionSyntax baseExpression, string methodName, params ArgumentSyntax[] arguments)
642+
{
643+
// The base expression is an AwaitExpression like: await Assert.That(x).IsEqualTo(5)
644+
// We need to extract the invocation, add .Within(2) to it, and re-wrap in await
645+
if (baseExpression is AwaitExpressionSyntax awaitExpr)
646+
{
647+
var innerInvocation = awaitExpr.Expression;
648+
649+
// Create the chained method access: Assert.That(x).IsEqualTo(5).Within
650+
var chainedAccess = SyntaxFactory.MemberAccessExpression(
651+
SyntaxKind.SimpleMemberAccessExpression,
652+
innerInvocation,
653+
SyntaxFactory.IdentifierName(methodName)
654+
);
655+
656+
// Create the invocation: Assert.That(x).IsEqualTo(5).Within(2)
657+
var chainedInvocation = SyntaxFactory.InvocationExpression(
658+
chainedAccess,
659+
arguments.Length > 0
660+
? SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(arguments))
661+
: SyntaxFactory.ArgumentList()
662+
);
663+
664+
// Re-wrap in await
665+
var awaitKeyword = SyntaxFactory.Token(SyntaxKind.AwaitKeyword)
666+
.WithTrailingTrivia(SyntaxFactory.Space);
667+
return SyntaxFactory.AwaitExpression(awaitKeyword, chainedInvocation);
668+
}
669+
670+
// Fallback: just return the base expression if it's not the expected shape
671+
return baseExpression;
672+
}
616673

617674
private ExpressionSyntax? ConvertClassicAssertion(InvocationExpressionSyntax invocation, string methodName)
618675
{
@@ -624,6 +681,12 @@ private ExpressionSyntax CreateInRangeAssertion(ExpressionSyntax actualValue, Se
624681
return ConvertNUnitThrows(invocation);
625682
}
626683

684+
// Handle Assert.DoesNotThrow and Assert.DoesNotThrowAsync
685+
if (methodName is "DoesNotThrow" or "DoesNotThrowAsync")
686+
{
687+
return ConvertDoesNotThrow(arguments);
688+
}
689+
627690
// Handle special assertions (Pass, Inconclusive, Fail, Warn)
628691
return methodName switch
629692
{
@@ -812,6 +875,53 @@ private ExpressionSyntax ConvertNUnitThrows(InvocationExpressionSyntax invocatio
812875
: invocation.ArgumentList.Arguments[0].Expression;
813876
return CreateTUnitAssertion("Throws", fallbackArg);
814877
}
878+
879+
private ExpressionSyntax ConvertDoesNotThrow(SeparatedSyntaxList<ArgumentSyntax> arguments)
880+
{
881+
// Assert.DoesNotThrow(() => action) -> await Assert.That(() => action).ThrowsNothing()
882+
if (arguments.Count == 0)
883+
{
884+
// Fallback - shouldn't happen but handle gracefully
885+
return SyntaxFactory.InvocationExpression(
886+
SyntaxFactory.MemberAccessExpression(
887+
SyntaxKind.SimpleMemberAccessExpression,
888+
SyntaxFactory.IdentifierName("Assert"),
889+
SyntaxFactory.IdentifierName("Pass")
890+
)
891+
);
892+
}
893+
894+
var action = arguments[0].Expression;
895+
896+
// Create Assert.That(() => action)
897+
var assertThatInvocation = SyntaxFactory.InvocationExpression(
898+
SyntaxFactory.MemberAccessExpression(
899+
SyntaxKind.SimpleMemberAccessExpression,
900+
SyntaxFactory.IdentifierName("Assert"),
901+
SyntaxFactory.IdentifierName("That")
902+
),
903+
SyntaxFactory.ArgumentList(
904+
SyntaxFactory.SingletonSeparatedList(
905+
SyntaxFactory.Argument(action)
906+
)
907+
)
908+
);
909+
910+
// Chain .ThrowsNothing()
911+
var throwsNothingInvocation = SyntaxFactory.InvocationExpression(
912+
SyntaxFactory.MemberAccessExpression(
913+
SyntaxKind.SimpleMemberAccessExpression,
914+
assertThatInvocation,
915+
SyntaxFactory.IdentifierName("ThrowsNothing")
916+
),
917+
SyntaxFactory.ArgumentList()
918+
);
919+
920+
// Wrap in await
921+
var awaitKeyword = SyntaxFactory.Token(SyntaxKind.AwaitKeyword)
922+
.WithTrailingTrivia(SyntaxFactory.Space);
923+
return SyntaxFactory.AwaitExpression(awaitKeyword, throwsNothingInvocation);
924+
}
815925

816926
/// <summary>
817927
/// Attempts to extract the exception type from NUnit constraint expressions like Is.TypeOf(typeof(T)).
@@ -875,34 +985,46 @@ private ExpressionSyntax CreatePassAssertion(SeparatedSyntaxList<ArgumentSyntax>
875985

876986
private ExpressionSyntax CreateFailAssertion(SeparatedSyntaxList<ArgumentSyntax> arguments)
877987
{
988+
// TUnit: Fail.Test("reason") - not awaited, throws synchronously
989+
var reasonArg = arguments.Count > 0
990+
? arguments[0]
991+
: SyntaxFactory.Argument(
992+
SyntaxFactory.LiteralExpression(
993+
SyntaxKind.StringLiteralExpression,
994+
SyntaxFactory.Literal("Test failed")));
995+
878996
var failInvocation = SyntaxFactory.InvocationExpression(
879997
SyntaxFactory.MemberAccessExpression(
880998
SyntaxKind.SimpleMemberAccessExpression,
881-
SyntaxFactory.IdentifierName("Assert"),
882-
SyntaxFactory.IdentifierName("Fail")
999+
SyntaxFactory.IdentifierName("Fail"),
1000+
SyntaxFactory.IdentifierName("Test")
8831001
),
884-
arguments.Count > 0
885-
? SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(arguments[0]))
886-
: SyntaxFactory.ArgumentList()
1002+
SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(reasonArg))
8871003
);
8881004

889-
return SyntaxFactory.AwaitExpression(failInvocation);
1005+
return failInvocation;
8901006
}
8911007

8921008
private ExpressionSyntax CreateSkipAssertion(SeparatedSyntaxList<ArgumentSyntax> arguments)
8931009
{
1010+
// TUnit: Skip.Test("reason") - not awaited, throws SkipTestException
1011+
var reasonArg = arguments.Count > 0
1012+
? arguments[0]
1013+
: SyntaxFactory.Argument(
1014+
SyntaxFactory.LiteralExpression(
1015+
SyntaxKind.StringLiteralExpression,
1016+
SyntaxFactory.Literal("Test skipped")));
1017+
8941018
var skipInvocation = SyntaxFactory.InvocationExpression(
8951019
SyntaxFactory.MemberAccessExpression(
8961020
SyntaxKind.SimpleMemberAccessExpression,
897-
SyntaxFactory.IdentifierName("Assert"),
898-
SyntaxFactory.IdentifierName("Skip")
1021+
SyntaxFactory.IdentifierName("Skip"),
1022+
SyntaxFactory.IdentifierName("Test")
8991023
),
900-
arguments.Count > 0
901-
? SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(arguments[0]))
902-
: SyntaxFactory.ArgumentList()
1024+
SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(reasonArg))
9031025
);
9041026

905-
return SyntaxFactory.AwaitExpression(skipInvocation);
1027+
return skipInvocation;
9061028
}
9071029
}
9081030

TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2154,6 +2154,156 @@ public async Task TestMethod()
21542154
);
21552155
}
21562156

2157+
[Test]
2158+
public async Task NUnit_WithinDelta_Converted()
2159+
{
2160+
await CodeFixer.VerifyCodeFixAsync(
2161+
"""
2162+
using NUnit.Framework;
2163+
2164+
{|#0:public class MyClass|}
2165+
{
2166+
[Test]
2167+
public void TestMethod()
2168+
{
2169+
Assert.That(10, Is.EqualTo(5).Within(2));
2170+
}
2171+
}
2172+
""",
2173+
Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
2174+
"""
2175+
using System.Threading.Tasks;
2176+
using TUnit.Core;
2177+
using TUnit.Assertions;
2178+
using static TUnit.Assertions.Assert;
2179+
using TUnit.Assertions.Extensions;
2180+
2181+
public class MyClass
2182+
{
2183+
[Test]
2184+
public async Task TestMethod()
2185+
{
2186+
await Assert.That(10).IsEqualTo(5).Within(2);
2187+
}
2188+
}
2189+
""",
2190+
ConfigureNUnitTest
2191+
);
2192+
}
2193+
2194+
[Test]
2195+
public async Task NUnit_DoesNotThrow_Converted()
2196+
{
2197+
await CodeFixer.VerifyCodeFixAsync(
2198+
"""
2199+
using NUnit.Framework;
2200+
2201+
{|#0:public class MyClass|}
2202+
{
2203+
[Test]
2204+
public void TestMethod()
2205+
{
2206+
int x = 1;
2207+
int y = 2;
2208+
Assert.DoesNotThrow(() => x += y);
2209+
}
2210+
}
2211+
""",
2212+
Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
2213+
"""
2214+
using System.Threading.Tasks;
2215+
using TUnit.Core;
2216+
using TUnit.Assertions;
2217+
using static TUnit.Assertions.Assert;
2218+
using TUnit.Assertions.Extensions;
2219+
2220+
public class MyClass
2221+
{
2222+
[Test]
2223+
public async Task TestMethod()
2224+
{
2225+
int x = 1;
2226+
int y = 2;
2227+
await Assert.That(() => x += y).ThrowsNothing();
2228+
}
2229+
}
2230+
""",
2231+
ConfigureNUnitTest
2232+
);
2233+
}
2234+
2235+
[Test]
2236+
public async Task NUnit_Ignore_Converted()
2237+
{
2238+
await CodeFixer.VerifyCodeFixAsync(
2239+
"""
2240+
using NUnit.Framework;
2241+
2242+
{|#0:public class MyClass|}
2243+
{
2244+
[Test]
2245+
public void TestMethod()
2246+
{
2247+
Assert.Ignore("Feature not supported");
2248+
}
2249+
}
2250+
""",
2251+
Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
2252+
"""
2253+
using TUnit.Core;
2254+
using TUnit.Assertions;
2255+
using static TUnit.Assertions.Assert;
2256+
using TUnit.Assertions.Extensions;
2257+
2258+
public class MyClass
2259+
{
2260+
[Test]
2261+
public void TestMethod()
2262+
{
2263+
Skip.Test("Feature not supported");
2264+
}
2265+
}
2266+
""",
2267+
ConfigureNUnitTest
2268+
);
2269+
}
2270+
2271+
[Test]
2272+
public async Task NUnit_Fail_Converted()
2273+
{
2274+
await CodeFixer.VerifyCodeFixAsync(
2275+
"""
2276+
using NUnit.Framework;
2277+
2278+
{|#0:public class MyClass|}
2279+
{
2280+
[Test]
2281+
public void TestMethod()
2282+
{
2283+
Assert.Fail("Test failed intentionally");
2284+
}
2285+
}
2286+
""",
2287+
Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
2288+
"""
2289+
using TUnit.Core;
2290+
using TUnit.Assertions;
2291+
using static TUnit.Assertions.Assert;
2292+
using TUnit.Assertions.Extensions;
2293+
2294+
public class MyClass
2295+
{
2296+
[Test]
2297+
public void TestMethod()
2298+
{
2299+
Fail.Test("Test failed intentionally");
2300+
}
2301+
}
2302+
""",
2303+
ConfigureNUnitTest
2304+
);
2305+
}
2306+
21572307
private static void ConfigureNUnitTest(Verifier.Test test)
21582308
{
21592309
test.TestState.AdditionalReferences.Add(typeof(NUnit.Framework.TestAttribute).Assembly);

0 commit comments

Comments
 (0)