diff --git a/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs b/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs index 0c42f0d347..8cb0b1debb 100644 --- a/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs +++ b/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs @@ -170,13 +170,18 @@ private static void GenerateExtensionMethods(SourceProductionContext context, As continue; } + // Check if the type parameter is a nullable reference type (e.g., string?) + var typeParam = data.AssertionBaseType.TypeArguments[0]; + var isNullableReferenceType = typeParam.NullableAnnotation == NullableAnnotation.Annotated && + typeParam.IsReferenceType; + // Generate positive assertion method - GenerateExtensionMethod(sourceBuilder, data, constructor, negated: false); + GenerateExtensionMethod(sourceBuilder, data, constructor, negated: false, isNullableOverload: false); // Generate negated assertion method if requested if (!string.IsNullOrEmpty(data.NegatedMethodName)) { - GenerateExtensionMethod(sourceBuilder, data, constructor, negated: true); + GenerateExtensionMethod(sourceBuilder, data, constructor, negated: true, isNullableOverload: false); } } @@ -211,7 +216,8 @@ private static void GenerateExtensionMethod( StringBuilder sourceBuilder, AssertionExtensionData data, IMethodSymbol constructor, - bool negated) + bool negated, + bool isNullableOverload) { var methodName = negated ? data.NegatedMethodName : data.MethodName; var assertionType = data.ClassSymbol; @@ -316,10 +322,18 @@ private static void GenerateExtensionMethod( sourceBuilder.AppendLine($" [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode(\"{escapedMessage}\")]"); } - // Add OverloadResolutionPriority attribute only if priority > 0 - if (data.OverloadResolutionPriority > 0) + // Add OverloadResolutionPriority attribute if specified + // For nullable overloads (generic with class constraint), increase priority by 1 + // so they're preferred over the base nullable overload when source is non-nullable + var effectivePriority = data.OverloadResolutionPriority; + if (isNullableOverload) { - sourceBuilder.AppendLine($" [global::System.Runtime.CompilerServices.OverloadResolutionPriority({data.OverloadResolutionPriority})]"); + effectivePriority += 1; + } + + if (effectivePriority > 0) + { + sourceBuilder.AppendLine($" [global::System.Runtime.CompilerServices.OverloadResolutionPriority({effectivePriority})]"); } // Method declaration @@ -327,11 +341,22 @@ private static void GenerateExtensionMethod( ? $"{assertionType.Name}{genericParamsString}" : assertionType.Name; - // The extension method always extends IAssertionSource where T is the type argument - // from the Assertion base class. This ensures the source.Context type matches what - // the assertion constructor expects. + // The extension method extends IAssertionSource where T is the type argument + // from the Assertion base class. string sourceType; - if (typeParam is ITypeParameterSymbol baseTypeParam) + string genericTypeParam = null; + string genericConstraint = null; + + if (isNullableOverload) + { + // For nullable reference types, we can't use two separate overloads for T and T? + // because NRT annotations are erased at runtime - they're the same type to the CLR. + // Instead, just use the nullable version and accept both nullable and non-nullable sources. + sourceType = $"IAssertionSource<{typeParam.ToDisplayString()}>"; + genericTypeParam = null; + genericConstraint = null; + } + else if (typeParam is ITypeParameterSymbol baseTypeParam) { sourceType = $"IAssertionSource<{baseTypeParam.Name}>"; } @@ -340,7 +365,13 @@ private static void GenerateExtensionMethod( sourceType = $"IAssertionSource<{typeParam.ToDisplayString()}>"; } - sourceBuilder.Append($" public static {returnType} {methodName}{genericParamsString}("); + sourceBuilder.Append($" public static {returnType} {methodName}"); + if (genericTypeParam != null) + { + sourceBuilder.Append($"<{genericTypeParam}>"); + } + sourceBuilder.Append(genericParamsString); + sourceBuilder.Append("("); sourceBuilder.Append($"this {sourceType} source"); // Add additional parameters @@ -366,10 +397,16 @@ private static void GenerateExtensionMethod( sourceBuilder.Append(")"); // Add type constraints on new line if any - if (typeConstraints.Count > 0) + var allConstraints = new List(typeConstraints); + if (genericConstraint != null) + { + allConstraints.Add(genericConstraint); + } + + if (allConstraints.Count > 0) { sourceBuilder.AppendLine(); - sourceBuilder.Append($" {string.Join(" ", typeConstraints)}"); + sourceBuilder.Append($" {string.Join(" ", allConstraints)}"); } sourceBuilder.AppendLine(); @@ -394,7 +431,16 @@ private static void GenerateExtensionMethod( { sourceBuilder.Append($"<{string.Join(", ", genericParams)}>"); } - sourceBuilder.Append("(source.Context"); + sourceBuilder.Append("("); + + if (isNullableOverload) + { + sourceBuilder.Append("source.Context.AsNullable()"); + } + else + { + sourceBuilder.Append("source.Context"); + } foreach (var param in additionalParams) { diff --git a/TUnit.Assertions.Tests/AssertConditions/BecauseTests.cs b/TUnit.Assertions.Tests/AssertConditions/BecauseTests.cs index 0193dc0403..1d44b17e48 100644 --- a/TUnit.Assertions.Tests/AssertConditions/BecauseTests.cs +++ b/TUnit.Assertions.Tests/AssertConditions/BecauseTests.cs @@ -91,7 +91,7 @@ await Assert.That(variable).IsTrue().Because(because) }; var exception = await Assert.ThrowsAsync(action); - await Assert.That(exception.Message).IsEqualTo(expectedMessage); + await Assert.That(exception.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage); } [Test] diff --git a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.ExactlyTests.cs b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.ExactlyTests.cs index 82fbb1f190..c3292ec78e 100644 --- a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.ExactlyTests.cs +++ b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.ExactlyTests.cs @@ -140,10 +140,12 @@ public async Task Conversion_To_Value_Assertion_Builder_On_Casted_Exception_Type await Assert.That((object)ex).IsAssignableTo(); }); - await Assert.That(assertionException).HasMessageStartingWith(""" + var expectedPrefix = """ Expected to throw exactly CustomException and to have message equal to "Foo bar message!" - """); + """; + + await Assert.That(assertionException.Message.NormalizeLineEndings()).StartsWith(expectedPrefix.NormalizeLineEndings()); } } } diff --git a/TUnit.Assertions.Tests/Bugs/Tests2145.cs b/TUnit.Assertions.Tests/Bugs/Tests2145.cs index a95f255b79..1112cc6b4a 100644 --- a/TUnit.Assertions.Tests/Bugs/Tests2145.cs +++ b/TUnit.Assertions.Tests/Bugs/Tests2145.cs @@ -5,16 +5,16 @@ public class Tests2145 [Test] public async Task TestFailMessage() { - await Assert.That(async () => + var exception = await Assert.ThrowsAsync(async () => { var val = "hello"; using var _ = Assert.Multiple(); await Assert.That(val).IsEqualTo("world"); await Assert.That(val).IsEqualTo("World"); - }).Throws() - .WithMessage( - """ + }); + + var expectedMessage = """ Expected to be equal to "world" but found "hello" @@ -24,7 +24,8 @@ Expected to be equal to "World" but found "hello" at Assert.That(val).IsEqualTo("World") - """ - ); + """; + + await Assert.That(exception!.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings()); } } diff --git a/TUnit.Assertions.Tests/GlobalUsings.cs b/TUnit.Assertions.Tests/GlobalUsings.cs index 4d93167cd3..1af00b83b7 100644 --- a/TUnit.Assertions.Tests/GlobalUsings.cs +++ b/TUnit.Assertions.Tests/GlobalUsings.cs @@ -1,3 +1,14 @@ global using TUnit.Assertions.Exceptions; global using TUnit.Assertions.Extensions; global using TUnit.Core; + +internal static class TestHelpers +{ + /// + /// Normalizes all line endings to Unix-style (\n) for consistent cross-platform testing. + /// + public static string NormalizeLineEndings(this string value) + { + return value.Replace("\r\n", "\n").Replace("\r", "\n"); + } +} diff --git a/TUnit.Assertions.Tests/NullabilityWarningTests.cs b/TUnit.Assertions.Tests/NullabilityWarningTests.cs new file mode 100644 index 0000000000..7a99bbe965 --- /dev/null +++ b/TUnit.Assertions.Tests/NullabilityWarningTests.cs @@ -0,0 +1,135 @@ +namespace TUnit.Assertions.Tests; + +/// +/// Tests to ensure that all combinations of nullable/non-nullable strings +/// work without generating nullability warnings (CS8604, CS8625, etc.) +/// This validates the fix for GitHub issue #3643. +/// +public class NullabilityWarningTests +{ + [Test] + public async Task NonNullableString_WithNonNullableExpected_NoWarning() + { + string actualValue = "test"; + string expectedValue = "test"; + + // Non-nullable to non-nullable should work + await Assert.That(actualValue).IsEqualTo(expectedValue); + } + + [Test] + public async Task NonNullableString_WithNullableExpected_NoWarning() + { + string actualValue = "test"; + string? expectedValue = "test"; + + // Non-nullable string should accept nullable expected without warning + await Assert.That(actualValue).IsEqualTo(expectedValue); + } + + [Test] + public async Task NullableString_WithNonNullableExpected_NoWarning() + { + string? actualValue = "test"; + string expectedValue = "test"; + + // Nullable string should accept non-nullable expected without warning + await Assert.That(actualValue).IsEqualTo(expectedValue); + } + + [Test] + public async Task NullableString_WithNullableExpected_NoWarning() + { + string? actualValue = "test"; + string? expectedValue = "test"; + + // Nullable to nullable should work (this is the main issue from #3643) + await Assert.That(actualValue).IsEqualTo(expectedValue); + } + + [Test] + public async Task NonNullableString_WithNullLiteral_NoWarning() + { + string actualValue = "test"; + + // Passing null literal should be allowed (tests for null mismatch) + await Assert.ThrowsAsync(async () => + await Assert.That(actualValue).IsEqualTo(null)); + } + + [Test] + public async Task NullableString_WithNullLiteral_NoWarning() + { + string? actualValue = null; + + // Nullable string with null literal should work + await Assert.That(actualValue).IsEqualTo(null); + } + + [Test] + public async Task NonNullableString_WithStringComparison_NoWarning() + { + string actualValue = "TEST"; + string? expectedValue = "test"; + + // Non-nullable with StringComparison parameter and nullable expected + await Assert.That(actualValue).IsEqualTo(expectedValue, StringComparison.OrdinalIgnoreCase); + } + + [Test] + public async Task NullableString_WithStringComparison_NoWarning() + { + string? actualValue = "TEST"; + string? expectedValue = "test"; + + // Nullable with StringComparison parameter + await Assert.That(actualValue).IsEqualTo(expectedValue, StringComparison.OrdinalIgnoreCase); + } + + [Test] + public async Task NonNullableString_WithModifiers_NoWarning() + { + string actualValue = " TEST "; + string? expectedValue = "test"; + + // Non-nullable with modifiers and nullable expected + await Assert.That(actualValue).IsEqualTo(expectedValue).WithTrimming().IgnoringCase(); + } + + [Test] + public async Task NullableString_WithModifiers_NoWarning() + { + string? actualValue = " TEST "; + string? expectedValue = "test"; + + // Nullable with modifiers + await Assert.That(actualValue).IsEqualTo(expectedValue).WithTrimming().IgnoringCase(); + } + + [Test] + public async Task MixedNullability_FromMethods_NoWarning() + { + // Testing that method return values work correctly + string nonNullable = GetNonNullableString(); + string? nullable = GetNullableString(); + + await Assert.That(nonNullable).IsEqualTo(nullable); + await Assert.That(nullable).IsEqualTo(nonNullable); + } + + [Test] + public async Task ImplicitConversion_NonNullableToNullable_NoWarning() + { + // This verifies that the generated extension method signature + // IAssertionSource accepts both string and string? without warnings + string nonNullable = "test"; + + // The implicit conversion from string to string? should work seamlessly + await Assert.That(nonNullable).IsEqualTo("test"); + await Assert.That(nonNullable).IsEqualTo((string?)"test"); + } + + private static string GetNonNullableString() => "result"; + + private static string? GetNullableString() => "result"; +} diff --git a/TUnit.Assertions.Tests/NullableStringEqualityTests.cs b/TUnit.Assertions.Tests/NullableStringEqualityTests.cs new file mode 100644 index 0000000000..666abd0ae6 --- /dev/null +++ b/TUnit.Assertions.Tests/NullableStringEqualityTests.cs @@ -0,0 +1,151 @@ +namespace TUnit.Assertions.Tests; + +/// +/// Tests to verify that IsEqualTo works correctly with nullable strings +/// without requiring workarounds like `?? string.Empty`. +/// Addresses GitHub issue #3643. +/// +public class NullableStringEqualityTests +{ + [Test] + public async Task IsEqualTo_WithNullableStringActualAndNullableStringExpected_BothNonNull_Succeeds() + { + string? actualValue = "test"; + string? expectedValue = "test"; + + // This should work without requiring `expectedValue ?? string.Empty` + await Assert.That(actualValue).IsEqualTo(expectedValue); + } + + [Test] + public async Task IsEqualTo_WithNullableStringActualAndNullableStringExpected_BothNull_Succeeds() + { + string? actualValue = null; + string? expectedValue = null; + + // Both null should be considered equal + await Assert.That(actualValue).IsEqualTo(expectedValue); + } + + [Test] + public async Task IsEqualTo_WithNullableStringActualAndNullExpected_ActualNonNull_Fails() + { + string? actualValue = "test"; + string? expectedValue = null; + + await Assert.ThrowsAsync(async () => + await Assert.That(actualValue).IsEqualTo(expectedValue)); + } + + [Test] + public async Task IsEqualTo_WithNullableStringActualAndNullExpected_ActualNull_Succeeds() + { + string? actualValue = null; + string? expectedValue = "test"; + + await Assert.ThrowsAsync(async () => + await Assert.That(actualValue).IsEqualTo(expectedValue)); + } + + [Test] + public async Task IsEqualTo_WithNullableString_AndIgnoringCase_Succeeds() + { + string? actualValue = "TEST"; + string? expectedValue = "test"; + + // Nullable strings should work with string-specific modifiers + await Assert.That(actualValue).IsEqualTo(expectedValue).IgnoringCase(); + } + + [Test] + public async Task IsEqualTo_WithNullableString_AndWithTrimming_Succeeds() + { + string? actualValue = " test "; + string? expectedValue = "test"; + + // Nullable strings should work with string-specific modifiers + await Assert.That(actualValue).IsEqualTo(expectedValue).WithTrimming(); + } + + [Test] + public async Task IsEqualTo_WithNullableString_AndIgnoringWhitespace_Succeeds() + { + string? actualValue = "t e s t"; + string? expectedValue = "test"; + + // Nullable strings should work with string-specific modifiers + await Assert.That(actualValue).IsEqualTo(expectedValue).IgnoringWhitespace(); + } + + [Test] + public async Task IsEqualTo_WithNullableString_AndWithNullAndEmptyEquality_NullActualEmptyExpected_Succeeds() + { + string? actualValue = null; + string? expectedValue = ""; + + // null and empty should be considered equal with this modifier + await Assert.That(actualValue).IsEqualTo(expectedValue).WithNullAndEmptyEquality(); + } + + [Test] + public async Task IsEqualTo_WithNullableString_AndWithNullAndEmptyEquality_EmptyActualNullExpected_Succeeds() + { + string? actualValue = ""; + string? expectedValue = null; + + // null and empty should be considered equal with this modifier + await Assert.That(actualValue).IsEqualTo(expectedValue).WithNullAndEmptyEquality(); + } + + [Test] + public async Task IsEqualTo_WithNullableString_AndWithNullAndEmptyEquality_BothNull_Succeeds() + { + string? actualValue = null; + string? expectedValue = null; + + await Assert.That(actualValue).IsEqualTo(expectedValue).WithNullAndEmptyEquality(); + } + + [Test] + public async Task IsEqualTo_WithNullableString_AndWithComparison_Succeeds() + { + string? actualValue = "TEST"; + string? expectedValue = "test"; + + // Nullable strings should work with StringComparison parameter + await Assert.That(actualValue).IsEqualTo(expectedValue).WithComparison(StringComparison.OrdinalIgnoreCase); + } + + [Test] + public async Task IsEqualTo_WithNullableString_CombinedModifiers_Succeeds() + { + string? actualValue = " T E S T "; + string? expectedValue = "test"; + + // Multiple modifiers should work together with nullable strings + await Assert.That(actualValue).IsEqualTo(expectedValue).WithTrimming().IgnoringWhitespace().IgnoringCase(); + } + + [Test] + public async Task IsEqualTo_WithNullableVariableDeclaredWithVar_Succeeds() + { + // Verify it works with var (type inference) as well + string? actual = GetNullableString(); + string? expected = "result"; + + await Assert.That(actual).IsEqualTo(expected); + } + + [Test] + public async Task IsEqualTo_WithNullableStringFromMethod_BothNull_Succeeds() + { + string? actual = GetNullString(); + string? expected = GetNullString(); + + await Assert.That(actual).IsEqualTo(expected); + } + + private static string? GetNullableString() => "result"; + + private static string? GetNullString() => null; +} diff --git a/TUnit.Assertions.Tests/Old/AssertMultipleTests.cs b/TUnit.Assertions.Tests/Old/AssertMultipleTests.cs index e1fcd0b335..a12e4a5421 100644 --- a/TUnit.Assertions.Tests/Old/AssertMultipleTests.cs +++ b/TUnit.Assertions.Tests/Old/AssertMultipleTests.cs @@ -28,35 +28,35 @@ public async Task MultipleFailures() var exception4 = (TUnitAssertionException) aggregateException.InnerExceptions[3]; var exception5 = (TUnitAssertionException) aggregateException.InnerExceptions[4]; - await TUnitAssert.That(exception1.Message).IsEqualTo(""" + await TUnitAssert.That(exception1.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 2 but found 1 at Assert.That(1).IsEqualTo(2) """); - await TUnitAssert.That(exception2.Message).IsEqualTo(""" + await TUnitAssert.That(exception2.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 3 but found 2 at Assert.That(2).IsEqualTo(3) """); - await TUnitAssert.That(exception3.Message).IsEqualTo(""" + await TUnitAssert.That(exception3.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 4 but found 3 at Assert.That(3).IsEqualTo(4) """); - await TUnitAssert.That(exception4.Message).IsEqualTo(""" + await TUnitAssert.That(exception4.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 5 but found 4 at Assert.That(4).IsEqualTo(5) """); - await TUnitAssert.That(exception5.Message).IsEqualTo(""" + await TUnitAssert.That(exception5.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 6 but found 5 @@ -87,7 +87,7 @@ public async Task MultipleFailures_With_Connectors() var exception4 = (TUnitAssertionException) aggregateException.InnerExceptions[3]; var exception5 = (TUnitAssertionException) aggregateException.InnerExceptions[4]; - await TUnitAssert.That(exception1.Message).IsEqualTo(""" + await TUnitAssert.That(exception1.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 2 or to be 3 but found 1 @@ -95,7 +95,7 @@ but found 1 at Assert.That(1).IsEqualTo(2).Or.IsEqualTo(3) """); - await TUnitAssert.That(exception2.Message).IsEqualTo(""" + await TUnitAssert.That(exception2.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 3 and to be 4 but found 2 @@ -103,7 +103,7 @@ but found 2 at Assert.That(2).IsEqualTo(3).And.IsEqualTo(4) """); - await TUnitAssert.That(exception3.Message).IsEqualTo(""" + await TUnitAssert.That(exception3.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 4 or to be 5 but found 3 @@ -111,7 +111,7 @@ but found 3 at Assert.That(3).IsEqualTo(4).Or.IsEqualTo(5) """); - await TUnitAssert.That(exception4.Message).IsEqualTo(""" + await TUnitAssert.That(exception4.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 5 and to be 6 but found 4 @@ -119,7 +119,7 @@ but found 4 at Assert.That(4).IsEqualTo(5).And.IsEqualTo(6) """); - await TUnitAssert.That(exception5.Message).IsEqualTo(""" + await TUnitAssert.That(exception5.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 6 or to be 7 but found 5 @@ -171,49 +171,49 @@ public async Task Nested_Multiples() var assertionException6 = (TUnitAssertionException) aggregateException.InnerExceptions[5]; var assertionException7 = (TUnitAssertionException) aggregateException.InnerExceptions[6]; - await TUnitAssert.That(assertionException1.Message).IsEqualTo(""" + await TUnitAssert.That(assertionException1.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 2 but found 1 at Assert.That(1).IsEqualTo(2) """); - await TUnitAssert.That(assertionException2.Message).IsEqualTo(""" + await TUnitAssert.That(assertionException2.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 3 but found 2 at Assert.That(2).IsEqualTo(3) """); - await TUnitAssert.That(assertionException3.Message).IsEqualTo(""" + await TUnitAssert.That(assertionException3.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 4 but found 3 at Assert.That(3).IsEqualTo(4) """); - await TUnitAssert.That(assertionException4.Message).IsEqualTo(""" + await TUnitAssert.That(assertionException4.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 5 but found 4 at Assert.That(4).IsEqualTo(5) """); - await TUnitAssert.That(assertionException5.Message).IsEqualTo(""" + await TUnitAssert.That(assertionException5.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 6 but found 5 at Assert.That(5).IsEqualTo(6) """); - await TUnitAssert.That(assertionException6.Message).IsEqualTo(""" + await TUnitAssert.That(assertionException6.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 7 but found 6 at Assert.That(6).IsEqualTo(7) """); - await TUnitAssert.That(assertionException7.Message).IsEqualTo(""" + await TUnitAssert.That(assertionException7.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 8 but found 7 diff --git a/TUnit.Assertions.Tests/Old/EquivalentAssertionTests.cs b/TUnit.Assertions.Tests/Old/EquivalentAssertionTests.cs index ea82d335f6..841660fe8d 100644 --- a/TUnit.Assertions.Tests/Old/EquivalentAssertionTests.cs +++ b/TUnit.Assertions.Tests/Old/EquivalentAssertionTests.cs @@ -130,7 +130,7 @@ public async Task Different_Enumerables__Thrown_When_Non_Matching_Order() var exception = await TUnitAssert.ThrowsAsync(async () => await TUnitAssert.That(array).IsEquivalentTo(list, CollectionOrdering.Matching)); - await TUnitAssert.That(exception!.Message).IsEqualTo( + await TUnitAssert.That(exception!.Message.NormalizeLineEndings()).IsEqualTo( """ Expected to be equivalent to [1, 2, 3, 4, 5] but collection item at index 1 does not match: expected 2, but was 5 @@ -149,7 +149,7 @@ public async Task Different_Enumerables__Thrown_When_Non_Matching_Order2() var exception = await TUnitAssert.ThrowsAsync(async () => await TUnitAssert.That(array).IsEquivalentTo(list, CollectionOrdering.Matching)); - await TUnitAssert.That(exception!.Message).IsEqualTo( + await TUnitAssert.That(exception!.Message.NormalizeLineEndings()).IsEqualTo( """ Expected to be equivalent to [1, 2, 3, 4, 5] but collection item at index 1 does not match: expected 2, but was 5 @@ -177,7 +177,7 @@ public async Task Different_Mismatched_Objects_Still_Are_Not_Equivalent() $"Received: \"Foo\"{Environment.NewLine}" + $"{Environment.NewLine}" + $"at Assert.That(result1).IsEquivalentTo(result2)"; - await TUnitAssert.That(exception!.Message).IsEqualTo(expectedMessage); + await TUnitAssert.That(exception!.Message.NormalizeLineEndings().NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings()); } [Test] @@ -197,7 +197,7 @@ public async Task Mismatched_Objects_Are_Not_Equivalent() $"Received: null{Environment.NewLine}" + $"{Environment.NewLine}" + $"at Assert.That(object1).IsEquivalentTo(object2)"; - await TUnitAssert.That(exception!.Message).IsEqualTo(expectedMessage); + await TUnitAssert.That(exception!.Message.NormalizeLineEndings().NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings()); } [Test] @@ -233,7 +233,7 @@ public async Task Objects_With_Nested_Mismatch_Are_Not_Equivalent() $"Received: null{Environment.NewLine}" + $"{Environment.NewLine}" + $"at Assert.That(object1).IsEquivalentTo(object2)"; - await TUnitAssert.That(exception!.Message).IsEqualTo(expectedMessage); + await TUnitAssert.That(exception!.Message.NormalizeLineEndings().NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings()); } [Test] @@ -365,7 +365,7 @@ public async Task Objects_With_Nested_Enumerable_Mismatch_Are_Not_Equivalent() $"Received: null{Environment.NewLine}" + $"{Environment.NewLine}" + $"at Assert.That(object1).IsEquivalentTo(object2)"; - await TUnitAssert.That(exception!.Message).IsEqualTo(expectedMessage); + await TUnitAssert.That(exception!.Message.NormalizeLineEndings().NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings()); } [Test] @@ -496,7 +496,7 @@ public async Task Objects_With_Partial_Properties_Match_With_Full_Equivalency_Ar $"Received: InnerClass{Environment.NewLine}" + $"{Environment.NewLine}" + $"at Assert.That(object1).IsEquivalentTo(object2)"; - await TUnitAssert.That(exception!.Message).IsEqualTo(expectedMessage); + await TUnitAssert.That(exception!.Message.NormalizeLineEndings().NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings()); } [Test] @@ -572,7 +572,7 @@ public async Task Objects_With_Mismatch_With_Partial_Equivalency_Kind_Are_Not_Eq $"Received: \"Bar\"{Environment.NewLine}" + $"{Environment.NewLine}" + $"at Assert.That(object1).IsEquivalentTo(object2).WithPartialEquivalency()"; - await TUnitAssert.That(exception!.Message).IsEqualTo(expectedMessage); + await TUnitAssert.That(exception!.Message.NormalizeLineEndings().NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings()); } [Test] @@ -624,7 +624,7 @@ public async Task Object_With_Partial_Fields_Match_With_Full_Equivalency_Are_Not $"Received: Int32{Environment.NewLine}" + $"{Environment.NewLine}" + $"at Assert.That(object1).IsEquivalentTo(object2)"; - await TUnitAssert.That(exception!.Message).IsEqualTo(expectedMessage); + await TUnitAssert.That(exception!.Message.NormalizeLineEndings().NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings()); } [Test] diff --git a/TUnit.Assertions.Tests/Old/StringEqualsAssertionTests.cs b/TUnit.Assertions.Tests/Old/StringEqualsAssertionTests.cs index 874606d7bf..7768d5aab7 100644 --- a/TUnit.Assertions.Tests/Old/StringEqualsAssertionTests.cs +++ b/TUnit.Assertions.Tests/Old/StringEqualsAssertionTests.cs @@ -160,6 +160,6 @@ Volutpat vero est ea clita clita magna dolor nulla ipsum aliquyam nonumy. $" ↑{Environment.NewLine}" + $"{Environment.NewLine}" + $"at Assert.That(value1).IsEqualTo(value2)"; - await TUnitAssert.That(exception!.Message).IsEqualTo(expectedMessage); + await TUnitAssert.That(exception!.Message.NormalizeLineEndings().NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings()); } } diff --git a/TUnit.Assertions/Conditions/MappedSatisfiesAssertion.cs b/TUnit.Assertions/Conditions/MappedSatisfiesAssertion.cs index cf254143a4..518cb67ae6 100644 --- a/TUnit.Assertions/Conditions/MappedSatisfiesAssertion.cs +++ b/TUnit.Assertions/Conditions/MappedSatisfiesAssertion.cs @@ -75,16 +75,17 @@ protected override async Task CheckAsync(EvaluationMetadata m.GetNameAsync(), assert => assert.IsEqualTo("John")); /// -public class AsyncMappedSatisfiesAssertion : Assertion +public class AsyncMappedSatisfiesAssertion : Assertion + where TAssertion : Assertion { private readonly Func> _selector; - private readonly Func, Assertion?> _assertions; + private readonly Func, TAssertion> _assertions; private readonly string _selectorDescription; public AsyncMappedSatisfiesAssertion( AssertionContext context, Func> selector, - Func, Assertion?> assertions, + Func, TAssertion> assertions, string selectorDescription) : base(context) { diff --git a/TUnit.Assertions/Conditions/StringEqualsAssertion.cs b/TUnit.Assertions/Conditions/StringEqualsAssertion.cs index 99ee7e6af4..15b267ca0d 100644 --- a/TUnit.Assertions/Conditions/StringEqualsAssertion.cs +++ b/TUnit.Assertions/Conditions/StringEqualsAssertion.cs @@ -6,10 +6,10 @@ namespace TUnit.Assertions.Conditions; /// /// Asserts that a string is equal to an expected value. -/// Demonstrates multiple custom methods WITHOUT wrappers! +/// Generic to support both nullable and non-nullable string sources. /// -[AssertionExtension("IsEqualTo", OverloadResolutionPriority = 2)] -public class StringEqualsAssertion : Assertion +[AssertionExtension("IsEqualTo")] +public class StringEqualsAssertion : Assertion { private readonly string? _expected; private StringComparison _comparison = StringComparison.Ordinal; @@ -18,7 +18,7 @@ public class StringEqualsAssertion : Assertion private bool _ignoringWhitespace; public StringEqualsAssertion( - AssertionContext context, + AssertionContext context, string? expected) : base(context) { @@ -26,7 +26,7 @@ public StringEqualsAssertion( } public StringEqualsAssertion( - AssertionContext context, + AssertionContext context, string? expected, StringComparison comparison) : base(context) @@ -38,7 +38,7 @@ public StringEqualsAssertion( /// /// Makes the comparison case-insensitive. /// - public StringEqualsAssertion IgnoringCase() + public StringEqualsAssertion IgnoringCase() { _comparison = StringComparison.OrdinalIgnoreCase; Context.ExpressionBuilder.Append(".IgnoringCase()"); @@ -48,7 +48,7 @@ public StringEqualsAssertion IgnoringCase() /// /// Specifies a custom string comparison type. /// - public StringEqualsAssertion WithComparison(StringComparison comparison) + public StringEqualsAssertion WithComparison(StringComparison comparison) { _comparison = comparison; Context.ExpressionBuilder.Append($".WithComparison({comparison})"); @@ -58,7 +58,7 @@ public StringEqualsAssertion WithComparison(StringComparison comparison) /// /// Trims both strings before comparing. /// - public StringEqualsAssertion WithTrimming() + public StringEqualsAssertion WithTrimming() { _trimming = true; Context.ExpressionBuilder.Append(".WithTrimming()"); @@ -68,7 +68,7 @@ public StringEqualsAssertion WithTrimming() /// /// Treats null and empty string as equal. /// - public StringEqualsAssertion WithNullAndEmptyEquality() + public StringEqualsAssertion WithNullAndEmptyEquality() { _nullAndEmptyEquality = true; Context.ExpressionBuilder.Append(".WithNullAndEmptyEquality()"); @@ -78,16 +78,16 @@ public StringEqualsAssertion WithNullAndEmptyEquality() /// /// Removes all whitespace from both strings before comparing. /// - public StringEqualsAssertion IgnoringWhitespace() + public StringEqualsAssertion IgnoringWhitespace() { _ignoringWhitespace = true; Context.ExpressionBuilder.Append(".IgnoringWhitespace()"); return this; } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { - var value = metadata.Value; + var value = metadata.Value as string; var exception = metadata.Exception; var actualValue = value; diff --git a/TUnit.Assertions/Core/AssertionContext.cs b/TUnit.Assertions/Core/AssertionContext.cs index 09ab90bc3b..7c7157f31b 100644 --- a/TUnit.Assertions/Core/AssertionContext.cs +++ b/TUnit.Assertions/Core/AssertionContext.cs @@ -153,4 +153,20 @@ internal void SetPendingLink(Assertion previous, CombinerType type) PendingLinkType = null; return result; } + + /// + /// Converts a non-nullable reference type context to its nullable equivalent. + /// This is safe because reference types and their nullable counterparts have identical runtime representations. + /// Used primarily by the assertion source generator to handle non-nullable to nullable conversions. + /// IMPORTANT: This should only be called when TValue is a reference type (class). + /// + /// The same context instance viewed as nullable + internal AssertionContext AsNullable() + { + // This cast is safe because for reference types, TValue and TValue? + // have the same runtime representation. We are only changing the + // compiler's static analysis view of the type. + // The generator ensures this is only called for reference types. + return (AssertionContext)(object)this; + } } diff --git a/TUnit.Assertions/Extensions/AssertionExtensions.cs b/TUnit.Assertions/Extensions/AssertionExtensions.cs index a902bdc7aa..1261b6fa33 100644 --- a/TUnit.Assertions/Extensions/AssertionExtensions.cs +++ b/TUnit.Assertions/Extensions/AssertionExtensions.cs @@ -742,14 +742,15 @@ public static NotStructuralEquivalencyAssertion IsNotEquivalentTo m.AsyncValue, assert => assert.IsEqualTo("Hello")); /// - public static AsyncMappedSatisfiesAssertion Satisfies( + public static AsyncMappedSatisfiesAssertion Satisfies( this IAssertionSource source, Func> selector, - Func, Assertion> assertions, + Func, TAssertion> assertions, [CallerArgumentExpression(nameof(selector))] string? selectorExpression = null) + where TAssertion : Assertion { source.Context.ExpressionBuilder.Append($".Satisfies({selectorExpression}, ...)"); - return new AsyncMappedSatisfiesAssertion( + return new AsyncMappedSatisfiesAssertion( source.Context, selector!, assertions, diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt index ff4415bf1a..cdefb8c89e 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -273,9 +273,10 @@ namespace .Conditions public . Context { get; } public . IsTypeOf() { } } - public class AsyncMappedSatisfiesAssertion : . + public class AsyncMappedSatisfiesAssertion : . + where TAssertion : . { - public AsyncMappedSatisfiesAssertion(. context, > selector, <., .?> assertions, string selectorDescription) { } + public AsyncMappedSatisfiesAssertion(. context, > selector, <., TAssertion> assertions, string selectorDescription) { } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } @@ -1070,18 +1071,18 @@ namespace .Conditions public . IgnoringCase() { } public . WithComparison( comparison) { } } - [.("IsEqualTo", OverloadResolutionPriority=2)] - public class StringEqualsAssertion : . + [.("IsEqualTo")] + public class StringEqualsAssertion : . { - public StringEqualsAssertion(. context, string? expected) { } - public StringEqualsAssertion(. context, string? expected, comparison) { } - protected override .<.> CheckAsync(. metadata) { } + public StringEqualsAssertion(. context, string? expected) { } + public StringEqualsAssertion(. context, string? expected, comparison) { } + protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } - public . IgnoringCase() { } - public . IgnoringWhitespace() { } - public . WithComparison( comparison) { } - public . WithNullAndEmptyEquality() { } - public . WithTrimming() { } + public . IgnoringCase() { } + public . IgnoringWhitespace() { } + public . WithComparison( comparison) { } + public . WithNullAndEmptyEquality() { } + public . WithTrimming() { } } [.("IsEmpty")] public class StringIsEmptyAssertion : . @@ -1640,8 +1641,9 @@ namespace .Extensions [.(3)] public static . Member(this . source, .<>> memberSelector, <.<., TKey, TValue>, .> assertions) { } public static . Satisfies(this . source, predicate, [.("predicate")] string? expression = null) { } - public static . Satisfies(this . source, > selector, <., .> assertions, [.("selector")] string? selectorExpression = null) { } public static . Satisfies(this . source, selector, <., .?> assertions, [.("selector")] string? selectorExpression = null) { } + public static . Satisfies(this . source, > selector, <., TAssertion> assertions, [.("selector")] string? selectorExpression = null) + where TAssertion : . { } public static . Throws(this . source) where TException : { } public static . Throws(this . source) @@ -3564,10 +3566,8 @@ namespace .Extensions } public static class StringEqualsAssertionExtensions { - [.(2)] - public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { } - [.(2)] - public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { } + public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } } public static class StringIsEmptyAssertionExtensions { diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt index 4724baca2c..738cb94062 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -270,9 +270,10 @@ namespace .Conditions public . Context { get; } public . IsTypeOf() { } } - public class AsyncMappedSatisfiesAssertion : . + public class AsyncMappedSatisfiesAssertion : . + where TAssertion : . { - public AsyncMappedSatisfiesAssertion(. context, > selector, <., .?> assertions, string selectorDescription) { } + public AsyncMappedSatisfiesAssertion(. context, > selector, <., TAssertion> assertions, string selectorDescription) { } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } @@ -1067,18 +1068,18 @@ namespace .Conditions public . IgnoringCase() { } public . WithComparison( comparison) { } } - [.("IsEqualTo", OverloadResolutionPriority=2)] - public class StringEqualsAssertion : . + [.("IsEqualTo")] + public class StringEqualsAssertion : . { - public StringEqualsAssertion(. context, string? expected) { } - public StringEqualsAssertion(. context, string? expected, comparison) { } - protected override .<.> CheckAsync(. metadata) { } + public StringEqualsAssertion(. context, string? expected) { } + public StringEqualsAssertion(. context, string? expected, comparison) { } + protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } - public . IgnoringCase() { } - public . IgnoringWhitespace() { } - public . WithComparison( comparison) { } - public . WithNullAndEmptyEquality() { } - public . WithTrimming() { } + public . IgnoringCase() { } + public . IgnoringWhitespace() { } + public . WithComparison( comparison) { } + public . WithNullAndEmptyEquality() { } + public . WithTrimming() { } } [.("IsEmpty")] public class StringIsEmptyAssertion : . @@ -1630,8 +1631,9 @@ namespace .Extensions public static . Member(this . source, .<> memberSelector, <., .> assertions) { } public static . Member(this . source, .<>> memberSelector, <.<., TKey, TValue>, .> assertions) { } public static . Satisfies(this . source, predicate, [.("predicate")] string? expression = null) { } - public static . Satisfies(this . source, > selector, <., .> assertions, [.("selector")] string? selectorExpression = null) { } public static . Satisfies(this . source, selector, <., .?> assertions, [.("selector")] string? selectorExpression = null) { } + public static . Satisfies(this . source, > selector, <., TAssertion> assertions, [.("selector")] string? selectorExpression = null) + where TAssertion : . { } public static . Throws(this . source) where TException : { } public static . Throws(this . source) @@ -3546,8 +3548,8 @@ namespace .Extensions } public static class StringEqualsAssertionExtensions { - public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { } - public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { } + public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } } public static class StringIsEmptyAssertionExtensions { diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt index aef5215e73..b5a64ac00a 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -273,9 +273,10 @@ namespace .Conditions public . Context { get; } public . IsTypeOf() { } } - public class AsyncMappedSatisfiesAssertion : . + public class AsyncMappedSatisfiesAssertion : . + where TAssertion : . { - public AsyncMappedSatisfiesAssertion(. context, > selector, <., .?> assertions, string selectorDescription) { } + public AsyncMappedSatisfiesAssertion(. context, > selector, <., TAssertion> assertions, string selectorDescription) { } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } @@ -1070,18 +1071,18 @@ namespace .Conditions public . IgnoringCase() { } public . WithComparison( comparison) { } } - [.("IsEqualTo", OverloadResolutionPriority=2)] - public class StringEqualsAssertion : . + [.("IsEqualTo")] + public class StringEqualsAssertion : . { - public StringEqualsAssertion(. context, string? expected) { } - public StringEqualsAssertion(. context, string? expected, comparison) { } - protected override .<.> CheckAsync(. metadata) { } + public StringEqualsAssertion(. context, string? expected) { } + public StringEqualsAssertion(. context, string? expected, comparison) { } + protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } - public . IgnoringCase() { } - public . IgnoringWhitespace() { } - public . WithComparison( comparison) { } - public . WithNullAndEmptyEquality() { } - public . WithTrimming() { } + public . IgnoringCase() { } + public . IgnoringWhitespace() { } + public . WithComparison( comparison) { } + public . WithNullAndEmptyEquality() { } + public . WithTrimming() { } } [.("IsEmpty")] public class StringIsEmptyAssertion : . @@ -1640,8 +1641,9 @@ namespace .Extensions [.(3)] public static . Member(this . source, .<>> memberSelector, <.<., TKey, TValue>, .> assertions) { } public static . Satisfies(this . source, predicate, [.("predicate")] string? expression = null) { } - public static . Satisfies(this . source, > selector, <., .> assertions, [.("selector")] string? selectorExpression = null) { } public static . Satisfies(this . source, selector, <., .?> assertions, [.("selector")] string? selectorExpression = null) { } + public static . Satisfies(this . source, > selector, <., TAssertion> assertions, [.("selector")] string? selectorExpression = null) + where TAssertion : . { } public static . Throws(this . source) where TException : { } public static . Throws(this . source) @@ -3564,10 +3566,8 @@ namespace .Extensions } public static class StringEqualsAssertionExtensions { - [.(2)] - public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { } - [.(2)] - public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { } + public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } } public static class StringIsEmptyAssertionExtensions { diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt index 94d42ce41d..b0ea263886 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -270,9 +270,10 @@ namespace .Conditions public . Context { get; } public . IsTypeOf() { } } - public class AsyncMappedSatisfiesAssertion : . + public class AsyncMappedSatisfiesAssertion : . + where TAssertion : . { - public AsyncMappedSatisfiesAssertion(. context, > selector, <., .?> assertions, string selectorDescription) { } + public AsyncMappedSatisfiesAssertion(. context, > selector, <., TAssertion> assertions, string selectorDescription) { } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } @@ -997,18 +998,18 @@ namespace .Conditions public . IgnoringCase() { } public . WithComparison( comparison) { } } - [.("IsEqualTo", OverloadResolutionPriority=2)] - public class StringEqualsAssertion : . + [.("IsEqualTo")] + public class StringEqualsAssertion : . { - public StringEqualsAssertion(. context, string? expected) { } - public StringEqualsAssertion(. context, string? expected, comparison) { } - protected override .<.> CheckAsync(. metadata) { } + public StringEqualsAssertion(. context, string? expected) { } + public StringEqualsAssertion(. context, string? expected, comparison) { } + protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } - public . IgnoringCase() { } - public . IgnoringWhitespace() { } - public . WithComparison( comparison) { } - public . WithNullAndEmptyEquality() { } - public . WithTrimming() { } + public . IgnoringCase() { } + public . IgnoringWhitespace() { } + public . WithComparison( comparison) { } + public . WithNullAndEmptyEquality() { } + public . WithTrimming() { } } [.("IsEmpty")] public class StringIsEmptyAssertion : . @@ -1513,8 +1514,9 @@ namespace .Extensions public static . Member(this . source, .<> memberSelector, <., .> assertions) { } public static . Member(this . source, .<>> memberSelector, <.<., TKey, TValue>, .> assertions) { } public static . Satisfies(this . source, predicate, [.("predicate")] string? expression = null) { } - public static . Satisfies(this . source, > selector, <., .> assertions, [.("selector")] string? selectorExpression = null) { } public static . Satisfies(this . source, selector, <., .?> assertions, [.("selector")] string? selectorExpression = null) { } + public static . Satisfies(this . source, > selector, <., TAssertion> assertions, [.("selector")] string? selectorExpression = null) + where TAssertion : . { } public static . Throws(this . source) where TException : { } public static . Throws(this . source) @@ -3116,8 +3118,8 @@ namespace .Extensions } public static class StringEqualsAssertionExtensions { - public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { } - public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { } + public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } } public static class StringIsEmptyAssertionExtensions {