From 785dc99d99b91e83d46ffc6d395c5a507078dedb Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 14 Oct 2025 20:46:13 +0100 Subject: [PATCH 1/4] chore(deps): update dependency microsoft.templateengine.authoring.cli to v9.0.306 (#3381) Co-authored-by: Renovate Bot --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index bcd79cd56d..70e0fda6d2 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "microsoft.templateengine.authoring.cli": { - "version": "9.0.305", + "version": "9.0.306", "commands": [ "dotnet-template-authoring" ], From 9365a79f72ddfbc615908d93696c05eb4a5d7ffe Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 14 Oct 2025 21:01:44 +0100 Subject: [PATCH 2/4] chore(deps): update dependency microsoft.build.utilities.core to 17.14.28 (#3380) Co-authored-by: Renovate Bot --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 08d6373952..cb3f41ddcd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,7 +20,7 @@ - + From 383e20d8ee04fd0e4887285801841b00ba5bb857 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 14 Oct 2025 22:29:56 +0100 Subject: [PATCH 3/4] Add generated assertions and extension methods for various constraints (#3386) - Implemented assertions for methods with multiple constraints, including HasProperty. - Added assertions for notnull constraints with HasValue. - Created assertions for reference type constraints with IsNullOrDefault. - Developed assertions for value type constraints with IsDefault. - Introduced a new test case for methods with IComparable constraints, generating appropriate assertions. - Updated test data files to include new assertion methods and ensure proper functionality across different .NET versions. --- ...rableTypeParameter.DotNet10_0.verified.txt | 2 + ...erableTypeParameter.DotNet8_0.verified.txt | 2 + ...erableTypeParameter.DotNet9_0.verified.txt | 2 + ...InferableTypeParameter.Net4_7.verified.txt | 2 + ...ultipleConstraints.DotNet10_0.verified.txt | 69 +++++++++++ ...MultipleConstraints.DotNet8_0.verified.txt | 69 +++++++++++ ...MultipleConstraints.DotNet9_0.verified.txt | 69 +++++++++++ ...ithMultipleConstraints.Net4_7.verified.txt | 69 +++++++++++ ...hNotNullConstraint.DotNet10_0.verified.txt | 69 +++++++++++ ...thNotNullConstraint.DotNet8_0.verified.txt | 69 +++++++++++ ...thNotNullConstraint.DotNet9_0.verified.txt | 69 +++++++++++ ...dWithNotNullConstraint.Net4_7.verified.txt | 69 +++++++++++ ...enceTypeConstraint.DotNet10_0.verified.txt | 69 +++++++++++ ...renceTypeConstraint.DotNet8_0.verified.txt | 69 +++++++++++ ...renceTypeConstraint.DotNet9_0.verified.txt | 69 +++++++++++ ...eferenceTypeConstraint.Net4_7.verified.txt | 69 +++++++++++ ...alueTypeConstraint.DotNet10_0.verified.txt | 64 +++++++++++ ...ValueTypeConstraint.DotNet8_0.verified.txt | 64 +++++++++++ ...ValueTypeConstraint.DotNet9_0.verified.txt | 64 +++++++++++ ...ithValueTypeConstraint.Net4_7.verified.txt | 64 +++++++++++ .../MethodAssertionGeneratorTests.cs | 101 ++++++++++++++++ .../MethodWithComparableConstraint.cs | 22 ++++ .../TestData/MethodWithMultipleConstraints.cs | 18 +++ .../TestData/MethodWithNotNullConstraint.cs | 16 +++ .../MethodWithReferenceTypeConstraint.cs | 16 +++ .../TestData/MethodWithValueTypeConstraint.cs | 16 +++ .../Generators/AssertionMethodGenerator.cs | 108 ++++++++++++++++++ .../Generators/MethodAssertionGenerator.cs | 73 ++++++++++++ dotnet.config | 2 - global.json | 3 + 30 files changed, 1465 insertions(+), 2 deletions(-) create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithMultipleConstraints.DotNet10_0.verified.txt create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithMultipleConstraints.DotNet8_0.verified.txt create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithMultipleConstraints.DotNet9_0.verified.txt create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithMultipleConstraints.Net4_7.verified.txt create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithNotNullConstraint.DotNet10_0.verified.txt create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithNotNullConstraint.DotNet8_0.verified.txt create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithNotNullConstraint.DotNet9_0.verified.txt create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithNotNullConstraint.Net4_7.verified.txt create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithReferenceTypeConstraint.DotNet10_0.verified.txt create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithReferenceTypeConstraint.DotNet8_0.verified.txt create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithReferenceTypeConstraint.DotNet9_0.verified.txt create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithReferenceTypeConstraint.Net4_7.verified.txt create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithValueTypeConstraint.DotNet10_0.verified.txt create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithValueTypeConstraint.DotNet8_0.verified.txt create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithValueTypeConstraint.DotNet9_0.verified.txt create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithValueTypeConstraint.Net4_7.verified.txt create mode 100644 TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithComparableConstraint.cs create mode 100644 TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithMultipleConstraints.cs create mode 100644 TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithNotNullConstraint.cs create mode 100644 TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithReferenceTypeConstraint.cs create mode 100644 TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithValueTypeConstraint.cs delete mode 100644 dotnet.config diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.GenericMethodWithNonInferableTypeParameter.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.GenericMethodWithNonInferableTypeParameter.DotNet10_0.verified.txt index b261f758d3..9d6785dcca 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.GenericMethodWithNonInferableTypeParameter.DotNet10_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.GenericMethodWithNonInferableTypeParameter.DotNet10_0.verified.txt @@ -14,6 +14,7 @@ namespace TUnit.Assertions.Extensions; /// [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] public sealed class ResultTValue_IsErrorOfType_Assertion : Assertion> + where TError : System.Exception { public ResultTValue_IsErrorOfType_Assertion(AssertionContext> context) : base(context) @@ -54,6 +55,7 @@ public static partial class GenericMethodWithNonInferableTypeParameterExtensions /// [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] public static ResultTValue_IsErrorOfType_Assertion IsErrorOfType(this IAssertionSource> source) + where TError : System.Exception { source.Context.ExpressionBuilder.Append(".IsErrorOfType()"); return new ResultTValue_IsErrorOfType_Assertion(source.Context); diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.GenericMethodWithNonInferableTypeParameter.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.GenericMethodWithNonInferableTypeParameter.DotNet8_0.verified.txt index b261f758d3..9d6785dcca 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.GenericMethodWithNonInferableTypeParameter.DotNet8_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.GenericMethodWithNonInferableTypeParameter.DotNet8_0.verified.txt @@ -14,6 +14,7 @@ namespace TUnit.Assertions.Extensions; /// [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] public sealed class ResultTValue_IsErrorOfType_Assertion : Assertion> + where TError : System.Exception { public ResultTValue_IsErrorOfType_Assertion(AssertionContext> context) : base(context) @@ -54,6 +55,7 @@ public static partial class GenericMethodWithNonInferableTypeParameterExtensions /// [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] public static ResultTValue_IsErrorOfType_Assertion IsErrorOfType(this IAssertionSource> source) + where TError : System.Exception { source.Context.ExpressionBuilder.Append(".IsErrorOfType()"); return new ResultTValue_IsErrorOfType_Assertion(source.Context); diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.GenericMethodWithNonInferableTypeParameter.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.GenericMethodWithNonInferableTypeParameter.DotNet9_0.verified.txt index b261f758d3..9d6785dcca 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.GenericMethodWithNonInferableTypeParameter.DotNet9_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.GenericMethodWithNonInferableTypeParameter.DotNet9_0.verified.txt @@ -14,6 +14,7 @@ namespace TUnit.Assertions.Extensions; /// [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] public sealed class ResultTValue_IsErrorOfType_Assertion : Assertion> + where TError : System.Exception { public ResultTValue_IsErrorOfType_Assertion(AssertionContext> context) : base(context) @@ -54,6 +55,7 @@ public static partial class GenericMethodWithNonInferableTypeParameterExtensions /// [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] public static ResultTValue_IsErrorOfType_Assertion IsErrorOfType(this IAssertionSource> source) + where TError : System.Exception { source.Context.ExpressionBuilder.Append(".IsErrorOfType()"); return new ResultTValue_IsErrorOfType_Assertion(source.Context); diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.GenericMethodWithNonInferableTypeParameter.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.GenericMethodWithNonInferableTypeParameter.Net4_7.verified.txt index b261f758d3..9d6785dcca 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.GenericMethodWithNonInferableTypeParameter.Net4_7.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.GenericMethodWithNonInferableTypeParameter.Net4_7.verified.txt @@ -14,6 +14,7 @@ namespace TUnit.Assertions.Extensions; /// [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] public sealed class ResultTValue_IsErrorOfType_Assertion : Assertion> + where TError : System.Exception { public ResultTValue_IsErrorOfType_Assertion(AssertionContext> context) : base(context) @@ -54,6 +55,7 @@ public static partial class GenericMethodWithNonInferableTypeParameterExtensions /// [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] public static ResultTValue_IsErrorOfType_Assertion IsErrorOfType(this IAssertionSource> source) + where TError : System.Exception { source.Context.ExpressionBuilder.Append(".IsErrorOfType()"); return new ResultTValue_IsErrorOfType_Assertion(source.Context); diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithMultipleConstraints.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithMultipleConstraints.DotNet10_0.verified.txt new file mode 100644 index 0000000000..59063c240c --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithMultipleConstraints.DotNet10_0.verified.txt @@ -0,0 +1,69 @@ +[ +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using TUnit.Assertions.Core; +using TUnit.Assertions.Tests.TestData; + +namespace TUnit.Assertions.Extensions; + +/// +/// Generated assertion for HasProperty +/// +[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] +public sealed class String_HasProperty_T_Assertion : Assertion + where T : class, System.IComparable, new() +{ + private readonly T _value; + + public String_HasProperty_T_Assertion(AssertionContext context, T value) + : base(context) + { + _value = value; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); + } + + if (value is null) + { + return Task.FromResult(AssertionResult.Failed("Actual value is null")); + } + + var result = value.HasProperty(_value); + return Task.FromResult(result + ? AssertionResult.Passed + : AssertionResult.Failed($"found {value}")); + } + + protected override string GetExpectation() + { + return $"have the property"; + } +} + +public static partial class MultipleConstraintsExtensions +{ + /// + /// Generated extension method for HasProperty + /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static String_HasProperty_T_Assertion HasProperty(this IAssertionSource source, T value, [CallerArgumentExpression(nameof(value))] string? valueExpression = null) + where T : class, System.IComparable, new() + { + source.Context.ExpressionBuilder.Append($".HasProperty({valueExpression})"); + return new String_HasProperty_T_Assertion(source.Context, value); + } + +} + +] \ No newline at end of file diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithMultipleConstraints.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithMultipleConstraints.DotNet8_0.verified.txt new file mode 100644 index 0000000000..59063c240c --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithMultipleConstraints.DotNet8_0.verified.txt @@ -0,0 +1,69 @@ +[ +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using TUnit.Assertions.Core; +using TUnit.Assertions.Tests.TestData; + +namespace TUnit.Assertions.Extensions; + +/// +/// Generated assertion for HasProperty +/// +[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] +public sealed class String_HasProperty_T_Assertion : Assertion + where T : class, System.IComparable, new() +{ + private readonly T _value; + + public String_HasProperty_T_Assertion(AssertionContext context, T value) + : base(context) + { + _value = value; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); + } + + if (value is null) + { + return Task.FromResult(AssertionResult.Failed("Actual value is null")); + } + + var result = value.HasProperty(_value); + return Task.FromResult(result + ? AssertionResult.Passed + : AssertionResult.Failed($"found {value}")); + } + + protected override string GetExpectation() + { + return $"have the property"; + } +} + +public static partial class MultipleConstraintsExtensions +{ + /// + /// Generated extension method for HasProperty + /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static String_HasProperty_T_Assertion HasProperty(this IAssertionSource source, T value, [CallerArgumentExpression(nameof(value))] string? valueExpression = null) + where T : class, System.IComparable, new() + { + source.Context.ExpressionBuilder.Append($".HasProperty({valueExpression})"); + return new String_HasProperty_T_Assertion(source.Context, value); + } + +} + +] \ No newline at end of file diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithMultipleConstraints.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithMultipleConstraints.DotNet9_0.verified.txt new file mode 100644 index 0000000000..59063c240c --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithMultipleConstraints.DotNet9_0.verified.txt @@ -0,0 +1,69 @@ +[ +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using TUnit.Assertions.Core; +using TUnit.Assertions.Tests.TestData; + +namespace TUnit.Assertions.Extensions; + +/// +/// Generated assertion for HasProperty +/// +[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] +public sealed class String_HasProperty_T_Assertion : Assertion + where T : class, System.IComparable, new() +{ + private readonly T _value; + + public String_HasProperty_T_Assertion(AssertionContext context, T value) + : base(context) + { + _value = value; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); + } + + if (value is null) + { + return Task.FromResult(AssertionResult.Failed("Actual value is null")); + } + + var result = value.HasProperty(_value); + return Task.FromResult(result + ? AssertionResult.Passed + : AssertionResult.Failed($"found {value}")); + } + + protected override string GetExpectation() + { + return $"have the property"; + } +} + +public static partial class MultipleConstraintsExtensions +{ + /// + /// Generated extension method for HasProperty + /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static String_HasProperty_T_Assertion HasProperty(this IAssertionSource source, T value, [CallerArgumentExpression(nameof(value))] string? valueExpression = null) + where T : class, System.IComparable, new() + { + source.Context.ExpressionBuilder.Append($".HasProperty({valueExpression})"); + return new String_HasProperty_T_Assertion(source.Context, value); + } + +} + +] \ No newline at end of file diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithMultipleConstraints.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithMultipleConstraints.Net4_7.verified.txt new file mode 100644 index 0000000000..59063c240c --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithMultipleConstraints.Net4_7.verified.txt @@ -0,0 +1,69 @@ +[ +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using TUnit.Assertions.Core; +using TUnit.Assertions.Tests.TestData; + +namespace TUnit.Assertions.Extensions; + +/// +/// Generated assertion for HasProperty +/// +[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] +public sealed class String_HasProperty_T_Assertion : Assertion + where T : class, System.IComparable, new() +{ + private readonly T _value; + + public String_HasProperty_T_Assertion(AssertionContext context, T value) + : base(context) + { + _value = value; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); + } + + if (value is null) + { + return Task.FromResult(AssertionResult.Failed("Actual value is null")); + } + + var result = value.HasProperty(_value); + return Task.FromResult(result + ? AssertionResult.Passed + : AssertionResult.Failed($"found {value}")); + } + + protected override string GetExpectation() + { + return $"have the property"; + } +} + +public static partial class MultipleConstraintsExtensions +{ + /// + /// Generated extension method for HasProperty + /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static String_HasProperty_T_Assertion HasProperty(this IAssertionSource source, T value, [CallerArgumentExpression(nameof(value))] string? valueExpression = null) + where T : class, System.IComparable, new() + { + source.Context.ExpressionBuilder.Append($".HasProperty({valueExpression})"); + return new String_HasProperty_T_Assertion(source.Context, value); + } + +} + +] \ No newline at end of file diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithNotNullConstraint.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithNotNullConstraint.DotNet10_0.verified.txt new file mode 100644 index 0000000000..acb27ea130 --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithNotNullConstraint.DotNet10_0.verified.txt @@ -0,0 +1,69 @@ +[ +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using TUnit.Assertions.Core; +using TUnit.Assertions.Tests.TestData; + +namespace TUnit.Assertions.Extensions; + +/// +/// Generated assertion for HasValue +/// +[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] +public sealed class String_HasValue_T_Assertion : Assertion + where T : notnull +{ + private readonly T _value; + + public String_HasValue_T_Assertion(AssertionContext context, T value) + : base(context) + { + _value = value; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); + } + + if (value is null) + { + return Task.FromResult(AssertionResult.Failed("Actual value is null")); + } + + var result = value.HasValue(_value); + return Task.FromResult(result + ? AssertionResult.Passed + : AssertionResult.Failed($"found {value}")); + } + + protected override string GetExpectation() + { + return $"have a non-null value"; + } +} + +public static partial class NotNullConstraintExtensions +{ + /// + /// Generated extension method for HasValue + /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static String_HasValue_T_Assertion HasValue(this IAssertionSource source, T value, [CallerArgumentExpression(nameof(value))] string? valueExpression = null) + where T : notnull + { + source.Context.ExpressionBuilder.Append($".HasValue({valueExpression})"); + return new String_HasValue_T_Assertion(source.Context, value); + } + +} + +] \ No newline at end of file diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithNotNullConstraint.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithNotNullConstraint.DotNet8_0.verified.txt new file mode 100644 index 0000000000..acb27ea130 --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithNotNullConstraint.DotNet8_0.verified.txt @@ -0,0 +1,69 @@ +[ +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using TUnit.Assertions.Core; +using TUnit.Assertions.Tests.TestData; + +namespace TUnit.Assertions.Extensions; + +/// +/// Generated assertion for HasValue +/// +[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] +public sealed class String_HasValue_T_Assertion : Assertion + where T : notnull +{ + private readonly T _value; + + public String_HasValue_T_Assertion(AssertionContext context, T value) + : base(context) + { + _value = value; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); + } + + if (value is null) + { + return Task.FromResult(AssertionResult.Failed("Actual value is null")); + } + + var result = value.HasValue(_value); + return Task.FromResult(result + ? AssertionResult.Passed + : AssertionResult.Failed($"found {value}")); + } + + protected override string GetExpectation() + { + return $"have a non-null value"; + } +} + +public static partial class NotNullConstraintExtensions +{ + /// + /// Generated extension method for HasValue + /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static String_HasValue_T_Assertion HasValue(this IAssertionSource source, T value, [CallerArgumentExpression(nameof(value))] string? valueExpression = null) + where T : notnull + { + source.Context.ExpressionBuilder.Append($".HasValue({valueExpression})"); + return new String_HasValue_T_Assertion(source.Context, value); + } + +} + +] \ No newline at end of file diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithNotNullConstraint.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithNotNullConstraint.DotNet9_0.verified.txt new file mode 100644 index 0000000000..acb27ea130 --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithNotNullConstraint.DotNet9_0.verified.txt @@ -0,0 +1,69 @@ +[ +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using TUnit.Assertions.Core; +using TUnit.Assertions.Tests.TestData; + +namespace TUnit.Assertions.Extensions; + +/// +/// Generated assertion for HasValue +/// +[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] +public sealed class String_HasValue_T_Assertion : Assertion + where T : notnull +{ + private readonly T _value; + + public String_HasValue_T_Assertion(AssertionContext context, T value) + : base(context) + { + _value = value; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); + } + + if (value is null) + { + return Task.FromResult(AssertionResult.Failed("Actual value is null")); + } + + var result = value.HasValue(_value); + return Task.FromResult(result + ? AssertionResult.Passed + : AssertionResult.Failed($"found {value}")); + } + + protected override string GetExpectation() + { + return $"have a non-null value"; + } +} + +public static partial class NotNullConstraintExtensions +{ + /// + /// Generated extension method for HasValue + /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static String_HasValue_T_Assertion HasValue(this IAssertionSource source, T value, [CallerArgumentExpression(nameof(value))] string? valueExpression = null) + where T : notnull + { + source.Context.ExpressionBuilder.Append($".HasValue({valueExpression})"); + return new String_HasValue_T_Assertion(source.Context, value); + } + +} + +] \ No newline at end of file diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithNotNullConstraint.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithNotNullConstraint.Net4_7.verified.txt new file mode 100644 index 0000000000..acb27ea130 --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithNotNullConstraint.Net4_7.verified.txt @@ -0,0 +1,69 @@ +[ +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using TUnit.Assertions.Core; +using TUnit.Assertions.Tests.TestData; + +namespace TUnit.Assertions.Extensions; + +/// +/// Generated assertion for HasValue +/// +[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] +public sealed class String_HasValue_T_Assertion : Assertion + where T : notnull +{ + private readonly T _value; + + public String_HasValue_T_Assertion(AssertionContext context, T value) + : base(context) + { + _value = value; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); + } + + if (value is null) + { + return Task.FromResult(AssertionResult.Failed("Actual value is null")); + } + + var result = value.HasValue(_value); + return Task.FromResult(result + ? AssertionResult.Passed + : AssertionResult.Failed($"found {value}")); + } + + protected override string GetExpectation() + { + return $"have a non-null value"; + } +} + +public static partial class NotNullConstraintExtensions +{ + /// + /// Generated extension method for HasValue + /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static String_HasValue_T_Assertion HasValue(this IAssertionSource source, T value, [CallerArgumentExpression(nameof(value))] string? valueExpression = null) + where T : notnull + { + source.Context.ExpressionBuilder.Append($".HasValue({valueExpression})"); + return new String_HasValue_T_Assertion(source.Context, value); + } + +} + +] \ No newline at end of file diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithReferenceTypeConstraint.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithReferenceTypeConstraint.DotNet10_0.verified.txt new file mode 100644 index 0000000000..6ad2f9cbbc --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithReferenceTypeConstraint.DotNet10_0.verified.txt @@ -0,0 +1,69 @@ +[ +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using TUnit.Assertions.Core; +using TUnit.Assertions.Tests.TestData; + +namespace TUnit.Assertions.Extensions; + +/// +/// Generated assertion for IsNullOrDefault +/// +[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] +public sealed class String_IsNullOrDefault_T_Assertion : Assertion + where T : class +{ + private readonly T _obj; + + public String_IsNullOrDefault_T_Assertion(AssertionContext context, T obj) + : base(context) + { + _obj = obj; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); + } + + if (value is null) + { + return Task.FromResult(AssertionResult.Failed("Actual value is null")); + } + + var result = value.IsNullOrDefault(_obj); + return Task.FromResult(result + ? AssertionResult.Passed + : AssertionResult.Failed($"found {value}")); + } + + protected override string GetExpectation() + { + return $"be null or default"; + } +} + +public static partial class ReferenceTypeConstraintExtensions +{ + /// + /// Generated extension method for IsNullOrDefault + /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static String_IsNullOrDefault_T_Assertion IsNullOrDefault(this IAssertionSource source, T obj, [CallerArgumentExpression(nameof(obj))] string? objExpression = null) + where T : class + { + source.Context.ExpressionBuilder.Append($".IsNullOrDefault({objExpression})"); + return new String_IsNullOrDefault_T_Assertion(source.Context, obj); + } + +} + +] \ No newline at end of file diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithReferenceTypeConstraint.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithReferenceTypeConstraint.DotNet8_0.verified.txt new file mode 100644 index 0000000000..6ad2f9cbbc --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithReferenceTypeConstraint.DotNet8_0.verified.txt @@ -0,0 +1,69 @@ +[ +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using TUnit.Assertions.Core; +using TUnit.Assertions.Tests.TestData; + +namespace TUnit.Assertions.Extensions; + +/// +/// Generated assertion for IsNullOrDefault +/// +[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] +public sealed class String_IsNullOrDefault_T_Assertion : Assertion + where T : class +{ + private readonly T _obj; + + public String_IsNullOrDefault_T_Assertion(AssertionContext context, T obj) + : base(context) + { + _obj = obj; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); + } + + if (value is null) + { + return Task.FromResult(AssertionResult.Failed("Actual value is null")); + } + + var result = value.IsNullOrDefault(_obj); + return Task.FromResult(result + ? AssertionResult.Passed + : AssertionResult.Failed($"found {value}")); + } + + protected override string GetExpectation() + { + return $"be null or default"; + } +} + +public static partial class ReferenceTypeConstraintExtensions +{ + /// + /// Generated extension method for IsNullOrDefault + /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static String_IsNullOrDefault_T_Assertion IsNullOrDefault(this IAssertionSource source, T obj, [CallerArgumentExpression(nameof(obj))] string? objExpression = null) + where T : class + { + source.Context.ExpressionBuilder.Append($".IsNullOrDefault({objExpression})"); + return new String_IsNullOrDefault_T_Assertion(source.Context, obj); + } + +} + +] \ No newline at end of file diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithReferenceTypeConstraint.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithReferenceTypeConstraint.DotNet9_0.verified.txt new file mode 100644 index 0000000000..6ad2f9cbbc --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithReferenceTypeConstraint.DotNet9_0.verified.txt @@ -0,0 +1,69 @@ +[ +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using TUnit.Assertions.Core; +using TUnit.Assertions.Tests.TestData; + +namespace TUnit.Assertions.Extensions; + +/// +/// Generated assertion for IsNullOrDefault +/// +[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] +public sealed class String_IsNullOrDefault_T_Assertion : Assertion + where T : class +{ + private readonly T _obj; + + public String_IsNullOrDefault_T_Assertion(AssertionContext context, T obj) + : base(context) + { + _obj = obj; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); + } + + if (value is null) + { + return Task.FromResult(AssertionResult.Failed("Actual value is null")); + } + + var result = value.IsNullOrDefault(_obj); + return Task.FromResult(result + ? AssertionResult.Passed + : AssertionResult.Failed($"found {value}")); + } + + protected override string GetExpectation() + { + return $"be null or default"; + } +} + +public static partial class ReferenceTypeConstraintExtensions +{ + /// + /// Generated extension method for IsNullOrDefault + /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static String_IsNullOrDefault_T_Assertion IsNullOrDefault(this IAssertionSource source, T obj, [CallerArgumentExpression(nameof(obj))] string? objExpression = null) + where T : class + { + source.Context.ExpressionBuilder.Append($".IsNullOrDefault({objExpression})"); + return new String_IsNullOrDefault_T_Assertion(source.Context, obj); + } + +} + +] \ No newline at end of file diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithReferenceTypeConstraint.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithReferenceTypeConstraint.Net4_7.verified.txt new file mode 100644 index 0000000000..6ad2f9cbbc --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithReferenceTypeConstraint.Net4_7.verified.txt @@ -0,0 +1,69 @@ +[ +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using TUnit.Assertions.Core; +using TUnit.Assertions.Tests.TestData; + +namespace TUnit.Assertions.Extensions; + +/// +/// Generated assertion for IsNullOrDefault +/// +[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] +public sealed class String_IsNullOrDefault_T_Assertion : Assertion + where T : class +{ + private readonly T _obj; + + public String_IsNullOrDefault_T_Assertion(AssertionContext context, T obj) + : base(context) + { + _obj = obj; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); + } + + if (value is null) + { + return Task.FromResult(AssertionResult.Failed("Actual value is null")); + } + + var result = value.IsNullOrDefault(_obj); + return Task.FromResult(result + ? AssertionResult.Passed + : AssertionResult.Failed($"found {value}")); + } + + protected override string GetExpectation() + { + return $"be null or default"; + } +} + +public static partial class ReferenceTypeConstraintExtensions +{ + /// + /// Generated extension method for IsNullOrDefault + /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static String_IsNullOrDefault_T_Assertion IsNullOrDefault(this IAssertionSource source, T obj, [CallerArgumentExpression(nameof(obj))] string? objExpression = null) + where T : class + { + source.Context.ExpressionBuilder.Append($".IsNullOrDefault({objExpression})"); + return new String_IsNullOrDefault_T_Assertion(source.Context, obj); + } + +} + +] \ No newline at end of file diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithValueTypeConstraint.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithValueTypeConstraint.DotNet10_0.verified.txt new file mode 100644 index 0000000000..9ea7b3d69a --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithValueTypeConstraint.DotNet10_0.verified.txt @@ -0,0 +1,64 @@ +[ +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using TUnit.Assertions.Core; +using TUnit.Assertions.Tests.TestData; + +namespace TUnit.Assertions.Extensions; + +/// +/// Generated assertion for IsDefault +/// +[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] +public sealed class Int_IsDefault_T_Assertion : Assertion + where T : struct +{ + private readonly T _obj; + + public Int_IsDefault_T_Assertion(AssertionContext context, T obj) + : base(context) + { + _obj = obj; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); + } + + var result = value.IsDefault(_obj); + return Task.FromResult(result + ? AssertionResult.Passed + : AssertionResult.Failed($"found {value}")); + } + + protected override string GetExpectation() + { + return $"be the default value"; + } +} + +public static partial class ValueTypeConstraintExtensions +{ + /// + /// Generated extension method for IsDefault + /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Int_IsDefault_T_Assertion IsDefault(this IAssertionSource source, T obj, [CallerArgumentExpression(nameof(obj))] string? objExpression = null) + where T : struct + { + source.Context.ExpressionBuilder.Append($".IsDefault({objExpression})"); + return new Int_IsDefault_T_Assertion(source.Context, obj); + } + +} + +] \ No newline at end of file diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithValueTypeConstraint.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithValueTypeConstraint.DotNet8_0.verified.txt new file mode 100644 index 0000000000..9ea7b3d69a --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithValueTypeConstraint.DotNet8_0.verified.txt @@ -0,0 +1,64 @@ +[ +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using TUnit.Assertions.Core; +using TUnit.Assertions.Tests.TestData; + +namespace TUnit.Assertions.Extensions; + +/// +/// Generated assertion for IsDefault +/// +[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] +public sealed class Int_IsDefault_T_Assertion : Assertion + where T : struct +{ + private readonly T _obj; + + public Int_IsDefault_T_Assertion(AssertionContext context, T obj) + : base(context) + { + _obj = obj; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); + } + + var result = value.IsDefault(_obj); + return Task.FromResult(result + ? AssertionResult.Passed + : AssertionResult.Failed($"found {value}")); + } + + protected override string GetExpectation() + { + return $"be the default value"; + } +} + +public static partial class ValueTypeConstraintExtensions +{ + /// + /// Generated extension method for IsDefault + /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Int_IsDefault_T_Assertion IsDefault(this IAssertionSource source, T obj, [CallerArgumentExpression(nameof(obj))] string? objExpression = null) + where T : struct + { + source.Context.ExpressionBuilder.Append($".IsDefault({objExpression})"); + return new Int_IsDefault_T_Assertion(source.Context, obj); + } + +} + +] \ No newline at end of file diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithValueTypeConstraint.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithValueTypeConstraint.DotNet9_0.verified.txt new file mode 100644 index 0000000000..9ea7b3d69a --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithValueTypeConstraint.DotNet9_0.verified.txt @@ -0,0 +1,64 @@ +[ +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using TUnit.Assertions.Core; +using TUnit.Assertions.Tests.TestData; + +namespace TUnit.Assertions.Extensions; + +/// +/// Generated assertion for IsDefault +/// +[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] +public sealed class Int_IsDefault_T_Assertion : Assertion + where T : struct +{ + private readonly T _obj; + + public Int_IsDefault_T_Assertion(AssertionContext context, T obj) + : base(context) + { + _obj = obj; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); + } + + var result = value.IsDefault(_obj); + return Task.FromResult(result + ? AssertionResult.Passed + : AssertionResult.Failed($"found {value}")); + } + + protected override string GetExpectation() + { + return $"be the default value"; + } +} + +public static partial class ValueTypeConstraintExtensions +{ + /// + /// Generated extension method for IsDefault + /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Int_IsDefault_T_Assertion IsDefault(this IAssertionSource source, T obj, [CallerArgumentExpression(nameof(obj))] string? objExpression = null) + where T : struct + { + source.Context.ExpressionBuilder.Append($".IsDefault({objExpression})"); + return new Int_IsDefault_T_Assertion(source.Context, obj); + } + +} + +] \ No newline at end of file diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithValueTypeConstraint.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithValueTypeConstraint.Net4_7.verified.txt new file mode 100644 index 0000000000..9ea7b3d69a --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithValueTypeConstraint.Net4_7.verified.txt @@ -0,0 +1,64 @@ +[ +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using TUnit.Assertions.Core; +using TUnit.Assertions.Tests.TestData; + +namespace TUnit.Assertions.Extensions; + +/// +/// Generated assertion for IsDefault +/// +[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] +public sealed class Int_IsDefault_T_Assertion : Assertion + where T : struct +{ + private readonly T _obj; + + public Int_IsDefault_T_Assertion(AssertionContext context, T obj) + : base(context) + { + _obj = obj; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); + } + + var result = value.IsDefault(_obj); + return Task.FromResult(result + ? AssertionResult.Passed + : AssertionResult.Failed($"found {value}")); + } + + protected override string GetExpectation() + { + return $"be the default value"; + } +} + +public static partial class ValueTypeConstraintExtensions +{ + /// + /// Generated extension method for IsDefault + /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Int_IsDefault_T_Assertion IsDefault(this IAssertionSource source, T obj, [CallerArgumentExpression(nameof(obj))] string? objExpression = null) + where T : struct + { + source.Context.ExpressionBuilder.Append($".IsDefault({objExpression})"); + return new Int_IsDefault_T_Assertion(source.Context, obj); + } + +} + +] \ No newline at end of file diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs index 095cce86d7..02f0a8960c 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs @@ -87,5 +87,106 @@ public Task GenericMethodWithNonInferableTypeParameter() => RunTest( await Assert.That(mainFile!).Contains("value.IsErrorOfType()"); // Verify the assertion class is generic await Assert.That(mainFile!).Contains("ResultTValue_IsErrorOfType_Assertion"); + // Verify the constraint is preserved + await Assert.That(mainFile!).Contains("where TError : System.Exception"); + }); + + [Test] + public Task MethodWithComparableConstraint() => RunTest( + Path.Combine(Sourcy.Git.RootDirectory.FullName, + "TUnit.Assertions.SourceGenerator.Tests", + "TestData", + "MethodWithComparableConstraint.cs"), + async generatedFiles => + { + await Assert.That(generatedFiles).HasCount().EqualTo(1); + + var mainFile = generatedFiles.First(); + await Assert.That(mainFile).IsNotNull(); + + // Verify IsGreaterThan generates with constraint + await Assert.That(mainFile).Contains("Int_IsGreaterThan_T_Assertion"); + await Assert.That(mainFile).Contains("where T : System.IComparable"); + await Assert.That(mainFile).Contains("IsGreaterThan(this IAssertionSource source"); + + // Verify IsBetween generates with constraint + await Assert.That(mainFile).Contains("Int_IsBetween_T_Assertion"); + await Assert.That(mainFile).Contains("IsBetween(this IAssertionSource source"); + }); + + [Test] + public Task MethodWithMultipleConstraints() => RunTest( + Path.Combine(Sourcy.Git.RootDirectory.FullName, + "TUnit.Assertions.SourceGenerator.Tests", + "TestData", + "MethodWithMultipleConstraints.cs"), + async generatedFiles => + { + await Assert.That(generatedFiles).HasCount().EqualTo(1); + + var mainFile = generatedFiles.First(); + await Assert.That(mainFile).IsNotNull(); + + // Verify all constraints are preserved + await Assert.That(mainFile).Contains("String_HasProperty_T_Assertion"); + await Assert.That(mainFile).Contains("where T : class, System.IComparable, new()"); + await Assert.That(mainFile).Contains("HasProperty(this IAssertionSource source"); + }); + + [Test] + public Task MethodWithReferenceTypeConstraint() => RunTest( + Path.Combine(Sourcy.Git.RootDirectory.FullName, + "TUnit.Assertions.SourceGenerator.Tests", + "TestData", + "MethodWithReferenceTypeConstraint.cs"), + async generatedFiles => + { + await Assert.That(generatedFiles).HasCount().EqualTo(1); + + var mainFile = generatedFiles.First(); + await Assert.That(mainFile).IsNotNull(); + + // Verify class constraint is preserved + await Assert.That(mainFile).Contains("String_IsNullOrDefault_T_Assertion"); + await Assert.That(mainFile).Contains("where T : class"); + await Assert.That(mainFile).Contains("IsNullOrDefault(this IAssertionSource source"); + }); + + [Test] + public Task MethodWithValueTypeConstraint() => RunTest( + Path.Combine(Sourcy.Git.RootDirectory.FullName, + "TUnit.Assertions.SourceGenerator.Tests", + "TestData", + "MethodWithValueTypeConstraint.cs"), + async generatedFiles => + { + await Assert.That(generatedFiles).HasCount().EqualTo(1); + + var mainFile = generatedFiles.First(); + await Assert.That(mainFile).IsNotNull(); + + // Verify struct constraint is preserved + await Assert.That(mainFile).Contains("Int_IsDefault_T_Assertion"); + await Assert.That(mainFile).Contains("where T : struct"); + await Assert.That(mainFile).Contains("IsDefault(this IAssertionSource source"); + }); + + [Test] + public Task MethodWithNotNullConstraint() => RunTest( + Path.Combine(Sourcy.Git.RootDirectory.FullName, + "TUnit.Assertions.SourceGenerator.Tests", + "TestData", + "MethodWithNotNullConstraint.cs"), + async generatedFiles => + { + await Assert.That(generatedFiles).HasCount().EqualTo(1); + + var mainFile = generatedFiles.First(); + await Assert.That(mainFile).IsNotNull(); + + // Verify notnull constraint is preserved + await Assert.That(mainFile).Contains("String_HasValue_T_Assertion"); + await Assert.That(mainFile).Contains("where T : notnull"); + await Assert.That(mainFile).Contains("HasValue(this IAssertionSource source"); }); } diff --git a/TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithComparableConstraint.cs b/TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithComparableConstraint.cs new file mode 100644 index 0000000000..21ae60231f --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithComparableConstraint.cs @@ -0,0 +1,22 @@ +using TUnit.Assertions.Attributes; + +namespace TUnit.Assertions.Tests.TestData; + +/// +/// Test case: Method with IComparable constraint +/// Should generate assertion class and extension method preserving the constraint +/// +public static partial class ComparableConstraintExtensions +{ + [GenerateAssertion(ExpectationMessage = "be greater than {0}")] + public static bool IsGreaterThan(this int value, T other) where T : IComparable + { + return other.CompareTo(default(T)) > 0; + } + + [GenerateAssertion(ExpectationMessage = "be between {0} and {1}")] + public static bool IsBetween(this int value, T min, T max) where T : IComparable + { + return min.CompareTo(max) <= 0; + } +} diff --git a/TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithMultipleConstraints.cs b/TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithMultipleConstraints.cs new file mode 100644 index 0000000000..4af7adc56c --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithMultipleConstraints.cs @@ -0,0 +1,18 @@ +using TUnit.Assertions.Attributes; + +namespace TUnit.Assertions.Tests.TestData; + +/// +/// Test case: Method with multiple constraints +/// Should generate assertion preserving all constraints +/// +public static partial class MultipleConstraintsExtensions +{ + [GenerateAssertion(ExpectationMessage = "have the property")] + public static bool HasProperty(this string obj, T value) + where T : class, IComparable, new() + { + // Simplified implementation - just checks if value is not null + return value != null; + } +} diff --git a/TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithNotNullConstraint.cs b/TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithNotNullConstraint.cs new file mode 100644 index 0000000000..b547420536 --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithNotNullConstraint.cs @@ -0,0 +1,16 @@ +using TUnit.Assertions.Attributes; + +namespace TUnit.Assertions.Tests.TestData; + +/// +/// Test case: Method with notnull constraint +/// Should generate assertion preserving the notnull constraint +/// +public static partial class NotNullConstraintExtensions +{ + [GenerateAssertion(ExpectationMessage = "have a non-null value")] + public static bool HasValue(this string str, T value) where T : notnull + { + return value != null; + } +} diff --git a/TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithReferenceTypeConstraint.cs b/TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithReferenceTypeConstraint.cs new file mode 100644 index 0000000000..617adaae7e --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithReferenceTypeConstraint.cs @@ -0,0 +1,16 @@ +using TUnit.Assertions.Attributes; + +namespace TUnit.Assertions.Tests.TestData; + +/// +/// Test case: Method with reference type constraint (class) +/// Should generate assertion preserving the class constraint +/// +public static partial class ReferenceTypeConstraintExtensions +{ + [GenerateAssertion(ExpectationMessage = "be null or default")] + public static bool IsNullOrDefault(this string value, T obj) where T : class + { + return obj == null; + } +} diff --git a/TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithValueTypeConstraint.cs b/TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithValueTypeConstraint.cs new file mode 100644 index 0000000000..50addfe5b7 --- /dev/null +++ b/TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithValueTypeConstraint.cs @@ -0,0 +1,16 @@ +using TUnit.Assertions.Attributes; + +namespace TUnit.Assertions.Tests.TestData; + +/// +/// Test case: Method with value type constraint (struct) +/// Should generate assertion preserving the struct constraint +/// +public static partial class ValueTypeConstraintExtensions +{ + [GenerateAssertion(ExpectationMessage = "be the default value")] + public static bool IsDefault(this int value, T obj) where T : struct + { + return EqualityComparer.Default.Equals(obj, default(T)); + } +} diff --git a/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs b/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs index 0b75f2b292..d2c023ea00 100644 --- a/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs +++ b/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs @@ -553,6 +553,17 @@ private static void GenerateAssertConditionClassForMethod(SourceProductionContex attributeData.TargetType.ContainingNamespace?.ToDisplayString() == "System.Threading.Tasks" && attributeData.TargetType.TypeParameters.Length == 0; // Non-generic Task + // Collect generic constraints from the method + var genericConstraints = CollectGenericConstraints(staticMethod); + + // Determine if we need to generate a generic assertion class + var hasMethodTypeParameters = staticMethod.IsGenericMethod && staticMethod.TypeParameters.Length > 0; + string? genericTypeParams = null; + if (hasMethodTypeParameters) + { + genericTypeParams = $"<{string.Join(", ", staticMethod.TypeParameters.Select(tp => tp.Name))}>"; + } + // For Enum.IsDefined, we need to use a generic type parameter instead of the concrete Enum type if (attributeData is { RequiresGenericTypeParameter: true, TargetType.Name: "Enum" }) { @@ -566,6 +577,19 @@ private static void GenerateAssertConditionClassForMethod(SourceProductionContex sourceBuilder.AppendLine($"public class {className} : Assertion"); sourceBuilder.AppendLine($" where TTask : {targetTypeName}"); } + else if (hasMethodTypeParameters) + { + // Generate generic assertion class with the method's type parameters + sourceBuilder.AppendLine($"public class {className}{genericTypeParams} : Assertion<{targetTypeName}>"); + // Apply constraints to the assertion class + if (genericConstraints.Count > 0) + { + foreach (var constraint in genericConstraints) + { + sourceBuilder.AppendLine($" {constraint}"); + } + } + } else { sourceBuilder.AppendLine($"public class {className} : Assertion<{targetTypeName}>"); @@ -592,6 +616,11 @@ private static void GenerateAssertConditionClassForMethod(SourceProductionContex { contextType = "AssertionContext"; } + else if (hasMethodTypeParameters) + { + // Use target type with no generic parameters from method (method generics are separate) + contextType = $"AssertionContext<{targetTypeName}>"; + } else { contextType = $"AssertionContext<{targetTypeName}>"; @@ -624,6 +653,11 @@ private static void GenerateAssertConditionClassForMethod(SourceProductionContex { metadataType = "EvaluationMetadata"; } + else if (hasMethodTypeParameters) + { + // Use target type with no generic parameters from method + metadataType = $"EvaluationMetadata<{targetTypeName}>"; + } else { metadataType = $"EvaluationMetadata<{targetTypeName}>"; @@ -912,6 +946,53 @@ private static string GetSimpleTypeNameFromTypeSymbol(ITypeSymbol type) }; } + /// + /// Collects generic constraints from method type parameters. + /// Returns a list of constraint strings in the format "where T : constraint1, constraint2" + /// + private static List CollectGenericConstraints(IMethodSymbol method) + { + var constraints = new List(); + + if (!method.IsGenericMethod || method.TypeParameters.Length == 0) + { + return constraints; + } + + foreach (var typeParameter in method.TypeParameters) + { + var typeConstraints = new List(); + + if (typeParameter.HasReferenceTypeConstraint) + { + typeConstraints.Add("class"); + } + if (typeParameter.HasValueTypeConstraint) + { + typeConstraints.Add("struct"); + } + if (typeParameter.HasNotNullConstraint) + { + typeConstraints.Add("notnull"); + } + foreach (var constraintType in typeParameter.ConstraintTypes) + { + typeConstraints.Add(constraintType.ToDisplayString()); + } + if (typeParameter.HasConstructorConstraint) + { + typeConstraints.Add("new()"); + } + + if (typeConstraints.Count > 0) + { + constraints.Add($"where {typeParameter.Name} : {string.Join(", ", typeConstraints)}"); + } + } + + return constraints; + } + private static void GenerateMethodsForSpecificOverload(SourceProductionContext context, StringBuilder sourceBuilder, CreateAssertionAttributeData attributeData, IMethodSymbol staticMethod) { var targetTypeName = attributeData.TargetType.ToDisplayString(); @@ -959,6 +1040,17 @@ private static void GenerateMethod(StringBuilder sourceBuilder, string targetTyp attributeData.TargetType.ContainingNamespace?.ToDisplayString() == "System.Threading.Tasks" && attributeData.TargetType.TypeParameters.Length == 0; // Non-generic Task + // Collect generic constraints from the method + var genericConstraints = CollectGenericConstraints(method); + + // Determine if the method has generic type parameters + var hasMethodTypeParameters = method.IsGenericMethod && method.TypeParameters.Length > 0; + string? methodGenericTypeParams = null; + if (hasMethodTypeParameters) + { + methodGenericTypeParams = $"<{string.Join(", ", method.TypeParameters.Select(tp => tp.Name))}>"; + } + // Generate the extension method using the modern IAssertionSource pattern if (attributeData is { RequiresGenericTypeParameter: true, TargetType.Name: "Enum" }) { @@ -969,6 +1061,11 @@ private static void GenerateMethod(StringBuilder sourceBuilder, string targetTyp // For Task, generate a generic method that works with Task and Task sourceBuilder.Append($" public static {assertConditionClassName} {generatedMethodName}(this IAssertionSource source"); } + else if (hasMethodTypeParameters) + { + // Method has generic type parameters - include them in the extension method + sourceBuilder.Append($" public static {assertConditionClassName}{methodGenericTypeParams} {generatedMethodName}{methodGenericTypeParams}(this IAssertionSource<{targetTypeName}> source"); + } else { sourceBuilder.Append($" public static {assertConditionClassName} {generatedMethodName}(this IAssertionSource<{targetTypeName}> source"); @@ -998,6 +1095,12 @@ private static void GenerateMethod(StringBuilder sourceBuilder, string targetTyp sourceBuilder.AppendLine(); sourceBuilder.Append($" where TTask : {targetTypeName}"); } + else if (hasMethodTypeParameters && genericConstraints.Count > 0) + { + // Add generic constraints from the method + sourceBuilder.AppendLine(); + sourceBuilder.Append($" {string.Join(" ", genericConstraints)}"); + } sourceBuilder.AppendLine(); sourceBuilder.AppendLine(" {"); @@ -1022,6 +1125,11 @@ private static void GenerateMethod(StringBuilder sourceBuilder, string targetTyp { sourceBuilder.Append($" return new {assertConditionClassName}(source.Context"); } + else if (hasMethodTypeParameters) + { + // Include generic type parameters in the assertion class instantiation + sourceBuilder.Append($" return new {assertConditionClassName}{methodGenericTypeParams}(source.Context"); + } else { sourceBuilder.Append($" return new {assertConditionClassName}(source.Context"); diff --git a/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs b/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs index 48a9a055e4..d32840697b 100644 --- a/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs +++ b/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs @@ -226,6 +226,9 @@ private static void GenerateAssertionClass(StringBuilder sb, AssertionMethodData var genericDeclaration = genericParams.Length > 0 ? $"<{string.Join(", ", genericParams)}>" : ""; var isNullable = data.TargetType.IsReferenceType || data.TargetType.NullableAnnotation == NullableAnnotation.Annotated; + // Collect generic constraints from the method + var genericConstraints = CollectGenericConstraints(data.Method); + // Class declaration sb.AppendLine($"/// "); sb.AppendLine($"/// Generated assertion for {data.Method.Name}"); @@ -238,6 +241,16 @@ private static void GenerateAssertionClass(StringBuilder sb, AssertionMethodData } sb.AppendLine($"public sealed class {className}{genericDeclaration} : Assertion<{targetTypeName}>"); + + // Apply generic constraints if present + if (genericConstraints.Count > 0) + { + foreach (var constraint in genericConstraints) + { + sb.AppendLine($" {constraint}"); + } + } + sb.AppendLine("{"); // Private fields for additional parameters @@ -414,6 +427,9 @@ private static void GenerateExtensionMethod(StringBuilder sb, AssertionMethodDat var genericParams = GetGenericTypeParameters(data.TargetType, data.Method); var genericDeclaration = genericParams.Length > 0 ? $"<{string.Join(", ", genericParams)}>" : ""; + // Collect generic constraints from the method + var genericConstraints = CollectGenericConstraints(data.Method); + // XML documentation sb.AppendLine(" /// "); sb.AppendLine($" /// Generated extension method for {methodName}"); @@ -443,6 +459,16 @@ private static void GenerateExtensionMethod(StringBuilder sb, AssertionMethodDat } sb.AppendLine(")"); + + // Apply generic constraints if present + if (genericConstraints.Count > 0) + { + foreach (var constraint in genericConstraints) + { + sb.AppendLine($" {constraint}"); + } + } + sb.AppendLine(" {"); // Build expression string @@ -538,6 +564,53 @@ private static string GetSimpleTypeName(ITypeSymbol type) return simpleName; } + /// + /// Collects generic constraints from method type parameters. + /// Returns a list of constraint strings in the format "where T : constraint1, constraint2" + /// + private static List CollectGenericConstraints(IMethodSymbol method) + { + var constraints = new List(); + + if (!method.IsGenericMethod || method.TypeParameters.Length == 0) + { + return constraints; + } + + foreach (var typeParameter in method.TypeParameters) + { + var typeConstraints = new List(); + + if (typeParameter.HasReferenceTypeConstraint) + { + typeConstraints.Add("class"); + } + if (typeParameter.HasValueTypeConstraint) + { + typeConstraints.Add("struct"); + } + if (typeParameter.HasNotNullConstraint) + { + typeConstraints.Add("notnull"); + } + foreach (var constraintType in typeParameter.ConstraintTypes) + { + typeConstraints.Add(constraintType.ToDisplayString()); + } + if (typeParameter.HasConstructorConstraint) + { + typeConstraints.Add("new()"); + } + + if (typeConstraints.Count > 0) + { + constraints.Add($"where {typeParameter.Name} : {string.Join(", ", typeConstraints)}"); + } + } + + return constraints; + } + private enum ReturnTypeKind { Bool, diff --git a/dotnet.config b/dotnet.config deleted file mode 100644 index b87edde3a9..0000000000 --- a/dotnet.config +++ /dev/null @@ -1,2 +0,0 @@ -[dotnet.test.runner] -name = "Microsoft.Testing.Platform" \ No newline at end of file diff --git a/global.json b/global.json index dfba20f63f..2e2218a904 100644 --- a/global.json +++ b/global.json @@ -2,5 +2,8 @@ "sdk": { "version": "10.0.100-rc.1.25451.107", "rollForward": "latestFeature" + }, + "test": { + "runner": "Microsoft.Testing.Platform" } } From f76b94f08db66a042ea3916857111ebe0b926206 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 14 Oct 2025 23:06:16 +0100 Subject: [PATCH 4/4] feat(tests): add new equality assertions for enums and custom value types (#3390) --- TUnit.Assertions.Tests/EnumTests.cs | 119 ++++++++++++++++++ .../Extensions/AssertionExtensions.cs | 8 +- ...Has_No_API_Changes.DotNet10_0.verified.txt | 4 +- ..._Has_No_API_Changes.DotNet9_0.verified.txt | 4 +- 4 files changed, 127 insertions(+), 8 deletions(-) diff --git a/TUnit.Assertions.Tests/EnumTests.cs b/TUnit.Assertions.Tests/EnumTests.cs index 5596a044aa..dea4ad0fcd 100644 --- a/TUnit.Assertions.Tests/EnumTests.cs +++ b/TUnit.Assertions.Tests/EnumTests.cs @@ -167,4 +167,123 @@ await Assert.That(async () => await Assert.That(value).DoesNotHaveSameValueAs(value2) ).Throws(); } + + [Test] + public async Task IsEqualTo_Good() + { + var value = MyEnum.One; + + await Assert.That(value).IsEqualTo(MyEnum.One); + } + + [Test] + public async Task IsEqualTo_Bad() + { + var value = MyEnum.One; + + await Assert.That(async () => + await Assert.That(value).IsEqualTo(MyEnum.Two) + ).Throws(); + } + + [Test] + public async Task IsEqualTo_Nullable_Good() + { + MyEnum? value = MyEnum.One; + + await Assert.That(value).IsEqualTo(MyEnum.One); + } + + [Test] + public async Task IsEqualTo_Nullable_Bad() + { + MyEnum? value = MyEnum.One; + + await Assert.That(async () => + await Assert.That(value).IsEqualTo(MyEnum.Two) + ).Throws(); + } + + [Test] + public async Task IsEqualTo_Nullable_Null() + { + MyEnum? value = null; + + await Assert.That(async () => + await Assert.That(value).IsEqualTo(MyEnum.One) + ).Throws(); + } + + [Test] + public async Task IsTypeOf_Enum() + { + object value = MyEnum.One; + + await Assert.That(value).IsTypeOf(); + } + + [Test] + public async Task IsNotEqualTo_Good() + { + var value = MyEnum.One; + + await Assert.That(value).IsNotEqualTo(MyEnum.Two); + } + + [Test] + public async Task IsNotEqualTo_Bad() + { + var value = MyEnum.One; + + await Assert.That(async () => + await Assert.That(value).IsNotEqualTo(MyEnum.One) + ).Throws(); + } + + // Custom value type (struct) tests + public struct CustomValueType + { + public int Value { get; set; } + public string Name { get; set; } + + public CustomValueType(int value, string name) + { + Value = value; + Name = name; + } + } + + [Test] + public async Task CustomValueType_IsEqualTo_Good() + { + var value = new CustomValueType(42, "Test"); + + await Assert.That(value).IsEqualTo(new CustomValueType(42, "Test")); + } + + [Test] + public async Task CustomValueType_IsEqualTo_Bad() + { + var value = new CustomValueType(42, "Test"); + + await Assert.That(async () => + await Assert.That(value).IsEqualTo(new CustomValueType(99, "Different")) + ).Throws(); + } + + [Test] + public async Task CustomValueType_IsNotEqualTo_Good() + { + var value = new CustomValueType(42, "Test"); + + await Assert.That(value).IsNotEqualTo(new CustomValueType(99, "Different")); + } + + [Test] + public async Task CustomValueType_IsTypeOf() + { + object value = new CustomValueType(42, "Test"); + + await Assert.That(value).IsTypeOf(); + } } diff --git a/TUnit.Assertions/Extensions/AssertionExtensions.cs b/TUnit.Assertions/Extensions/AssertionExtensions.cs index 226f79d6a5..a845e9a273 100644 --- a/TUnit.Assertions/Extensions/AssertionExtensions.cs +++ b/TUnit.Assertions/Extensions/AssertionExtensions.cs @@ -206,9 +206,9 @@ public static EqualsAssertion IsEqualTo( /// Asserts that a struct implementing IEquatable<TExpected> is equal to the expected value. /// This enables direct equality comparisons for structs with cross-type IEquatable implementations. /// Example: A Wrapper struct implementing IEquatable<long> can be compared directly to a long value. - /// Priority 1: Higher priority than generic fallback, uses type-specific IEquatable.Equals. + /// Priority -1: Lower than generic fallback; only used for cross-type IEquatable scenarios (TActual != TExpected). /// - [OverloadResolutionPriority(1)] + [OverloadResolutionPriority(-1)] public static EquatableAssertion IsEqualTo( this IAssertionSource source, TExpected expected, @@ -222,9 +222,9 @@ public static EquatableAssertion IsEqualTo /// Asserts that a nullable struct implementing IEquatable<TExpected> is equal to the expected value. /// Handles nullable structs with cross-type IEquatable implementations. - /// Priority 1: Higher priority than generic fallback, uses type-specific IEquatable.Equals. + /// Priority -1: Lower than generic fallback; only used for cross-type IEquatable scenarios (TActual != TExpected). /// - [OverloadResolutionPriority(1)] + [OverloadResolutionPriority(-1)] public static NullableEquatableAssertion IsEqualTo( this IAssertionSource source, TExpected expected, 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 d93d0bf91d..6dc804c66b 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 @@ -1685,10 +1685,10 @@ namespace .Extensions public static . IsEqualTo(this . source, string expected, comparison, [.("expected")] string? expression = null) { } [.(0)] public static . IsEqualTo(this . source, TValue expected, [.("expected")] string? expression = null) { } - [.(1)] + [.(-1)] public static . IsEqualTo(this . source, TExpected expected, [.("expected")] string? expression = null) where TActual : struct, { } - [.(1)] + [.(-1)] public static . IsEqualTo(this . source, TExpected expected, [.("expected")] string? expression = null) where TActual : struct, { } public static . IsEquivalentTo(this . source, object? expected, [.("expected")] string? expression = null) { } 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 9413a01ac5..c1e61d2657 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 @@ -1685,10 +1685,10 @@ namespace .Extensions public static . IsEqualTo(this . source, string expected, comparison, [.("expected")] string? expression = null) { } [.(0)] public static . IsEqualTo(this . source, TValue expected, [.("expected")] string? expression = null) { } - [.(1)] + [.(-1)] public static . IsEqualTo(this . source, TExpected expected, [.("expected")] string? expression = null) where TActual : struct, { } - [.(1)] + [.(-1)] public static . IsEqualTo(this . source, TExpected expected, [.("expected")] string? expression = null) where TActual : struct, { } public static . IsEquivalentTo(this . source, object? expected, [.("expected")] string? expression = null) { }