From 95df4acd945e35dbd3c0ef22e0bc5a7c0d801baf Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:27:51 +0000 Subject: [PATCH 1/3] fix: resolve CS8620 and CS8619 warnings for Task.FromResult with nullable and non-nullable types --- TUnit.Assertions.Tests/Bugs/Issue3580Tests.cs | 122 ++++++++++++++++++ .../Conditions/StringEqualsAssertion.cs | 16 ++- TUnit.Assertions/Extensions/Assert.cs | 9 +- .../Extensions/AssertionExtensions.cs | 15 --- ...Has_No_API_Changes.DotNet10_0.verified.txt | 6 +- ..._Has_No_API_Changes.DotNet8_0.verified.txt | 5 +- ..._Has_No_API_Changes.DotNet9_0.verified.txt | 6 +- ...ary_Has_No_API_Changes.Net4_7.verified.txt | 5 +- 8 files changed, 156 insertions(+), 28 deletions(-) create mode 100644 TUnit.Assertions.Tests/Bugs/Issue3580Tests.cs diff --git a/TUnit.Assertions.Tests/Bugs/Issue3580Tests.cs b/TUnit.Assertions.Tests/Bugs/Issue3580Tests.cs new file mode 100644 index 0000000000..69238daf1b --- /dev/null +++ b/TUnit.Assertions.Tests/Bugs/Issue3580Tests.cs @@ -0,0 +1,122 @@ +namespace TUnit.Assertions.Tests.Bugs; + +/// +/// Tests for issue #3580: More warnings CS8620 and CS8619 after update to >= 0.72 +/// https://github.com/thomhurst/TUnit/issues/3580 +/// +public class Issue3580Tests +{ + [Test] + public async Task Task_FromResult_With_NonNullable_Value_And_Nullable_Expected_Should_Not_Cause_Warnings() + { + // Scenario 1: Both parameters nullable (from issue) + object? value = "test"; + object? expected = "test"; + + // This should not produce CS8620 or CS8619 warnings + await Assert.That(Task.FromResult(value)).IsEqualTo(expected); + } + + [Test] + public async Task Task_FromResult_With_NonNullable_Value_And_NonNullable_Expected_Should_Not_Cause_Warnings() + { + // Scenario 2: Non-nullable value with nullable expectation (from issue) + object value = "test"; + object? expected = "test"; + + // This should not produce CS8620 or CS8619 warnings + await Assert.That(Task.FromResult(value)).IsEqualTo(expected); + } + + [Test] + public async Task Task_FromResult_With_Both_NonNullable_Should_Not_Cause_Warnings() + { + // Scenario 3: Both non-nullable (from issue) + object value = "test"; + object expected = "test"; + + // This should not produce CS8619 warnings + await Assert.That(Task.FromResult(value)).IsEqualTo(expected); + } + + [Test] + public async Task Task_FromResult_With_String_Should_Not_Cause_Warnings() + { + // Test with string type (reference type) + var value = "hello"; + var expected = "hello"; + + await Assert.That(Task.FromResult(value)).IsEqualTo(expected); + } + + [Test] + public async Task Task_FromResult_With_ValueType_Should_Not_Cause_Warnings() + { + // Test with value type (int) + var value = 42; + var expected = 42; + + await Assert.That(Task.FromResult(value)).IsEqualTo(expected); + } + + [Test] + public async Task Task_FromResult_With_Null_Value_Should_Work() + { + // Test with null value + object? value = null; + + var result = await Task.FromResult(value); + await Assert.That(result).IsNull(); + } + + [Test] + public async Task Task_FromResult_With_Custom_Type_Should_Not_Cause_Warnings() + { + // Test with custom type + var value = new TestData { Value = "test" }; + var expected = value; + + var result = await Task.FromResult(value); + await Assert.That(result).IsSameReferenceAs(expected); + } + + [Test] + public async Task Async_Method_Returning_NonNullable_Task_Should_Not_Cause_Warnings() + { + // Test with async method that returns non-nullable task + // Just verify we can await it and get a non-null result + var result = await GetNonNullableValueAsync(); + + await Assert.That(result).IsNotNull(); + } + + [Test] + public async Task Task_FromResult_With_IsNotNull_Should_Not_Cause_Warnings() + { + // Test IsNotNull assertion + object value = "test"; + + var result = await Task.FromResult(value); + await Assert.That(result).IsNotNull(); + } + + [Test] + public async Task Task_FromResult_With_IsGreaterThan_Should_Not_Cause_Warnings() + { + // Test numeric comparison + var value = 10; + + await Assert.That(Task.FromResult(value)).IsGreaterThan(5); + } + + private static async Task GetNonNullableValueAsync() + { + await Task.Yield(); + return new object(); + } + + private class TestData + { + public string Value { get; set; } = string.Empty; + } +} diff --git a/TUnit.Assertions/Conditions/StringEqualsAssertion.cs b/TUnit.Assertions/Conditions/StringEqualsAssertion.cs index 973e69f6a6..0c29255d19 100644 --- a/TUnit.Assertions/Conditions/StringEqualsAssertion.cs +++ b/TUnit.Assertions/Conditions/StringEqualsAssertion.cs @@ -13,9 +13,9 @@ public class StringEqualsAssertion : Assertion { private readonly string? _expected; private StringComparison _comparison = StringComparison.Ordinal; - private bool _trimming = false; - private bool _nullAndEmptyEquality = false; - private bool _ignoringWhitespace = false; + private bool _trimming; + private bool _nullAndEmptyEquality; + private bool _ignoringWhitespace; public StringEqualsAssertion( AssertionContext context, @@ -25,6 +25,16 @@ public StringEqualsAssertion( _expected = expected; } + public StringEqualsAssertion( + AssertionContext context, + string? expected, + StringComparison comparison) + : base(context) + { + _expected = expected; + _comparison = comparison; + } + /// /// Makes the comparison case-insensitive. /// diff --git a/TUnit.Assertions/Extensions/Assert.cs b/TUnit.Assertions/Extensions/Assert.cs index 1cd31aee78..fbc5c78e7e 100644 --- a/TUnit.Assertions/Extensions/Assert.cs +++ b/TUnit.Assertions/Extensions/Assert.cs @@ -111,14 +111,19 @@ public static AsyncFuncAssertion That( /// /// Creates an assertion for a Task that returns a value. /// Supports both result assertions (e.g., IsEqualTo) and task state assertions (e.g., IsCompleted). + /// This method accepts both nullable and non-nullable Task results seamlessly. /// Example: await Assert.That(GetValueAsync()).IsEqualTo(expected); + /// Example: await Assert.That(Task.FromResult(value)).IsEqualTo(expected); /// Example: await Assert.That(GetValueAsync()).IsCompleted(); /// public static TaskAssertion That( - Task task, + Task task, [CallerArgumentExpression(nameof(task))] string? expression = null) { - return new TaskAssertion(task, expression); + // Convert Task to Task to handle both nullable and non-nullable scenarios + // This eliminates CS8620/CS8619 warnings when using Task.FromResult with non-nullable types + var nullableTask = task.ContinueWith(t => (TValue?)t.Result, TaskContinuationOptions.ExecuteSynchronously); + return new TaskAssertion(nullableTask, expression); } /// diff --git a/TUnit.Assertions/Extensions/AssertionExtensions.cs b/TUnit.Assertions/Extensions/AssertionExtensions.cs index cf6e6e5a74..a902bdc7aa 100644 --- a/TUnit.Assertions/Extensions/AssertionExtensions.cs +++ b/TUnit.Assertions/Extensions/AssertionExtensions.cs @@ -75,21 +75,6 @@ public static EqualsAssertion EqualTo( return new EqualsAssertion(source.Context, expected); } - /// - /// Asserts that the string is equal to the expected value using the specified comparison. - /// Uses .WithComparison() since StringEqualsAssertion doesn't have a constructor for this. - /// - public static StringEqualsAssertion IsEqualTo( - this IAssertionSource source, - string? expected, - StringComparison comparison, - [CallerArgumentExpression(nameof(expected))] string? expression = null) - { - source.Context.ExpressionBuilder.Append($".IsEqualTo({expression}, {comparison})"); - var assertion = new StringEqualsAssertion(source.Context, expected); - return assertion.WithComparison(comparison); - } - /// /// Asserts that the numeric value is greater than zero (positive). /// 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 fa73df8d0f..108a42a252 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 @@ -23,7 +23,7 @@ namespace public static . That(.? value, [.("value")] string? expression = null) { } public static . That(<.> func, [.("func")] string? expression = null) { } public static . That( func, [.("func")] string? expression = null) { } - public static . That(. task, [.("task")] string? expression = null) { } + public static . That(. task, [.("task")] string? expression = null) { } public static . That(TValue? value, [.("value")] string? expression = null) { } [.(3)] public static . That(. value, [.("value")] string? expression = null) { } @@ -1400,6 +1400,7 @@ namespace .Conditions public class StringEqualsAssertion : . { public StringEqualsAssertion(. context, string? expected) { } + public StringEqualsAssertion(. context, string? expected, comparison) { } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } public . IgnoringCase() { } @@ -1977,7 +1978,6 @@ namespace .Extensions public static .<> IsBeforeOrEqualTo(this .<> source, expected, [.("expected")] string? expression = null) { } public static ..IsDefinedAssertion IsDefined(this . source) where TEnum : struct, { } - public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expression = null) { } [.("Uses reflection to compare members")] public static . IsEquivalentTo(this . source, object? expected, [.("expected")] string? expression = null) { } public static . IsNegative(this . source) @@ -3760,6 +3760,8 @@ namespace .Extensions { [.(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 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 74d83c96fd..276ac64fda 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 @@ -21,7 +21,7 @@ namespace public static . That(.? value, [.("value")] string? expression = null) { } public static . That(<.> func, [.("func")] string? expression = null) { } public static . That( func, [.("func")] string? expression = null) { } - public static . That(. task, [.("task")] string? expression = null) { } + public static . That(. task, [.("task")] string? expression = null) { } public static . That(TValue? value, [.("value")] string? expression = null) { } public static . That(. value, [.("value")] string? expression = null) { } public static Throws( exceptionType, action) { } @@ -1397,6 +1397,7 @@ namespace .Conditions public class StringEqualsAssertion : . { public StringEqualsAssertion(. context, string? expected) { } + public StringEqualsAssertion(. context, string? expected, comparison) { } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } public . IgnoringCase() { } @@ -1974,7 +1975,6 @@ namespace .Extensions public static .<> IsBeforeOrEqualTo(this .<> source, expected, [.("expected")] string? expression = null) { } public static ..IsDefinedAssertion IsDefined(this . source) where TEnum : struct, { } - public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expression = null) { } [.("Uses reflection to compare members")] public static . IsEquivalentTo(this . source, object? expected, [.("expected")] string? expression = null) { } public static . IsNegative(this . source) @@ -3741,6 +3741,7 @@ 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 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 a13bbb36f9..cb85a09ae9 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 @@ -23,7 +23,7 @@ namespace public static . That(.? value, [.("value")] string? expression = null) { } public static . That(<.> func, [.("func")] string? expression = null) { } public static . That( func, [.("func")] string? expression = null) { } - public static . That(. task, [.("task")] string? expression = null) { } + public static . That(. task, [.("task")] string? expression = null) { } public static . That(TValue? value, [.("value")] string? expression = null) { } [.(3)] public static . That(. value, [.("value")] string? expression = null) { } @@ -1400,6 +1400,7 @@ namespace .Conditions public class StringEqualsAssertion : . { public StringEqualsAssertion(. context, string? expected) { } + public StringEqualsAssertion(. context, string? expected, comparison) { } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } public . IgnoringCase() { } @@ -1977,7 +1978,6 @@ namespace .Extensions public static .<> IsBeforeOrEqualTo(this .<> source, expected, [.("expected")] string? expression = null) { } public static ..IsDefinedAssertion IsDefined(this . source) where TEnum : struct, { } - public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expression = null) { } [.("Uses reflection to compare members")] public static . IsEquivalentTo(this . source, object? expected, [.("expected")] string? expression = null) { } public static . IsNegative(this . source) @@ -3760,6 +3760,8 @@ namespace .Extensions { [.(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 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 499b883a65..c415d7ee64 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 @@ -21,7 +21,7 @@ namespace public static . That(.? value, [.("value")] string? expression = null) { } public static . That(<.> func, [.("func")] string? expression = null) { } public static . That( func, [.("func")] string? expression = null) { } - public static . That(. task, [.("task")] string? expression = null) { } + public static . That(. task, [.("task")] string? expression = null) { } public static . That(TValue? value, [.("value")] string? expression = null) { } public static . That(. value, [.("value")] string? expression = null) { } public static Throws( exceptionType, action) { } @@ -1285,6 +1285,7 @@ namespace .Conditions public class StringEqualsAssertion : . { public StringEqualsAssertion(. context, string? expected) { } + public StringEqualsAssertion(. context, string? expected, comparison) { } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } public . IgnoringCase() { } @@ -1806,7 +1807,6 @@ namespace .Extensions public static .<> IsBeforeOrEqualTo(this .<> source, expected, [.("expected")] string? expression = null) { } public static ..IsDefinedAssertion IsDefined(this . source) where TEnum : struct, { } - public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expression = null) { } public static . IsEquivalentTo(this . source, object? expected, [.("expected")] string? expression = null) { } public static . IsNegative(this . source) where TValue : { } @@ -3256,6 +3256,7 @@ 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 class StringIsEmptyAssertionExtensions { From 3ddb0d622e031fc58cac83204a44180f01c0d826 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:33:32 +0000 Subject: [PATCH 2/3] fix: resolve CS8620 and CS8619 warnings for Task.FromResult with nullable and non-nullable types --- TUnit.Assertions/Extensions/Assert.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/TUnit.Assertions/Extensions/Assert.cs b/TUnit.Assertions/Extensions/Assert.cs index fbc5c78e7e..2e4a731f2e 100644 --- a/TUnit.Assertions/Extensions/Assert.cs +++ b/TUnit.Assertions/Extensions/Assert.cs @@ -120,10 +120,14 @@ public static TaskAssertion That( Task task, [CallerArgumentExpression(nameof(task))] string? expression = null) { - // Convert Task to Task to handle both nullable and non-nullable scenarios - // This eliminates CS8620/CS8619 warnings when using Task.FromResult with non-nullable types - var nullableTask = task.ContinueWith(t => (TValue?)t.Result, TaskContinuationOptions.ExecuteSynchronously); + var nullableTask = ConvertToNullableTask(task); return new TaskAssertion(nullableTask, expression); + + static async Task ConvertToNullableTask(Task sourceTask) + { + var result = await sourceTask.ConfigureAwait(false); + return result; + } } /// From 4b157596acc4f47306d404ffea9f6ec05f82c10f Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:45:01 +0000 Subject: [PATCH 3/3] fix: correct error message in EqualsAssertionTests for StringComparison.Ordinal --- TUnit.Assertions.Tests/Old/EqualsAssertionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TUnit.Assertions.Tests/Old/EqualsAssertionTests.cs b/TUnit.Assertions.Tests/Old/EqualsAssertionTests.cs index 01cb5f75cc..0a9c64d653 100644 --- a/TUnit.Assertions.Tests/Old/EqualsAssertionTests.cs +++ b/TUnit.Assertions.Tests/Old/EqualsAssertionTests.cs @@ -11,7 +11,7 @@ await TUnitAssert.That(async () => await TUnitAssert.That(one).IsEqualTo("2", StringComparison.Ordinal).And.IsNotEqualTo("1").And.IsOfType(typeof(string)) ).ThrowsException() .And - .HasMessageContaining("Assert.That(one).IsEqualTo(\"2\", Ordinal)"); + .HasMessageContaining("Assert.That(one).IsEqualTo(\"2\", StringComparison.Ordinal)"); } [Test]