diff --git a/TUnit.Assertions.Analyzers.Tests/CollectionIsEqualToAnalyzerTests.cs b/TUnit.Assertions.Analyzers.Tests/CollectionIsEqualToAnalyzerTests.cs index 5ee2399049..94cfbfb42b 100644 --- a/TUnit.Assertions.Analyzers.Tests/CollectionIsEqualToAnalyzerTests.cs +++ b/TUnit.Assertions.Analyzers.Tests/CollectionIsEqualToAnalyzerTests.cs @@ -4,6 +4,13 @@ namespace TUnit.Assertions.Analyzers.Tests; public class CollectionIsEqualToAnalyzerTests { + // NOTE: Snippets that pass an expected of the same type as the source use the + // explicit type argument `IsEqualTo(...)` so the analyzer-test compiler + // (Roslyn 4.8 — bound by the testing harness) does not raise CS0121 between + // the source-generated `IsEqualTo` and the implicit-conversion-aware + // `IsEqualTo`. Roslyn 4.12+ honours [OverloadResolutionPriority] + // and disambiguates automatically, so this is purely a test-infrastructure + // workaround and does not affect users. [Test] public async Task List_IsEqualTo_Raises_Info() { @@ -23,7 +30,7 @@ public async Task Test() { var a = new List { 1, 2, 3 }; var b = new List { 1, 2, 3 }; - await Assert.That(a).{|#0:IsEqualTo(b)|}; + await Assert.That(a).{|#0:IsEqualTo>(b)|}; } } """, @@ -93,7 +100,7 @@ public async Task Test() { int[] a = { 1, 2 }; int[] b = { 1, 2 }; - await Assert.That(a).{|#0:IsEqualTo(b)|}; + await Assert.That(a).{|#0:IsEqualTo(b)|}; } } """, diff --git a/TUnit.Assertions.Analyzers.Tests/PreferIsTrueOrIsFalseAnalyzerTests.cs b/TUnit.Assertions.Analyzers.Tests/PreferIsTrueOrIsFalseAnalyzerTests.cs index 7d648b38f4..a92815c8cb 100644 --- a/TUnit.Assertions.Analyzers.Tests/PreferIsTrueOrIsFalseAnalyzerTests.cs +++ b/TUnit.Assertions.Analyzers.Tests/PreferIsTrueOrIsFalseAnalyzerTests.cs @@ -4,6 +4,12 @@ namespace TUnit.Assertions.Analyzers.Tests; public class PreferIsTrueOrIsFalseAnalyzerTests { + // NOTE: Snippets use the explicit type argument `IsEqualTo(...)` so the + // analyzer-test compiler (Roslyn 4.8 — bound by the testing harness) does not + // raise CS0121 between the source-generated `IsEqualTo` and the + // implicit-conversion-aware `IsEqualTo`. Roslyn 4.12+ honours + // [OverloadResolutionPriority] and disambiguates without the explicit arg, so + // this is purely a test-infrastructure workaround and does not affect users. [Test] public async Task IsEqualTo_True_Is_Flagged() { @@ -21,7 +27,7 @@ public class MyClass public async Task MyTest() { var value = true; - await {|#0:Assert.That(value).IsEqualTo(true)|}; + await {|#0:Assert.That(value).IsEqualTo(true)|}; } } """, @@ -49,7 +55,7 @@ public class MyClass public async Task MyTest() { var value = false; - await {|#0:Assert.That(value).IsEqualTo(false)|}; + await {|#0:Assert.That(value).IsEqualTo(false)|}; } } """, diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithDefaultValues.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithDefaultValues.DotNet10_0.verified.txt index 1bc9aba825..7477905bf8 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithDefaultValues.DotNet10_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithDefaultValues.DotNet10_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for DefaultValuesAssertion. /// -public static class DefaultValuesAssertionExtensions +public static partial class DefaultValuesAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithDefaultValues.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithDefaultValues.DotNet8_0.verified.txt index 1bc9aba825..7477905bf8 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithDefaultValues.DotNet8_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithDefaultValues.DotNet8_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for DefaultValuesAssertion. /// -public static class DefaultValuesAssertionExtensions +public static partial class DefaultValuesAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithDefaultValues.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithDefaultValues.DotNet9_0.verified.txt index 1bc9aba825..7477905bf8 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithDefaultValues.DotNet9_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithDefaultValues.DotNet9_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for DefaultValuesAssertion. /// -public static class DefaultValuesAssertionExtensions +public static partial class DefaultValuesAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithDefaultValues.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithDefaultValues.Net4_7.verified.txt index 1bc9aba825..7477905bf8 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithDefaultValues.Net4_7.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithDefaultValues.Net4_7.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for DefaultValuesAssertion. /// -public static class DefaultValuesAssertionExtensions +public static partial class DefaultValuesAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithEnumDefault.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithEnumDefault.DotNet10_0.verified.txt index d8a843708c..9245c1cf6e 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithEnumDefault.DotNet10_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithEnumDefault.DotNet10_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for StringComparisonAssertion. /// -public static class StringComparisonAssertionExtensions +public static partial class StringComparisonAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithEnumDefault.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithEnumDefault.DotNet8_0.verified.txt index d8a843708c..9245c1cf6e 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithEnumDefault.DotNet8_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithEnumDefault.DotNet8_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for StringComparisonAssertion. /// -public static class StringComparisonAssertionExtensions +public static partial class StringComparisonAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithEnumDefault.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithEnumDefault.DotNet9_0.verified.txt index d8a843708c..9245c1cf6e 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithEnumDefault.DotNet9_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithEnumDefault.DotNet9_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for StringComparisonAssertion. /// -public static class StringComparisonAssertionExtensions +public static partial class StringComparisonAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithEnumDefault.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithEnumDefault.Net4_7.verified.txt index d8a843708c..9245c1cf6e 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithEnumDefault.Net4_7.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithEnumDefault.Net4_7.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for StringComparisonAssertion. /// -public static class StringComparisonAssertionExtensions +public static partial class StringComparisonAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithGenericConstraints.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithGenericConstraints.DotNet10_0.verified.txt index 2497c7a9c3..16d59c3df3 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithGenericConstraints.DotNet10_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithGenericConstraints.DotNet10_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for GreaterThanAssertion. /// -public static class GreaterThanAssertionExtensions +public static partial class GreaterThanAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithGenericConstraints.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithGenericConstraints.DotNet8_0.verified.txt index 2497c7a9c3..16d59c3df3 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithGenericConstraints.DotNet8_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithGenericConstraints.DotNet8_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for GreaterThanAssertion. /// -public static class GreaterThanAssertionExtensions +public static partial class GreaterThanAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithGenericConstraints.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithGenericConstraints.DotNet9_0.verified.txt index 2497c7a9c3..16d59c3df3 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithGenericConstraints.DotNet9_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithGenericConstraints.DotNet9_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for GreaterThanAssertion. /// -public static class GreaterThanAssertionExtensions +public static partial class GreaterThanAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithGenericConstraints.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithGenericConstraints.Net4_7.verified.txt index 2497c7a9c3..16d59c3df3 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithGenericConstraints.Net4_7.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithGenericConstraints.Net4_7.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for GreaterThanAssertion. /// -public static class GreaterThanAssertionExtensions +public static partial class GreaterThanAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleConstructors.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleConstructors.DotNet10_0.verified.txt index d603f4ef32..69cb2805cc 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleConstructors.DotNet10_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleConstructors.DotNet10_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for EqualsAssertion. /// -public static class EqualsAssertionExtensions +public static partial class EqualsAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleConstructors.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleConstructors.DotNet8_0.verified.txt index d603f4ef32..69cb2805cc 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleConstructors.DotNet8_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleConstructors.DotNet8_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for EqualsAssertion. /// -public static class EqualsAssertionExtensions +public static partial class EqualsAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleConstructors.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleConstructors.DotNet9_0.verified.txt index d603f4ef32..69cb2805cc 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleConstructors.DotNet9_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleConstructors.DotNet9_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for EqualsAssertion. /// -public static class EqualsAssertionExtensions +public static partial class EqualsAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleConstructors.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleConstructors.Net4_7.verified.txt index d603f4ef32..69cb2805cc 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleConstructors.Net4_7.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleConstructors.Net4_7.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for EqualsAssertion. /// -public static class EqualsAssertionExtensions +public static partial class EqualsAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleParameters.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleParameters.DotNet10_0.verified.txt index 894271df3d..51e2278585 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleParameters.DotNet10_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleParameters.DotNet10_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for BetweenAssertion. /// -public static class BetweenAssertionExtensions +public static partial class BetweenAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleParameters.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleParameters.DotNet8_0.verified.txt index 894271df3d..51e2278585 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleParameters.DotNet8_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleParameters.DotNet8_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for BetweenAssertion. /// -public static class BetweenAssertionExtensions +public static partial class BetweenAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleParameters.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleParameters.DotNet9_0.verified.txt index 894271df3d..51e2278585 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleParameters.DotNet9_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleParameters.DotNet9_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for BetweenAssertion. /// -public static class BetweenAssertionExtensions +public static partial class BetweenAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleParameters.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleParameters.Net4_7.verified.txt index 894271df3d..51e2278585 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleParameters.Net4_7.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithMultipleParameters.Net4_7.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for BetweenAssertion. /// -public static class BetweenAssertionExtensions +public static partial class BetweenAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithNegatedMethod.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithNegatedMethod.DotNet10_0.verified.txt index 4d945048c0..3d9dd576a8 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithNegatedMethod.DotNet10_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithNegatedMethod.DotNet10_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for TrueAssertion. /// -public static class TrueAssertionExtensions +public static partial class TrueAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithNegatedMethod.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithNegatedMethod.DotNet8_0.verified.txt index 4d945048c0..3d9dd576a8 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithNegatedMethod.DotNet8_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithNegatedMethod.DotNet8_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for TrueAssertion. /// -public static class TrueAssertionExtensions +public static partial class TrueAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithNegatedMethod.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithNegatedMethod.DotNet9_0.verified.txt index 4d945048c0..3d9dd576a8 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithNegatedMethod.DotNet9_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithNegatedMethod.DotNet9_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for TrueAssertion. /// -public static class TrueAssertionExtensions +public static partial class TrueAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithNegatedMethod.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithNegatedMethod.Net4_7.verified.txt index 4d945048c0..3d9dd576a8 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithNegatedMethod.Net4_7.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithNegatedMethod.Net4_7.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for TrueAssertion. /// -public static class TrueAssertionExtensions +public static partial class TrueAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithOptionalParameter.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithOptionalParameter.DotNet10_0.verified.txt index e177f25be6..b9ffc18673 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithOptionalParameter.DotNet10_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithOptionalParameter.DotNet10_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for NotEqualsAssertion. /// -public static class NotEqualsAssertionExtensions +public static partial class NotEqualsAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithOptionalParameter.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithOptionalParameter.DotNet8_0.verified.txt index e177f25be6..b9ffc18673 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithOptionalParameter.DotNet8_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithOptionalParameter.DotNet8_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for NotEqualsAssertion. /// -public static class NotEqualsAssertionExtensions +public static partial class NotEqualsAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithOptionalParameter.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithOptionalParameter.DotNet9_0.verified.txt index e177f25be6..b9ffc18673 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithOptionalParameter.DotNet9_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithOptionalParameter.DotNet9_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for NotEqualsAssertion. /// -public static class NotEqualsAssertionExtensions +public static partial class NotEqualsAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithOptionalParameter.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithOptionalParameter.Net4_7.verified.txt index e177f25be6..b9ffc18673 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithOptionalParameter.Net4_7.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.AssertionWithOptionalParameter.Net4_7.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for NotEqualsAssertion. /// -public static class NotEqualsAssertionExtensions +public static partial class NotEqualsAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.MultipleGenericParameters.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.MultipleGenericParameters.DotNet10_0.verified.txt index c44e2eeba7..d649af8312 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.MultipleGenericParameters.DotNet10_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.MultipleGenericParameters.DotNet10_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for IsAssignableToAssertion. /// -public static class IsAssignableToAssertionExtensions +public static partial class IsAssignableToAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.MultipleGenericParameters.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.MultipleGenericParameters.DotNet8_0.verified.txt index c44e2eeba7..d649af8312 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.MultipleGenericParameters.DotNet8_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.MultipleGenericParameters.DotNet8_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for IsAssignableToAssertion. /// -public static class IsAssignableToAssertionExtensions +public static partial class IsAssignableToAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.MultipleGenericParameters.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.MultipleGenericParameters.DotNet9_0.verified.txt index c44e2eeba7..d649af8312 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.MultipleGenericParameters.DotNet9_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.MultipleGenericParameters.DotNet9_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for IsAssignableToAssertion. /// -public static class IsAssignableToAssertionExtensions +public static partial class IsAssignableToAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.MultipleGenericParameters.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.MultipleGenericParameters.Net4_7.verified.txt index c44e2eeba7..d649af8312 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.MultipleGenericParameters.Net4_7.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.MultipleGenericParameters.Net4_7.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for IsAssignableToAssertion. /// -public static class IsAssignableToAssertionExtensions +public static partial class IsAssignableToAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.NonGenericAssertion.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.NonGenericAssertion.DotNet10_0.verified.txt index 0e71d35e99..0e56361ed7 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.NonGenericAssertion.DotNet10_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.NonGenericAssertion.DotNet10_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for StringIsEmptyAssertion. /// -public static class StringIsEmptyAssertionExtensions +public static partial class StringIsEmptyAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.NonGenericAssertion.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.NonGenericAssertion.DotNet8_0.verified.txt index 0e71d35e99..0e56361ed7 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.NonGenericAssertion.DotNet8_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.NonGenericAssertion.DotNet8_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for StringIsEmptyAssertion. /// -public static class StringIsEmptyAssertionExtensions +public static partial class StringIsEmptyAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.NonGenericAssertion.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.NonGenericAssertion.DotNet9_0.verified.txt index 0e71d35e99..0e56361ed7 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.NonGenericAssertion.DotNet9_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.NonGenericAssertion.DotNet9_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for StringIsEmptyAssertion. /// -public static class StringIsEmptyAssertionExtensions +public static partial class StringIsEmptyAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.NonGenericAssertion.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.NonGenericAssertion.Net4_7.verified.txt index 0e71d35e99..0e56361ed7 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.NonGenericAssertion.Net4_7.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.NonGenericAssertion.Net4_7.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for StringIsEmptyAssertion. /// -public static class StringIsEmptyAssertionExtensions +public static partial class StringIsEmptyAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.SingleGenericParameter.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.SingleGenericParameter.DotNet10_0.verified.txt index 3e4ab6af6a..a8d32ef43b 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.SingleGenericParameter.DotNet10_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.SingleGenericParameter.DotNet10_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for NullAssertion. /// -public static class NullAssertionExtensions +public static partial class NullAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.SingleGenericParameter.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.SingleGenericParameter.DotNet8_0.verified.txt index 3e4ab6af6a..a8d32ef43b 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.SingleGenericParameter.DotNet8_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.SingleGenericParameter.DotNet8_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for NullAssertion. /// -public static class NullAssertionExtensions +public static partial class NullAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.SingleGenericParameter.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.SingleGenericParameter.DotNet9_0.verified.txt index 3e4ab6af6a..a8d32ef43b 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.SingleGenericParameter.DotNet9_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.SingleGenericParameter.DotNet9_0.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for NullAssertion. /// -public static class NullAssertionExtensions +public static partial class NullAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.SingleGenericParameter.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.SingleGenericParameter.Net4_7.verified.txt index 3e4ab6af6a..a8d32ef43b 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.SingleGenericParameter.Net4_7.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.SingleGenericParameter.Net4_7.verified.txt @@ -12,7 +12,7 @@ namespace TUnit.Assertions.Extensions; /// /// Generated extension methods for NullAssertion. /// -public static class NullAssertionExtensions +public static partial class NullAssertionExtensions { /// diff --git a/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs b/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs index 85bc5b6f06..e1c09f5c23 100644 --- a/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs +++ b/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs @@ -159,7 +159,7 @@ private static void GenerateExtensionMethods(SourceProductionContext context, As sourceBuilder.AppendLine($"/// "); sourceBuilder.AppendLine($"/// Generated extension methods for {data.ClassSymbol.Name}."); sourceBuilder.AppendLine($"/// "); - sourceBuilder.AppendLine($"public static class {extensionClassName}"); + sourceBuilder.AppendLine($"public static partial class {extensionClassName}"); sourceBuilder.AppendLine("{"); // Generate extension methods for each constructor diff --git a/TUnit.Assertions.Tests/Bugs/Issue5720Tests.cs b/TUnit.Assertions.Tests/Bugs/Issue5720Tests.cs new file mode 100644 index 0000000000..7081355ec3 --- /dev/null +++ b/TUnit.Assertions.Tests/Bugs/Issue5720Tests.cs @@ -0,0 +1,193 @@ +namespace TUnit.Assertions.Tests.Bugs; + +/// +/// Regression tests for GitHub issue #5720: +/// Wrapper Value Objects with an implicit conversion to a primitive (e.g. +/// ProductCode with implicit operator string) should be comparable +/// against values of the underlying primitive without an explicit cast or +/// .Value access: +/// +/// +/// await Assert.That(updatedStockItem.ProductCode).IsEqualTo("Example"); +/// +/// +/// Before 1.39.0 this worked for string wrappers because Assert.That(string?) +/// silently accepted the implicitly-converted value. The new generalized solution +/// adds IsEqualTo<TValue, TOther> overloads that detect a user-defined +/// implicit operator at runtime and compare the converted values. +/// +public class Issue5720Tests +{ + public sealed record ProductCode(string Value) + { + public static implicit operator string(ProductCode pc) => pc.Value; + } + + public readonly record struct WrappedNumber(int Value) + { + public static implicit operator int(WrappedNumber w) => w.Value; + } + + public sealed record StockItem(ProductCode ProductCode, WrappedNumber Quantity); + + /// + /// Source type with no operators of its own — the implicit conversion to + /// is declared on the target. Exercises the FindImplicitOperatorOnTarget fallback. + /// + public sealed record SourceWithoutOperators(string Value); + + public sealed record TargetWithIncoming(string Value) + { + public static implicit operator TargetWithIncoming(SourceWithoutOperators s) => new(s.Value); + } + + /// + /// Type with no implicit conversion to or from . Used to verify + /// the diagnostic thrown when no implicit operator can be found. + /// + public sealed record UnrelatedType(string Value); + + [Test] + public async Task IsEqualTo_StringWrapper_Against_Primitive_Passes() + { + var stock = new StockItem(new ProductCode("Example"), new WrappedNumber(5)); + + await Assert.That(stock.ProductCode).IsEqualTo("Example"); + } + + [Test] + public async Task IsEqualTo_StringWrapper_Against_Primitive_Fails_When_Different() + { + var stock = new StockItem(new ProductCode("Example"), new WrappedNumber(5)); + + await Assert.That(async () => await Assert.That(stock.ProductCode).IsEqualTo("Other")) + .Throws(); + } + + [Test] + public async Task IsEqualTo_IntWrapper_Against_Primitive_Passes() + { + var stock = new StockItem(new ProductCode("Example"), new WrappedNumber(5)); + + await Assert.That(stock.Quantity).IsEqualTo(5); + } + + [Test] + public async Task IsEqualTo_IntWrapper_Against_Primitive_Fails_When_Different() + { + var stock = new StockItem(new ProductCode("Example"), new WrappedNumber(5)); + + await Assert.That(async () => await Assert.That(stock.Quantity).IsEqualTo(6)) + .Throws(); + } + + [Test] + public async Task IsNotEqualTo_StringWrapper_Against_Primitive_Passes_When_Different() + { + var stock = new StockItem(new ProductCode("Example"), new WrappedNumber(5)); + + await Assert.That(stock.ProductCode).IsNotEqualTo("Other"); + } + + [Test] + public async Task IsNotEqualTo_IntWrapper_Against_Primitive_Passes_When_Different() + { + var stock = new StockItem(new ProductCode("Example"), new WrappedNumber(5)); + + await Assert.That(stock.Quantity).IsNotEqualTo(6); + } + + [Test] + public async Task IsNotEqualTo_IntWrapper_Against_Primitive_Fails_When_Equal() + { + var stock = new StockItem(new ProductCode("Example"), new WrappedNumber(5)); + + await Assert.That(async () => await Assert.That(stock.Quantity).IsNotEqualTo(5)) + .Throws(); + } + + [Test] + public async Task IsEqualTo_SameType_Wrapper_Still_Compares_By_Default_Equality() + { + // When both sides are the same wrapper type, the original same-type overload + // is selected and the wrapper's record-based Equals is used (not the converter). + var a = new ProductCode("X"); + var b = new ProductCode("X"); + + await Assert.That(a).IsEqualTo(b); + } + + [Test] + public async Task IsEqualTo_NullWrapper_Against_Null_Primitive_Passes() + { + ProductCode? code = null; + + await Assert.That(code).IsEqualTo((string?)null); + } + + [Test] + public async Task IsNotEqualTo_NullWrapper_Against_Non_Null_Primitive_Passes() + { + ProductCode? code = null; + + await Assert.That(code).IsNotEqualTo("Example"); + } + + [Test] + public async Task IsNotEqualTo_NonNull_Wrapper_Against_Null_Primitive_Passes() + { + var stock = new StockItem(new ProductCode("Example"), new WrappedNumber(5)); + + await Assert.That(stock.ProductCode).IsNotEqualTo((string?)null); + } + + [Test] + public async Task IsEqualTo_OperatorDefinedOnTargetType_Passes() + { + // The implicit conversion lives on TargetWithIncoming, not on SourceWithoutOperators. + // Verifies the FindImplicitOperatorOnTarget fallback in BuildConverter. + var source = new SourceWithoutOperators("Example"); + + await Assert.That(source).IsEqualTo(new TargetWithIncoming("Example")); + } + + [Test] + public async Task IsNotEqualTo_OperatorDefinedOnTargetType_Passes_When_Different() + { + var source = new SourceWithoutOperators("Example"); + + await Assert.That(source).IsNotEqualTo(new TargetWithIncoming("Other")); + } + + [Test] + public async Task IsEqualTo_NoImplicitOperator_Surfaces_Diagnostic() + { + // When no implicit conversion exists, the converter throws InvalidOperationException + // with a clear diagnostic. EqualsAssertion captures the evaluation exception and + // re-throws it as an AssertionException whose InnerException is the original + // InvalidOperationException — the diagnostic must be reachable via that chain. + var source = new UnrelatedType("Example"); + + var assertionException = await Assert.That( + async () => await Assert.That(source).IsEqualTo("Example")) + .Throws(); + + await Assert.That(assertionException!.InnerException).IsTypeOf(); + await Assert.That(assertionException.InnerException!.Message).StartsWith( + $"No implicit conversion operator from '{typeof(UnrelatedType)}' to '{typeof(string)}' was found."); + } + + [Test] + public async Task IsNotEqualTo_NoImplicitOperator_Surfaces_Diagnostic() + { + var source = new UnrelatedType("Example"); + + var assertionException = await Assert.That( + async () => await Assert.That(source).IsNotEqualTo("Other")) + .Throws(); + + await Assert.That(assertionException!.InnerException).IsTypeOf(); + await Assert.That(assertionException.InnerException!.Message).StartsWith( + $"No implicit conversion operator from '{typeof(UnrelatedType)}' to '{typeof(string)}' was found."); + } +} diff --git a/TUnit.Assertions/Extensions/ImplicitConversionEqualityExtensions.cs b/TUnit.Assertions/Extensions/ImplicitConversionEqualityExtensions.cs new file mode 100644 index 0000000000..e9c792fde8 --- /dev/null +++ b/TUnit.Assertions/Extensions/ImplicitConversionEqualityExtensions.cs @@ -0,0 +1,148 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Conditions; +using TUnit.Assertions.Core; + +namespace TUnit.Assertions.Extensions; + +/// +/// Adds IsEqualTo overloads accepting an expected value of a different type when the +/// source type defines an implicit conversion operator to that type. Defined as a partial of +/// the source-generated EqualsAssertionExtensions so it lives in the same containing +/// type as the original overload — required for +/// to disambiguate calls where both overloads are applicable (e.g. enum or value-type sources). +/// +/// Enables ergonomic comparison of wrapper Value Objects against their wrapped primitive, +/// e.g. await Assert.That(productCode).IsEqualTo("Example") when +/// ProductCode defines public static implicit operator string(ProductCode pc). +/// +public static partial class EqualsAssertionExtensions +{ + /// + /// Asserts equality between the source value and an expected value of a different type, + /// provided that defines an implicit conversion operator + /// to . The source value is converted via that operator + /// and compared to using . + /// + [OverloadResolutionPriority(-1)] + [RequiresUnreferencedCode("Looks up implicit conversion operators via reflection. Trimming may remove user-defined operators.")] + public static EqualsAssertion IsEqualTo( + this IAssertionSource source, + TOther? expected, + [CallerArgumentExpression(nameof(expected))] string? expectedExpression = null) + { + var converter = ImplicitConversionCache.GetConverter(); + source.Context.ExpressionBuilder.Append($".IsEqualTo({expectedExpression})"); + var mapped = source.Context.Map(converter); + return new EqualsAssertion(mapped, expected); + } +} + +/// +/// Adds IsNotEqualTo overloads accepting an unexpected value of a different type when +/// the source type defines an implicit conversion operator to that type. See remarks on +/// for why this is a partial extension class. +/// +public static partial class NotEqualsAssertionExtensions +{ + /// + /// Asserts inequality between the source value and an unexpected value of a different + /// type, provided that defines an implicit conversion + /// operator to . The source value is converted via that + /// operator and compared to using + /// . + /// + [OverloadResolutionPriority(-1)] + [RequiresUnreferencedCode("Looks up implicit conversion operators via reflection. Trimming may remove user-defined operators.")] + public static NotEqualsAssertion IsNotEqualTo( + this IAssertionSource source, + TOther? notExpected, + [CallerArgumentExpression(nameof(notExpected))] string? notExpectedExpression = null) + { + var converter = ImplicitConversionCache.GetConverter(); + source.Context.ExpressionBuilder.Append($".IsNotEqualTo({notExpectedExpression})"); + var mapped = source.Context.Map(converter); + return new NotEqualsAssertion(mapped, notExpected!); + } +} + +/// +/// Caches user-defined implicit conversion operators from TFrom to TTo. +/// Reflection lookup is performed once per type pair and the resulting delegate is reused. +/// +internal static class ImplicitConversionCache +{ + private static readonly ConcurrentDictionary<(Type From, Type To), Delegate> Cache = new(); + + [RequiresUnreferencedCode("Reflects over user-defined operators on TFrom and TTo.")] + public static Func GetConverter() + { + var key = (typeof(TFrom), typeof(TTo)); + return (Func)Cache.GetOrAdd(key, static _ => BuildConverter()); + } + + [RequiresUnreferencedCode("Reflects over user-defined operators on TFrom and TTo.")] + [UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "User-defined operators are public static methods on TFrom or TTo; if trimmed, IsEqualTo throws InvalidOperationException at call time.")] + private static Func BuildConverter() + { + var fromType = typeof(TFrom); + var toType = typeof(TTo); + + // C# allows `public static implicit operator TTo(TFrom)` to be declared on + // either participant. Try TFrom first (the common Value Object pattern), + // then fall back to TTo for cases where the conversion lives on the target. + var op = FindImplicitOperator(fromType, fromType, toType) + ?? FindImplicitOperator(toType, fromType, toType); + + if (op is null) + { + return _ => throw new InvalidOperationException( + $"No implicit conversion operator from '{fromType}' to '{toType}' was found. " + + $"IsEqualTo / IsNotEqualTo with a different argument type requires '{fromType.Name}' " + + $"to define 'public static implicit operator {toType.Name}({fromType.Name} value)'."); + } + + // MethodInfo.Invoke boxes value-type arguments on every call. An + // Expression.Lambda>(...).Compile() avoids the box but + // requires [RequiresDynamicCode] which breaks Native AOT compatibility, + // so we accept the boxing here. + return value => + { + if (value is null) + { + return default; + } + return (TTo?)op.Invoke(null, [value]); + }; + } + + /// + /// Looks for public static implicit operator () + /// declared on . C# requires to be either + /// or ; we try both call sites. + /// + [UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "User-defined operators are looked up reflectively; warning is surfaced via RequiresUnreferencedCode on public entry points.")] + private static MethodInfo? FindImplicitOperator( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type definedOn, + Type fromType, + Type toType) + { + foreach (var method in definedOn.GetMethods(BindingFlags.Public | BindingFlags.Static)) + { + if (method.Name != "op_Implicit" || method.ReturnType != toType) + { + continue; + } + + var parameters = method.GetParameters(); + if (parameters.Length == 1 && parameters[0].ParameterType == fromType) + { + return method; + } + } + + return null; + } +} 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 54fa8fd2df..a983ec05d6 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 @@ -3787,6 +3787,10 @@ namespace .Extensions { public static . IsEqualTo(this . source, TValue? expected, [.("expected")] string? expectedExpression = null) { } public static . IsEqualTo(this . source, TValue? expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + [.("Looks up implicit conversion operators via reflection. Trimming may remove user-d" + + "efined operators.")] + [.(-1)] + public static . IsEqualTo(this . source, TOther? expected, [.("expected")] string? expectedExpression = null) { } } public static class EquatableAssertionExtensions { @@ -4736,6 +4740,10 @@ namespace .Extensions public static class NotEqualsAssertionExtensions { public static . IsNotEqualTo(this . source, TValue notExpected, .? comparer = null, [.("notExpected")] string? notExpectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + [.("Looks up implicit conversion operators via reflection. Trimming may remove user-d" + + "efined operators.")] + [.(-1)] + public static . IsNotEqualTo(this . source, TOther? notExpected, [.("notExpected")] string? notExpectedExpression = null) { } } public static class NotEquivalentToAssertionExtensions { 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 4a85957834..60a53f21db 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 @@ -3743,6 +3743,9 @@ namespace .Extensions { public static . IsEqualTo(this . source, TValue? expected, [.("expected")] string? expectedExpression = null) { } public static . IsEqualTo(this . source, TValue? expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + [.("Looks up implicit conversion operators via reflection. Trimming may remove user-d" + + "efined operators.")] + public static . IsEqualTo(this . source, TOther? expected, [.("expected")] string? expectedExpression = null) { } } public static class EquatableAssertionExtensions { @@ -4669,6 +4672,9 @@ namespace .Extensions public static class NotEqualsAssertionExtensions { public static . IsNotEqualTo(this . source, TValue notExpected, .? comparer = null, [.("notExpected")] string? notExpectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + [.("Looks up implicit conversion operators via reflection. Trimming may remove user-d" + + "efined operators.")] + public static . IsNotEqualTo(this . source, TOther? notExpected, [.("notExpected")] string? notExpectedExpression = null) { } } public static class NotEquivalentToAssertionExtensions { 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 c20c21a71f..0828aea374 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 @@ -3787,6 +3787,10 @@ namespace .Extensions { public static . IsEqualTo(this . source, TValue? expected, [.("expected")] string? expectedExpression = null) { } public static . IsEqualTo(this . source, TValue? expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + [.("Looks up implicit conversion operators via reflection. Trimming may remove user-d" + + "efined operators.")] + [.(-1)] + public static . IsEqualTo(this . source, TOther? expected, [.("expected")] string? expectedExpression = null) { } } public static class EquatableAssertionExtensions { @@ -4736,6 +4740,10 @@ namespace .Extensions public static class NotEqualsAssertionExtensions { public static . IsNotEqualTo(this . source, TValue notExpected, .? comparer = null, [.("notExpected")] string? notExpectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + [.("Looks up implicit conversion operators via reflection. Trimming may remove user-d" + + "efined operators.")] + [.(-1)] + public static . IsNotEqualTo(this . source, TOther? notExpected, [.("notExpected")] string? notExpectedExpression = null) { } } public static class NotEquivalentToAssertionExtensions { 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 a106aa161c..1ed9500f54 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 @@ -3338,6 +3338,10 @@ namespace .Extensions { public static . IsEqualTo(this . source, TValue? expected, [.("expected")] string? expectedExpression = null) { } public static . IsEqualTo(this . source, TValue? expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + [.("Looks up implicit conversion operators via reflection. Trimming may remove user-d" + + "efined operators.")] + [.(-1)] + public static . IsEqualTo(this . source, TOther? expected, [.("expected")] string? expectedExpression = null) { } } public static class EquatableAssertionExtensions { @@ -4166,6 +4170,10 @@ namespace .Extensions public static class NotEqualsAssertionExtensions { public static . IsNotEqualTo(this . source, TValue notExpected, .? comparer = null, [.("notExpected")] string? notExpectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + [.("Looks up implicit conversion operators via reflection. Trimming may remove user-d" + + "efined operators.")] + [.(-1)] + public static . IsNotEqualTo(this . source, TOther? notExpected, [.("notExpected")] string? notExpectedExpression = null) { } } public static class NotEquivalentToAssertionExtensions { diff --git a/TUnit.Templates.Tests/Snapshots/InstantiationTestWithFSharp.TUnit.AspNet.FSharp._.verified/TUnit.AspNet.FSharp/TUnit.AspNet.FSharp/Tests.fs b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithFSharp.TUnit.AspNet.FSharp._.verified/TUnit.AspNet.FSharp/TUnit.AspNet.FSharp/Tests.fs index ddc59ffecb..5181f1a2cf 100644 --- a/TUnit.Templates.Tests/Snapshots/InstantiationTestWithFSharp.TUnit.AspNet.FSharp._.verified/TUnit.AspNet.FSharp/TUnit.AspNet.FSharp/Tests.fs +++ b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithFSharp.TUnit.AspNet.FSharp._.verified/TUnit.AspNet.FSharp/TUnit.AspNet.FSharp/Tests.fs @@ -19,7 +19,10 @@ type Tests() = let client = this.WebApplicationFactory.CreateClient() let! response = client.GetAsync("/ping") |> Async.AwaitTask let! stringContent = response.Content.ReadAsStringAsync() |> Async.AwaitTask - do! check (Assert.That(stringContent).IsEqualTo("Hello, World!")) + // F# does not honour [OverloadResolutionPriority], so call the string-specific + // extension directly to avoid IsEqualTo ambiguity between the string, generic + // and implicit-conversion overloads. + do! check (StringEqualsAssertionExtensions.IsEqualTo(Assert.That(stringContent), "Hello, World!")) } [] diff --git a/TUnit.Templates.Tests/Snapshots/InstantiationTestWithFSharp.TUnit.FSharp._.verified/TUnit.FSharp/Tests.fs b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithFSharp.TUnit.FSharp._.verified/TUnit.FSharp/Tests.fs index 9648d247bf..aa9e04fda6 100644 --- a/TUnit.Templates.Tests/Snapshots/InstantiationTestWithFSharp.TUnit.FSharp._.verified/TUnit.FSharp/Tests.fs +++ b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithFSharp.TUnit.FSharp._.verified/TUnit.FSharp/Tests.fs @@ -23,7 +23,10 @@ type Tests() = async { Console.WriteLine("This one can accept arguments from an attribute") let result = a + b - do! check(Assert.That(result).IsEqualTo(c)) + // F# does not honour [OverloadResolutionPriority], so call the int-specific + // extension directly to avoid IsEqualTo ambiguity between the int, generic + // and implicit-conversion overloads. + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(result), c)) } @@ -33,7 +36,7 @@ type Tests() = async { Console.WriteLine("This one can accept arguments from a method") let result = a + b - do! check(Assert.That(result).IsEqualTo(c)) + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(result), c)) } [] @@ -51,7 +54,7 @@ type Tests() = async { Console.WriteLine("You can even define your own custom data generators") let result = a + b - do! check(Assert.That(result).IsEqualTo(c)) + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(result), c)) } static member DataSource() : IEnumerable = diff --git a/TUnit.Templates/content/TUnit.AspNet.FSharp/TestProject/Tests.fs b/TUnit.Templates/content/TUnit.AspNet.FSharp/TestProject/Tests.fs index e2bf591894..416b2429c7 100644 --- a/TUnit.Templates/content/TUnit.AspNet.FSharp/TestProject/Tests.fs +++ b/TUnit.Templates/content/TUnit.AspNet.FSharp/TestProject/Tests.fs @@ -19,7 +19,10 @@ type Tests() = let client = this.WebApplicationFactory.CreateClient() let! response = client.GetAsync("/ping") |> Async.AwaitTask let! stringContent = response.Content.ReadAsStringAsync() |> Async.AwaitTask - do! check (Assert.That(stringContent).IsEqualTo("Hello, World!")) + // F# does not honour [OverloadResolutionPriority], so call the string-specific + // extension directly to avoid IsEqualTo ambiguity between the string, generic + // and implicit-conversion overloads. + do! check (StringEqualsAssertionExtensions.IsEqualTo(Assert.That(stringContent), "Hello, World!")) } [] diff --git a/TUnit.Templates/content/TUnit.FSharp/Tests.fs b/TUnit.Templates/content/TUnit.FSharp/Tests.fs index 2777b77cbb..088a997d9b 100644 --- a/TUnit.Templates/content/TUnit.FSharp/Tests.fs +++ b/TUnit.Templates/content/TUnit.FSharp/Tests.fs @@ -23,7 +23,10 @@ type Tests() = async { Console.WriteLine("This one can accept arguments from an attribute") let result = a + b - do! check(Assert.That(result).IsEqualTo(c)) + // F# does not honour [OverloadResolutionPriority], so call the int-specific + // extension directly to avoid IsEqualTo ambiguity between the int, generic + // and implicit-conversion overloads. + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(result), c)) } @@ -33,7 +36,7 @@ type Tests() = async { Console.WriteLine("This one can accept arguments from a method") let result = a + b - do! check(Assert.That(result).IsEqualTo(c)) + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(result), c)) } [] @@ -51,7 +54,7 @@ type Tests() = async { Console.WriteLine("You can even define your own custom data generators") let result = a + b - do! check(Assert.That(result).IsEqualTo(c)) + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(result), c)) } static member DataSource() : IEnumerable = diff --git a/TUnit.TestProject.FSharp/AsyncTests.fs b/TUnit.TestProject.FSharp/AsyncTests.fs index 675fc756d0..97c63c1685 100644 --- a/TUnit.TestProject.FSharp/AsyncTests.fs +++ b/TUnit.TestProject.FSharp/AsyncTests.fs @@ -47,5 +47,7 @@ type AsyncTests() = [] member _.FSharpAsync_WithAssertion() : Async = async { let result = 1 + 1 - do! check (Assert.That(result).IsEqualTo(2)) + // F# can't disambiguate between the int-specific, generic, and implicit-conversion + // IsEqualTo extension overloads via OverloadResolutionPriority, so call directly. + do! check (IntEqualsAssertionExtensions.IsEqualTo(Assert.That(result), 2)) } diff --git a/TUnit.TestProject.FSharp/TaskAssertTests.fs b/TUnit.TestProject.FSharp/TaskAssertTests.fs index b2597ec104..3ea2e557dc 100644 --- a/TUnit.TestProject.FSharp/TaskAssertTests.fs +++ b/TUnit.TestProject.FSharp/TaskAssertTests.fs @@ -60,7 +60,9 @@ type TaskAssertTests() = let mutable exceptionThrown = false try do! taskAssert { - do! Assert.That(1 + 1).IsEqualTo(3) + // F# cannot use OverloadResolutionPriority — call the int-specific + // generated extension method directly to avoid IsEqualTo ambiguity. + do! IntEqualsAssertionExtensions.IsEqualTo(Assert.That(1 + 1), 3) } with | _ -> exceptionThrown <- true