From 3d8ca4cfe9d337d924aafc0e49de209b59ae6773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sat, 17 Oct 2026 17:46:36 +0000 Subject: [PATCH 01/49] test: make HttpStatusCode generator snapshot independent of test host ReferencesHelper builds the compilation's references from the assemblies already loaded in the test process. System.Net.Primitives (home of HttpStatusCode on .NET) is not loaded by a plain run of the test exe, but is loaded by server-mode hosts (`dotnet test`, Rider, VS). The .NET snapshots were captured with HttpStatusCode unresolved (bare type name, spurious `value is null` checks), so the test failed whenever it ran from an IDE or `dotnet test`. Reference the assembly explicitly and update the net8/9/10 snapshots; they now match the existing Net4_7 snapshot. --- ...atusCodeAssertions.DotNet10_0.verified.txt | 105 ++++++------------ ...tatusCodeAssertions.DotNet8_0.verified.txt | 105 ++++++------------ ...tatusCodeAssertions.DotNet9_0.verified.txt | 105 ++++++------------ .../ReferencesHelper.cs | 3 + 4 files changed, 108 insertions(+), 210 deletions(-) diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/HttpStatusCodeAssertionGeneratorTests.GeneratesHttpStatusCodeAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/HttpStatusCodeAssertionGeneratorTests.GeneratesHttpStatusCodeAssertions.DotNet10_0.verified.txt index 9a654aa3ff7..0229802d5bd 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/HttpStatusCodeAssertionGeneratorTests.GeneratesHttpStatusCodeAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/HttpStatusCodeAssertionGeneratorTests.GeneratesHttpStatusCodeAssertions.DotNet10_0.verified.txt @@ -14,16 +14,16 @@ namespace TUnit.Assertions.Extensions; /// /// Generated assertion for IsSuccess /// -public sealed class HttpStatusCode_IsSuccess_Assertion : Assertion +public sealed class HttpStatusCode_IsSuccess_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsSuccess_Assertion(AssertionContext context) + public HttpStatusCode_IsSuccess_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -33,12 +33,7 @@ public sealed class HttpStatusCode_IsSuccess_Assertion : Assertion= 200 and < 300; + var result = (int)value is >= 200 and < 300; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -53,16 +48,16 @@ public sealed class HttpStatusCode_IsSuccess_Assertion : Assertion /// Generated assertion for IsNotSuccess /// -public sealed class HttpStatusCode_IsNotSuccess_Assertion : Assertion +public sealed class HttpStatusCode_IsNotSuccess_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsNotSuccess_Assertion(AssertionContext context) + public HttpStatusCode_IsNotSuccess_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -72,12 +67,7 @@ public sealed class HttpStatusCode_IsNotSuccess_Assertion : Assertion= 300; + var result = (int)value is < 200 or >= 300; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -92,16 +82,16 @@ public sealed class HttpStatusCode_IsNotSuccess_Assertion : Assertion /// Generated assertion for IsClientError /// -public sealed class HttpStatusCode_IsClientError_Assertion : Assertion +public sealed class HttpStatusCode_IsClientError_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsClientError_Assertion(AssertionContext context) + public HttpStatusCode_IsClientError_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -111,12 +101,7 @@ public sealed class HttpStatusCode_IsClientError_Assertion : Assertion= 400 and < 500; + var result = (int)value is >= 400 and < 500; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -131,16 +116,16 @@ public sealed class HttpStatusCode_IsClientError_Assertion : Assertion /// Generated assertion for IsServerError /// -public sealed class HttpStatusCode_IsServerError_Assertion : Assertion +public sealed class HttpStatusCode_IsServerError_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsServerError_Assertion(AssertionContext context) + public HttpStatusCode_IsServerError_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -150,12 +135,7 @@ public sealed class HttpStatusCode_IsServerError_Assertion : Assertion= 500 and < 600; + var result = (int)value is >= 500 and < 600; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -170,16 +150,16 @@ public sealed class HttpStatusCode_IsServerError_Assertion : Assertion /// Generated assertion for IsRedirection /// -public sealed class HttpStatusCode_IsRedirection_Assertion : Assertion +public sealed class HttpStatusCode_IsRedirection_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsRedirection_Assertion(AssertionContext context) + public HttpStatusCode_IsRedirection_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -189,12 +169,7 @@ public sealed class HttpStatusCode_IsRedirection_Assertion : Assertion= 300 and < 400; + var result = (int)value is >= 300 and < 400; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -209,16 +184,16 @@ public sealed class HttpStatusCode_IsRedirection_Assertion : Assertion /// Generated assertion for IsInformational /// -public sealed class HttpStatusCode_IsInformational_Assertion : Assertion +public sealed class HttpStatusCode_IsInformational_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsInformational_Assertion(AssertionContext context) + public HttpStatusCode_IsInformational_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -228,12 +203,7 @@ public sealed class HttpStatusCode_IsInformational_Assertion : Assertion= 100 and < 200; + var result = (int)value is >= 100 and < 200; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -248,16 +218,16 @@ public sealed class HttpStatusCode_IsInformational_Assertion : Assertion /// Generated assertion for IsError /// -public sealed class HttpStatusCode_IsError_Assertion : Assertion +public sealed class HttpStatusCode_IsError_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsError_Assertion(AssertionContext context) + public HttpStatusCode_IsError_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -267,12 +237,7 @@ public sealed class HttpStatusCode_IsError_Assertion : Assertion return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); } - if (value is null) - { - return Task.FromResult(AssertionResult.Failed("Actual value is null")); - } - - var result = (int)value! is >= 400 and < 600; + var result = (int)value is >= 400 and < 600; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -289,7 +254,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsSuccess /// - public static HttpStatusCode_IsSuccess_Assertion IsSuccess(this IAssertionSource source) + public static HttpStatusCode_IsSuccess_Assertion IsSuccess(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsSuccess()"); return new HttpStatusCode_IsSuccess_Assertion(source.Context); @@ -298,7 +263,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsNotSuccess /// - public static HttpStatusCode_IsNotSuccess_Assertion IsNotSuccess(this IAssertionSource source) + public static HttpStatusCode_IsNotSuccess_Assertion IsNotSuccess(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsNotSuccess()"); return new HttpStatusCode_IsNotSuccess_Assertion(source.Context); @@ -307,7 +272,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsClientError /// - public static HttpStatusCode_IsClientError_Assertion IsClientError(this IAssertionSource source) + public static HttpStatusCode_IsClientError_Assertion IsClientError(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsClientError()"); return new HttpStatusCode_IsClientError_Assertion(source.Context); @@ -316,7 +281,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsServerError /// - public static HttpStatusCode_IsServerError_Assertion IsServerError(this IAssertionSource source) + public static HttpStatusCode_IsServerError_Assertion IsServerError(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsServerError()"); return new HttpStatusCode_IsServerError_Assertion(source.Context); @@ -325,7 +290,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsRedirection /// - public static HttpStatusCode_IsRedirection_Assertion IsRedirection(this IAssertionSource source) + public static HttpStatusCode_IsRedirection_Assertion IsRedirection(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsRedirection()"); return new HttpStatusCode_IsRedirection_Assertion(source.Context); @@ -334,7 +299,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsInformational /// - public static HttpStatusCode_IsInformational_Assertion IsInformational(this IAssertionSource source) + public static HttpStatusCode_IsInformational_Assertion IsInformational(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsInformational()"); return new HttpStatusCode_IsInformational_Assertion(source.Context); @@ -343,7 +308,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsError /// - public static HttpStatusCode_IsError_Assertion IsError(this IAssertionSource source) + public static HttpStatusCode_IsError_Assertion IsError(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsError()"); return new HttpStatusCode_IsError_Assertion(source.Context); diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/HttpStatusCodeAssertionGeneratorTests.GeneratesHttpStatusCodeAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/HttpStatusCodeAssertionGeneratorTests.GeneratesHttpStatusCodeAssertions.DotNet8_0.verified.txt index 9a654aa3ff7..0229802d5bd 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/HttpStatusCodeAssertionGeneratorTests.GeneratesHttpStatusCodeAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/HttpStatusCodeAssertionGeneratorTests.GeneratesHttpStatusCodeAssertions.DotNet8_0.verified.txt @@ -14,16 +14,16 @@ namespace TUnit.Assertions.Extensions; /// /// Generated assertion for IsSuccess /// -public sealed class HttpStatusCode_IsSuccess_Assertion : Assertion +public sealed class HttpStatusCode_IsSuccess_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsSuccess_Assertion(AssertionContext context) + public HttpStatusCode_IsSuccess_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -33,12 +33,7 @@ public sealed class HttpStatusCode_IsSuccess_Assertion : Assertion= 200 and < 300; + var result = (int)value is >= 200 and < 300; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -53,16 +48,16 @@ public sealed class HttpStatusCode_IsSuccess_Assertion : Assertion /// Generated assertion for IsNotSuccess /// -public sealed class HttpStatusCode_IsNotSuccess_Assertion : Assertion +public sealed class HttpStatusCode_IsNotSuccess_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsNotSuccess_Assertion(AssertionContext context) + public HttpStatusCode_IsNotSuccess_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -72,12 +67,7 @@ public sealed class HttpStatusCode_IsNotSuccess_Assertion : Assertion= 300; + var result = (int)value is < 200 or >= 300; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -92,16 +82,16 @@ public sealed class HttpStatusCode_IsNotSuccess_Assertion : Assertion /// Generated assertion for IsClientError /// -public sealed class HttpStatusCode_IsClientError_Assertion : Assertion +public sealed class HttpStatusCode_IsClientError_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsClientError_Assertion(AssertionContext context) + public HttpStatusCode_IsClientError_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -111,12 +101,7 @@ public sealed class HttpStatusCode_IsClientError_Assertion : Assertion= 400 and < 500; + var result = (int)value is >= 400 and < 500; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -131,16 +116,16 @@ public sealed class HttpStatusCode_IsClientError_Assertion : Assertion /// Generated assertion for IsServerError /// -public sealed class HttpStatusCode_IsServerError_Assertion : Assertion +public sealed class HttpStatusCode_IsServerError_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsServerError_Assertion(AssertionContext context) + public HttpStatusCode_IsServerError_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -150,12 +135,7 @@ public sealed class HttpStatusCode_IsServerError_Assertion : Assertion= 500 and < 600; + var result = (int)value is >= 500 and < 600; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -170,16 +150,16 @@ public sealed class HttpStatusCode_IsServerError_Assertion : Assertion /// Generated assertion for IsRedirection /// -public sealed class HttpStatusCode_IsRedirection_Assertion : Assertion +public sealed class HttpStatusCode_IsRedirection_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsRedirection_Assertion(AssertionContext context) + public HttpStatusCode_IsRedirection_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -189,12 +169,7 @@ public sealed class HttpStatusCode_IsRedirection_Assertion : Assertion= 300 and < 400; + var result = (int)value is >= 300 and < 400; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -209,16 +184,16 @@ public sealed class HttpStatusCode_IsRedirection_Assertion : Assertion /// Generated assertion for IsInformational /// -public sealed class HttpStatusCode_IsInformational_Assertion : Assertion +public sealed class HttpStatusCode_IsInformational_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsInformational_Assertion(AssertionContext context) + public HttpStatusCode_IsInformational_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -228,12 +203,7 @@ public sealed class HttpStatusCode_IsInformational_Assertion : Assertion= 100 and < 200; + var result = (int)value is >= 100 and < 200; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -248,16 +218,16 @@ public sealed class HttpStatusCode_IsInformational_Assertion : Assertion /// Generated assertion for IsError /// -public sealed class HttpStatusCode_IsError_Assertion : Assertion +public sealed class HttpStatusCode_IsError_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsError_Assertion(AssertionContext context) + public HttpStatusCode_IsError_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -267,12 +237,7 @@ public sealed class HttpStatusCode_IsError_Assertion : Assertion return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); } - if (value is null) - { - return Task.FromResult(AssertionResult.Failed("Actual value is null")); - } - - var result = (int)value! is >= 400 and < 600; + var result = (int)value is >= 400 and < 600; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -289,7 +254,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsSuccess /// - public static HttpStatusCode_IsSuccess_Assertion IsSuccess(this IAssertionSource source) + public static HttpStatusCode_IsSuccess_Assertion IsSuccess(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsSuccess()"); return new HttpStatusCode_IsSuccess_Assertion(source.Context); @@ -298,7 +263,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsNotSuccess /// - public static HttpStatusCode_IsNotSuccess_Assertion IsNotSuccess(this IAssertionSource source) + public static HttpStatusCode_IsNotSuccess_Assertion IsNotSuccess(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsNotSuccess()"); return new HttpStatusCode_IsNotSuccess_Assertion(source.Context); @@ -307,7 +272,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsClientError /// - public static HttpStatusCode_IsClientError_Assertion IsClientError(this IAssertionSource source) + public static HttpStatusCode_IsClientError_Assertion IsClientError(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsClientError()"); return new HttpStatusCode_IsClientError_Assertion(source.Context); @@ -316,7 +281,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsServerError /// - public static HttpStatusCode_IsServerError_Assertion IsServerError(this IAssertionSource source) + public static HttpStatusCode_IsServerError_Assertion IsServerError(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsServerError()"); return new HttpStatusCode_IsServerError_Assertion(source.Context); @@ -325,7 +290,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsRedirection /// - public static HttpStatusCode_IsRedirection_Assertion IsRedirection(this IAssertionSource source) + public static HttpStatusCode_IsRedirection_Assertion IsRedirection(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsRedirection()"); return new HttpStatusCode_IsRedirection_Assertion(source.Context); @@ -334,7 +299,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsInformational /// - public static HttpStatusCode_IsInformational_Assertion IsInformational(this IAssertionSource source) + public static HttpStatusCode_IsInformational_Assertion IsInformational(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsInformational()"); return new HttpStatusCode_IsInformational_Assertion(source.Context); @@ -343,7 +308,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsError /// - public static HttpStatusCode_IsError_Assertion IsError(this IAssertionSource source) + public static HttpStatusCode_IsError_Assertion IsError(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsError()"); return new HttpStatusCode_IsError_Assertion(source.Context); diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/HttpStatusCodeAssertionGeneratorTests.GeneratesHttpStatusCodeAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/HttpStatusCodeAssertionGeneratorTests.GeneratesHttpStatusCodeAssertions.DotNet9_0.verified.txt index 9a654aa3ff7..0229802d5bd 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/HttpStatusCodeAssertionGeneratorTests.GeneratesHttpStatusCodeAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/HttpStatusCodeAssertionGeneratorTests.GeneratesHttpStatusCodeAssertions.DotNet9_0.verified.txt @@ -14,16 +14,16 @@ namespace TUnit.Assertions.Extensions; /// /// Generated assertion for IsSuccess /// -public sealed class HttpStatusCode_IsSuccess_Assertion : Assertion +public sealed class HttpStatusCode_IsSuccess_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsSuccess_Assertion(AssertionContext context) + public HttpStatusCode_IsSuccess_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -33,12 +33,7 @@ public sealed class HttpStatusCode_IsSuccess_Assertion : Assertion= 200 and < 300; + var result = (int)value is >= 200 and < 300; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -53,16 +48,16 @@ public sealed class HttpStatusCode_IsSuccess_Assertion : Assertion /// Generated assertion for IsNotSuccess /// -public sealed class HttpStatusCode_IsNotSuccess_Assertion : Assertion +public sealed class HttpStatusCode_IsNotSuccess_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsNotSuccess_Assertion(AssertionContext context) + public HttpStatusCode_IsNotSuccess_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -72,12 +67,7 @@ public sealed class HttpStatusCode_IsNotSuccess_Assertion : Assertion= 300; + var result = (int)value is < 200 or >= 300; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -92,16 +82,16 @@ public sealed class HttpStatusCode_IsNotSuccess_Assertion : Assertion /// Generated assertion for IsClientError /// -public sealed class HttpStatusCode_IsClientError_Assertion : Assertion +public sealed class HttpStatusCode_IsClientError_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsClientError_Assertion(AssertionContext context) + public HttpStatusCode_IsClientError_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -111,12 +101,7 @@ public sealed class HttpStatusCode_IsClientError_Assertion : Assertion= 400 and < 500; + var result = (int)value is >= 400 and < 500; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -131,16 +116,16 @@ public sealed class HttpStatusCode_IsClientError_Assertion : Assertion /// Generated assertion for IsServerError /// -public sealed class HttpStatusCode_IsServerError_Assertion : Assertion +public sealed class HttpStatusCode_IsServerError_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsServerError_Assertion(AssertionContext context) + public HttpStatusCode_IsServerError_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -150,12 +135,7 @@ public sealed class HttpStatusCode_IsServerError_Assertion : Assertion= 500 and < 600; + var result = (int)value is >= 500 and < 600; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -170,16 +150,16 @@ public sealed class HttpStatusCode_IsServerError_Assertion : Assertion /// Generated assertion for IsRedirection /// -public sealed class HttpStatusCode_IsRedirection_Assertion : Assertion +public sealed class HttpStatusCode_IsRedirection_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsRedirection_Assertion(AssertionContext context) + public HttpStatusCode_IsRedirection_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -189,12 +169,7 @@ public sealed class HttpStatusCode_IsRedirection_Assertion : Assertion= 300 and < 400; + var result = (int)value is >= 300 and < 400; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -209,16 +184,16 @@ public sealed class HttpStatusCode_IsRedirection_Assertion : Assertion /// Generated assertion for IsInformational /// -public sealed class HttpStatusCode_IsInformational_Assertion : Assertion +public sealed class HttpStatusCode_IsInformational_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsInformational_Assertion(AssertionContext context) + public HttpStatusCode_IsInformational_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -228,12 +203,7 @@ public sealed class HttpStatusCode_IsInformational_Assertion : Assertion= 100 and < 200; + var result = (int)value is >= 100 and < 200; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -248,16 +218,16 @@ public sealed class HttpStatusCode_IsInformational_Assertion : Assertion /// Generated assertion for IsError /// -public sealed class HttpStatusCode_IsError_Assertion : Assertion +public sealed class HttpStatusCode_IsError_Assertion : Assertion { private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed); - public HttpStatusCode_IsError_Assertion(AssertionContext context) + public HttpStatusCode_IsError_Assertion(AssertionContext context) : base(context) { } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; @@ -267,12 +237,7 @@ public sealed class HttpStatusCode_IsError_Assertion : Assertion return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}")); } - if (value is null) - { - return Task.FromResult(AssertionResult.Failed("Actual value is null")); - } - - var result = (int)value! is >= 400 and < 600; + var result = (int)value is >= 400 and < 600; return result ? _passedTask : Task.FromResult(AssertionResult.Failed($"found {value}")); @@ -289,7 +254,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsSuccess /// - public static HttpStatusCode_IsSuccess_Assertion IsSuccess(this IAssertionSource source) + public static HttpStatusCode_IsSuccess_Assertion IsSuccess(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsSuccess()"); return new HttpStatusCode_IsSuccess_Assertion(source.Context); @@ -298,7 +263,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsNotSuccess /// - public static HttpStatusCode_IsNotSuccess_Assertion IsNotSuccess(this IAssertionSource source) + public static HttpStatusCode_IsNotSuccess_Assertion IsNotSuccess(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsNotSuccess()"); return new HttpStatusCode_IsNotSuccess_Assertion(source.Context); @@ -307,7 +272,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsClientError /// - public static HttpStatusCode_IsClientError_Assertion IsClientError(this IAssertionSource source) + public static HttpStatusCode_IsClientError_Assertion IsClientError(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsClientError()"); return new HttpStatusCode_IsClientError_Assertion(source.Context); @@ -316,7 +281,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsServerError /// - public static HttpStatusCode_IsServerError_Assertion IsServerError(this IAssertionSource source) + public static HttpStatusCode_IsServerError_Assertion IsServerError(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsServerError()"); return new HttpStatusCode_IsServerError_Assertion(source.Context); @@ -325,7 +290,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsRedirection /// - public static HttpStatusCode_IsRedirection_Assertion IsRedirection(this IAssertionSource source) + public static HttpStatusCode_IsRedirection_Assertion IsRedirection(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsRedirection()"); return new HttpStatusCode_IsRedirection_Assertion(source.Context); @@ -334,7 +299,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsInformational /// - public static HttpStatusCode_IsInformational_Assertion IsInformational(this IAssertionSource source) + public static HttpStatusCode_IsInformational_Assertion IsInformational(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsInformational()"); return new HttpStatusCode_IsInformational_Assertion(source.Context); @@ -343,7 +308,7 @@ public static partial class HttpStatusCodeAssertionExtensions /// /// Generated extension method for IsError /// - public static HttpStatusCode_IsError_Assertion IsError(this IAssertionSource source) + public static HttpStatusCode_IsError_Assertion IsError(this IAssertionSource source) { source.Context.ExpressionBuilder.Append(".IsError()"); return new HttpStatusCode_IsError_Assertion(source.Context); diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/ReferencesHelper.cs b/tests/TUnit.Assertions.SourceGenerator.Tests/ReferencesHelper.cs index b5dd459183d..c0d8cf916ea 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/ReferencesHelper.cs +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/ReferencesHelper.cs @@ -17,6 +17,9 @@ internal class ReferencesHelper MetadataReference.CreateFromFile(typeof(Polyfill).Assembly.Location), MetadataReference.CreateFromFile(typeof(System.Runtime.CompilerServices.CallerArgumentExpressionAttribute).Assembly.Location), MetadataReference.CreateFromFile(typeof(Uri).Assembly.Location), + // Not loaded by a plain test run, but loaded by IDE/`dotnet test` server-mode hosts, so it must + // be explicit or HttpStatusCode resolves differently depending on how the tests are launched. + MetadataReference.CreateFromFile(typeof(System.Net.HttpStatusCode).Assembly.Location), ]) .ToList(); } From eae530f46fc7f04b37e9a1b55148c155124934fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sat, 17 Oct 2026 18:22:39 +0000 Subject: [PATCH 02/49] test(engine): build and locate launched test apps by own configuration TUnit.Engine.Tests launches TUnit.TestProject, its F#/VB.NET siblings and TUnit.TestProject.HtmlReportDefaults from hard-coded bin/Release folders. CI builds everything in Release, but an IDE builds Debug and does not build TestProject/F#/VB at all (they are not in TUnit.Dev.slnx), so locally the suite failed with missing executables or ran stale Release binaries. Add build-only project references for the launched apps and resolve their output folder from this assembly's own configuration. CI is unchanged (Release/Release); a Debug run from Rider/VS/dotnet test now builds what it launches and passes without manual Release builds. --- tests/TUnit.Engine.Tests/EnvironmentVariables.cs | 9 ++++++++- tests/TUnit.Engine.Tests/ExternalCancellationTests.cs | 2 +- tests/TUnit.Engine.Tests/FSharp.cs | 2 +- tests/TUnit.Engine.Tests/HtmlReportCliTests.cs | 2 +- tests/TUnit.Engine.Tests/InvokableTestBase.cs | 2 +- tests/TUnit.Engine.Tests/ListTestsFilterTests.cs | 2 +- tests/TUnit.Engine.Tests/SigtermTests.cs | 2 +- tests/TUnit.Engine.Tests/TUnit.Engine.Tests.csproj | 5 +++++ tests/TUnit.Engine.Tests/VB.cs | 2 +- 9 files changed, 20 insertions(+), 8 deletions(-) diff --git a/tests/TUnit.Engine.Tests/EnvironmentVariables.cs b/tests/TUnit.Engine.Tests/EnvironmentVariables.cs index 17966a0bc1f..188f209a760 100644 --- a/tests/TUnit.Engine.Tests/EnvironmentVariables.cs +++ b/tests/TUnit.Engine.Tests/EnvironmentVariables.cs @@ -1,4 +1,6 @@ -namespace TUnit.Engine.Tests; +using System.Reflection; + +namespace TUnit.Engine.Tests; public class EnvironmentVariables { @@ -6,6 +8,11 @@ public class EnvironmentVariables public static readonly bool IsNetFramework = NetVersion?.StartsWith("net4") == true; + // The launched test applications are built as project references of this project, so they share its + // configuration: Release in CI, Debug when an IDE builds and runs the solution. + public static readonly string BuildConfiguration = + typeof(EnvironmentVariables).Assembly.GetCustomAttribute()?.Configuration ?? "Release"; + // Keep the report for the outer TUnit.Engine.Tests suite, but do not generate or upload // reports for the short-lived test applications that it launches as implementation details. public static Dictionary DisableHtmlReporterForChildProcess() => new() diff --git a/tests/TUnit.Engine.Tests/ExternalCancellationTests.cs b/tests/TUnit.Engine.Tests/ExternalCancellationTests.cs index 87c988a9879..3d0aa73120d 100644 --- a/tests/TUnit.Engine.Tests/ExternalCancellationTests.cs +++ b/tests/TUnit.Engine.Tests/ExternalCancellationTests.cs @@ -46,7 +46,7 @@ private async Task RunTestWithExternalCancellation(string filter, string markerF using var forcefulCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(25)); // Use cross-platform executable detection (Linux: no extension, Windows: .exe) - var binDir = new DirectoryInfo(Path.Combine(testProject.DirectoryName!, "bin", "Release", GetEnvironmentVariable)); + var binDir = new DirectoryInfo(Path.Combine(testProject.DirectoryName!, "bin", EnvironmentVariables.BuildConfiguration, GetEnvironmentVariable)); var file = binDir.GetFiles("TUnit.TestProject").FirstOrDefault()?.FullName ?? binDir.GetFiles("TUnit.TestProject.exe").First().FullName; diff --git a/tests/TUnit.Engine.Tests/FSharp.cs b/tests/TUnit.Engine.Tests/FSharp.cs index 3ec701c4ef2..3b92df7bed3 100644 --- a/tests/TUnit.Engine.Tests/FSharp.cs +++ b/tests/TUnit.Engine.Tests/FSharp.cs @@ -21,7 +21,7 @@ public async Task Test() "run", "--no-build", "-f", "net10.0", - "--configuration", "Release", + "--configuration", EnvironmentVariables.BuildConfiguration, "--report-trx", "--report-trx-filename", trxFilename, "--diagnostic-verbosity", "Debug", "--diagnostic", "--diagnostic-file-prefix", $"log_{GetType().Name}_", diff --git a/tests/TUnit.Engine.Tests/HtmlReportCliTests.cs b/tests/TUnit.Engine.Tests/HtmlReportCliTests.cs index 4a1839a641e..407b7c5da15 100644 --- a/tests/TUnit.Engine.Tests/HtmlReportCliTests.cs +++ b/tests/TUnit.Engine.Tests/HtmlReportCliTests.cs @@ -94,7 +94,7 @@ private static Task RunTUnitOnlyProject( "--no-build", "--project", testProject.FullName, "--framework", "net10.0", - "--configuration", "Release", + "--configuration", EnvironmentVariables.BuildConfiguration, "--", "--treenode-filter", "/*/*/DefaultHtmlReportTests/Pass", "--results-directory", resultsDirectory, diff --git a/tests/TUnit.Engine.Tests/InvokableTestBase.cs b/tests/TUnit.Engine.Tests/InvokableTestBase.cs index f3da6e32554..48106ac464d 100644 --- a/tests/TUnit.Engine.Tests/InvokableTestBase.cs +++ b/tests/TUnit.Engine.Tests/InvokableTestBase.cs @@ -50,7 +50,7 @@ private async Task RunWithoutAot(string filter, { var testProject = Sourcy.DotNet.Projects.TUnit_TestProject; var projectName = Path.GetFileNameWithoutExtension(testProject.Name); - var binDir = new DirectoryInfo(Path.Combine(testProject.DirectoryName!, "bin", "Release", GetEnvironmentVariable)); + var binDir = new DirectoryInfo(Path.Combine(testProject.DirectoryName!, "bin", EnvironmentVariables.BuildConfiguration, GetEnvironmentVariable)); var executable = binDir.EnumerateFiles(projectName).FirstOrDefault() ?? binDir.EnumerateFiles(projectName + ".exe").First(); diff --git a/tests/TUnit.Engine.Tests/ListTestsFilterTests.cs b/tests/TUnit.Engine.Tests/ListTestsFilterTests.cs index 241929889c0..184577ca8dc 100644 --- a/tests/TUnit.Engine.Tests/ListTestsFilterTests.cs +++ b/tests/TUnit.Engine.Tests/ListTestsFilterTests.cs @@ -52,7 +52,7 @@ private static async Task RunListTestsAsync(bool reflecti { var testProject = Sourcy.DotNet.Projects.TUnit_TestProject; var projectName = Path.GetFileNameWithoutExtension(testProject.Name); - var binDir = new DirectoryInfo(Path.Combine(testProject.DirectoryName!, "bin", "Release", NetVersion)); + var binDir = new DirectoryInfo(Path.Combine(testProject.DirectoryName!, "bin", EnvironmentVariables.BuildConfiguration, NetVersion)); var executable = binDir.EnumerateFiles(projectName).FirstOrDefault() ?? binDir.EnumerateFiles(projectName + ".exe").First(); diff --git a/tests/TUnit.Engine.Tests/SigtermTests.cs b/tests/TUnit.Engine.Tests/SigtermTests.cs index c447c78361a..871b54469b4 100644 --- a/tests/TUnit.Engine.Tests/SigtermTests.cs +++ b/tests/TUnit.Engine.Tests/SigtermTests.cs @@ -26,7 +26,7 @@ public async Task Sigterm_Cancels_Gracefully_And_Runs_After_Hooks(bool reflectio var afterMarker = Path.Combine(markerDirectory, "after.txt"); var testProject = Sourcy.DotNet.Projects.TUnit_TestProject; - var executable = Path.Combine(testProject.DirectoryName!, "bin", "Release", NetVersion, "TUnit.TestProject"); + var executable = Path.Combine(testProject.DirectoryName!, "bin", EnvironmentVariables.BuildConfiguration, NetVersion, "TUnit.TestProject"); var environmentVariables = EnvironmentVariables.DisableHtmlReporterForChildProcess(); environmentVariables["TUNIT_SIGTERM_MARKER_DIR"] = markerDirectory; diff --git a/tests/TUnit.Engine.Tests/TUnit.Engine.Tests.csproj b/tests/TUnit.Engine.Tests/TUnit.Engine.Tests.csproj index eae576b9440..3b26c37a168 100644 --- a/tests/TUnit.Engine.Tests/TUnit.Engine.Tests.csproj +++ b/tests/TUnit.Engine.Tests/TUnit.Engine.Tests.csproj @@ -29,6 +29,11 @@ + + + + diff --git a/tests/TUnit.Engine.Tests/VB.cs b/tests/TUnit.Engine.Tests/VB.cs index 936ec40e4ea..01d45df8aac 100644 --- a/tests/TUnit.Engine.Tests/VB.cs +++ b/tests/TUnit.Engine.Tests/VB.cs @@ -21,7 +21,7 @@ public async Task Test() "run", "--no-build", "-f", "net10.0", - "--configuration", "Release", + "--configuration", EnvironmentVariables.BuildConfiguration, "--report-trx", "--report-trx-filename", trxFilename, "--diagnostic-verbosity", "Debug", "--diagnostic", "--diagnostic-file-prefix", $"log_{GetType().Name}_", From b81ed962b4c8289675dd03ae047057dce5cd8472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sat, 17 Oct 2026 18:54:56 +0000 Subject: [PATCH 03/49] test: run SourceGenerator.IncrementalTests on xUnit v3 / MTP The project was an xUnit v2 VSTest project without a runner adapter, while global.json makes `dotnet test` Microsoft.Testing.Platform-only. `dotnet test` therefore refused it ("All projects must use that test runner"), which also aborted solution-wide runs of TUnit.slnx, and its tests only ran under an IDE's bundled xUnit runner. Move it to xunit.v3 with the MTP runner (already pinned centrally). No test code changes; all 64 tests pass both via `dotnet test` and as a plain executable. --- .../TUnit.SourceGenerator.IncrementalTests.csproj | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/TUnit.SourceGenerator.IncrementalTests.csproj b/tests/TUnit.SourceGenerator.IncrementalTests/TUnit.SourceGenerator.IncrementalTests.csproj index 063a016f6ce..4f48a6529bd 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/TUnit.SourceGenerator.IncrementalTests.csproj +++ b/tests/TUnit.SourceGenerator.IncrementalTests/TUnit.SourceGenerator.IncrementalTests.csproj @@ -5,14 +5,19 @@ enable enable false + + Exe + true + false + true - $(NoWarn);MSB3277 + $(NoWarn);MSB3277;xUnit1051 TUnit.Assertions.SourceGenerator.IncrementalTests - - + From 6aa6737fa709f951eae78d75aff142bc1087b653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sat, 17 Oct 2026 19:41:37 +0000 Subject: [PATCH 04/49] test: start child dotnet processes with the running .NET installation RpcTests and the Engine F#/VB/HTML-report tests start `dotnet build` / `dotnet run` via a bare "dotnet", i.e. whatever is first on PATH. An IDE does not necessarily share that PATH with the installation it builds and runs tests with (notably macOS apps started from the Dock), and that dotnet can lack an SDK satisfying global.json: the child then exits with 155 (SdkResolveFailure) and the tests fail with no explanation. Resolve the muxer from DOTNET_HOST_PATH, else from the runtime running the test process, falling back to PATH. Include the build output in the RpcTests pre-build failure message. --- tests/SharedTestHelpers/DotNetCli.cs | 36 +++++++++++++++++++ tests/TUnit.Engine.Tests/FSharp.cs | 3 +- .../TUnit.Engine.Tests/HtmlReportCliTests.cs | 3 +- .../TUnit.Engine.Tests.csproj | 4 +++ tests/TUnit.Engine.Tests/VB.cs | 3 +- tests/TUnit.RpcTests/TUnit.RpcTests.csproj | 4 +++ tests/TUnit.RpcTests/TestHostSession.cs | 3 +- tests/TUnit.RpcTests/TestProjectBuilds.cs | 9 +++-- 8 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 tests/SharedTestHelpers/DotNetCli.cs diff --git a/tests/SharedTestHelpers/DotNetCli.cs b/tests/SharedTestHelpers/DotNetCli.cs new file mode 100644 index 00000000000..14579e6b889 --- /dev/null +++ b/tests/SharedTestHelpers/DotNetCli.cs @@ -0,0 +1,36 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace TUnit.Tests.Shared; + +/// +/// Resolves the dotnet executable used to start child dotnet build/dotnet run processes. +/// +/// +/// A bare dotnet resolves through PATH, which an IDE does not necessarily share with the +/// installation it builds and runs tests with (on macOS, apps started from the Dock or Finder get a different +/// PATH than a terminal). That dotnet can lack an SDK satisfying global.json and exit with +/// 155 (SdkResolveFailure). Prefer the installation that is running the current test process. +/// +internal static class DotNetCli +{ + public static string Path { get; } = Resolve(); + + private static string Resolve() + { + // Set by the .NET CLI for processes it starts (e.g. under `dotnet test`). + var hostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); + if (!string.IsNullOrEmpty(hostPath) && File.Exists(hostPath)) + { + return hostPath; + } + + // /shared/Microsoft.NETCore.App// -> /dotnet + var runtimeDirectory = RuntimeEnvironment.GetRuntimeDirectory(); + var root = System.IO.Path.GetFullPath(System.IO.Path.Combine(runtimeDirectory, "..", "..", "..")); + var muxer = System.IO.Path.Combine(root, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"); + + return File.Exists(muxer) ? muxer : "dotnet"; + } +} diff --git a/tests/TUnit.Engine.Tests/FSharp.cs b/tests/TUnit.Engine.Tests/FSharp.cs index 3b92df7bed3..8bbad4ce7a4 100644 --- a/tests/TUnit.Engine.Tests/FSharp.cs +++ b/tests/TUnit.Engine.Tests/FSharp.cs @@ -2,6 +2,7 @@ using CliWrap.Buffered; using Shouldly; using TUnit.Engine.Tests.Enums; +using TUnit.Tests.Shared; namespace TUnit.Engine.Tests; @@ -15,7 +16,7 @@ public async Task Test() var testProject = Sourcy.DotNet.Projects.TUnit_TestProject_FSharp; var guid = Guid.NewGuid().ToString("N"); var trxFilename = guid + ".trx"; - var command = Cli.Wrap("dotnet") + var command = Cli.Wrap(DotNetCli.Path) .WithArguments( [ "run", diff --git a/tests/TUnit.Engine.Tests/HtmlReportCliTests.cs b/tests/TUnit.Engine.Tests/HtmlReportCliTests.cs index 407b7c5da15..cd25055a023 100644 --- a/tests/TUnit.Engine.Tests/HtmlReportCliTests.cs +++ b/tests/TUnit.Engine.Tests/HtmlReportCliTests.cs @@ -2,6 +2,7 @@ using CliWrap.Buffered; using Shouldly; using TUnit.Engine.Tests.Enums; +using TUnit.Tests.Shared; namespace TUnit.Engine.Tests; @@ -106,7 +107,7 @@ private static Task RunTUnitOnlyProject( arguments.Add("--reflection"); } - return Cli.Wrap("dotnet") + return Cli.Wrap(DotNetCli.Path) .WithArguments(arguments) .WithWorkingDirectory(testProject.DirectoryName!) .WithEnvironmentVariables(new Dictionary diff --git a/tests/TUnit.Engine.Tests/TUnit.Engine.Tests.csproj b/tests/TUnit.Engine.Tests/TUnit.Engine.Tests.csproj index 3b26c37a168..10e8d4dbe11 100644 --- a/tests/TUnit.Engine.Tests/TUnit.Engine.Tests.csproj +++ b/tests/TUnit.Engine.Tests/TUnit.Engine.Tests.csproj @@ -8,6 +8,10 @@ ..\..\eng\strongname.snk + + + + diff --git a/tests/TUnit.Engine.Tests/VB.cs b/tests/TUnit.Engine.Tests/VB.cs index 01d45df8aac..54cb789a57e 100644 --- a/tests/TUnit.Engine.Tests/VB.cs +++ b/tests/TUnit.Engine.Tests/VB.cs @@ -2,6 +2,7 @@ using CliWrap.Buffered; using Shouldly; using TUnit.Engine.Tests.Enums; +using TUnit.Tests.Shared; namespace TUnit.Engine.Tests; @@ -15,7 +16,7 @@ public async Task Test() var testProject = Sourcy.DotNet.Projects.TUnit_TestProject_VB_NET; var guid = Guid.NewGuid().ToString("N"); var trxFilename = guid + ".trx"; - var command = Cli.Wrap("dotnet") + var command = Cli.Wrap(DotNetCli.Path) .WithArguments( [ "run", diff --git a/tests/TUnit.RpcTests/TUnit.RpcTests.csproj b/tests/TUnit.RpcTests/TUnit.RpcTests.csproj index d15c5164119..4e7eb2aba50 100644 --- a/tests/TUnit.RpcTests/TUnit.RpcTests.csproj +++ b/tests/TUnit.RpcTests/TUnit.RpcTests.csproj @@ -10,6 +10,10 @@ false + + + + diff --git a/tests/TUnit.RpcTests/TestHostSession.cs b/tests/TUnit.RpcTests/TestHostSession.cs index bcb83d5c09b..17df0dd0a96 100644 --- a/tests/TUnit.RpcTests/TestHostSession.cs +++ b/tests/TUnit.RpcTests/TestHostSession.cs @@ -4,6 +4,7 @@ using StreamJsonRpc; using TUnit.RpcTests.Clients; using TUnit.RpcTests.Models; +using TUnit.Tests.Shared; namespace TUnit.RpcTests; @@ -46,7 +47,7 @@ public static async Task StartAsync(string framework, Cancellat listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; - var cliProcess = Cli.Wrap("dotnet") + var cliProcess = Cli.Wrap(DotNetCli.Path) .WithWorkingDirectory(TestProjectBuilds.WorkingDirectory) .WithArguments([ "run", diff --git a/tests/TUnit.RpcTests/TestProjectBuilds.cs b/tests/TUnit.RpcTests/TestProjectBuilds.cs index 94cff67706c..5e651d363dd 100644 --- a/tests/TUnit.RpcTests/TestProjectBuilds.cs +++ b/tests/TUnit.RpcTests/TestProjectBuilds.cs @@ -1,5 +1,7 @@ using System.Collections.Concurrent; using CliWrap; +using CliWrap.Buffered; +using TUnit.Tests.Shared; namespace TUnit.RpcTests; @@ -18,15 +20,16 @@ public static Task EnsureBuiltAsync(string framework, CancellationToken cancella private static async Task BuildAsync(string framework, CancellationToken cancellationToken) { - var result = await Cli.Wrap("dotnet") + var result = await Cli.Wrap(DotNetCli.Path) .WithWorkingDirectory(WorkingDirectory) .WithArguments(["build", "-c", "Debug", "-f", framework, "--nologo", "-v", "quiet"]) .WithValidation(CommandResultValidation.None) - .ExecuteAsync(cancellationToken); + .ExecuteBufferedAsync(cancellationToken); if (result.ExitCode != 0) { - throw new InvalidOperationException($"Test host pre-build ({framework}) failed with exit code {result.ExitCode}"); + throw new InvalidOperationException( + $"Test host pre-build ({framework}) failed with exit code {result.ExitCode} using '{DotNetCli.Path}':{Environment.NewLine}{result.StandardOutput}{result.StandardError}"); } } } From e762a871eb1fd67bb49fc574dfec1eadcc3a64f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sat, 17 Oct 2026 20:00:35 +0000 Subject: [PATCH 05/49] test: pin invariant culture where tests assert invariant number text These tests compare against invariant spellings ("1.1", "1.5", "-1", "[-5]") of values formatted with the current culture, so they failed on machines with e.g. sv-SE (decimal comma, U+2212 MINUS SIGN). In TUnit.TestProject this also failed TUnit.Engine.Tests' ExpectedStateTests, which requires every EngineTest=Pass test to pass. Mark them [InvariantCulture]; the product's culture-aware formatting is unchanged. --- tests/TUnit.Assertions.Tests/CollectionAssertionTests.cs | 3 +++ tests/TUnit.Mocks.Tests/AsyncTests.cs | 2 ++ tests/TUnit.Mocks.Tests/WrapNoParameterlessCtorTests.cs | 2 ++ tests/TUnit.TestProject/Bugs/2136/Tests.cs | 4 +++- 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/TUnit.Assertions.Tests/CollectionAssertionTests.cs b/tests/TUnit.Assertions.Tests/CollectionAssertionTests.cs index 24c21cfb101..720a92e8ab9 100644 --- a/tests/TUnit.Assertions.Tests/CollectionAssertionTests.cs +++ b/tests/TUnit.Assertions.Tests/CollectionAssertionTests.cs @@ -1,4 +1,5 @@ using TUnit.Assertions.Extensions; +using TUnit.Core.Executors; namespace TUnit.Assertions.Tests; @@ -315,6 +316,7 @@ await Assert.That(names) } [Test] + [InvariantCulture] public async Task All_Predicate_Failure_Message_Contains_Index_And_Value() { var items = new[] { 2, 4, -5, 8 }; @@ -339,6 +341,7 @@ await Assert.That(names).All(x => !string.IsNullOrEmpty(x)) } [Test] + [InvariantCulture] public async Task All_Predicate_Failure_Message_Contains_First_Failing_Item() { var items = new[] { 1, 2, 3, -1, -2, -3 }; diff --git a/tests/TUnit.Mocks.Tests/AsyncTests.cs b/tests/TUnit.Mocks.Tests/AsyncTests.cs index df46158d069..093b09bff03 100644 --- a/tests/TUnit.Mocks.Tests/AsyncTests.cs +++ b/tests/TUnit.Mocks.Tests/AsyncTests.cs @@ -1,3 +1,4 @@ +using TUnit.Core.Executors; using TUnit.Mocks; using TUnit.Mocks.Arguments; @@ -429,6 +430,7 @@ public async Task Typed_Throws_On_Async_Task_Method() } [Test] + [InvariantCulture] public async Task Typed_Throws_On_Async_ValueTask_Method() { var mock = IAsyncService.Mock(); diff --git a/tests/TUnit.Mocks.Tests/WrapNoParameterlessCtorTests.cs b/tests/TUnit.Mocks.Tests/WrapNoParameterlessCtorTests.cs index 6f44a9bd990..3aca9ca1d5b 100644 --- a/tests/TUnit.Mocks.Tests/WrapNoParameterlessCtorTests.cs +++ b/tests/TUnit.Mocks.Tests/WrapNoParameterlessCtorTests.cs @@ -1,3 +1,4 @@ +using TUnit.Core.Executors; using TUnit.Mocks; using TUnit.Mocks.Arguments; using TUnit.Mocks.Verification; @@ -174,6 +175,7 @@ public async Task Single_Value_Param() } [Test] + [InvariantCulture] public async Task Multiple_Params() { var mock = Mock.Wrap(new WrapMultiParam("widget", 3, 1.5)); diff --git a/tests/TUnit.TestProject/Bugs/2136/Tests.cs b/tests/TUnit.TestProject/Bugs/2136/Tests.cs index c57d2e6d19f..a015211d62a 100644 --- a/tests/TUnit.TestProject/Bugs/2136/Tests.cs +++ b/tests/TUnit.TestProject/Bugs/2136/Tests.cs @@ -1,4 +1,5 @@ -using TUnit.TestProject.Attributes; +using TUnit.Core.Executors; +using TUnit.TestProject.Attributes; namespace TUnit.TestProject.Bugs._2136; @@ -6,6 +7,7 @@ namespace TUnit.TestProject.Bugs._2136; public class Tests { [Test] + [InvariantCulture] [Arguments(true, "True")] [Arguments(1, "1")] [Arguments(1.1, "1.1")] From 9285eddb96a038411a5e57448dba9e019ef13409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sat, 17 Oct 2026 20:00:35 +0000 Subject: [PATCH 06/49] fix(aspire): format resource exit codes with the invariant culture Exit codes are identifiers people search for (e.g. -532462766 / 0xE0434352, the .NET unhandled exception code). Under cultures such as sv-SE the diagnostics printed negative codes with U+2212 MINUS SIGN, which also failed FormatExitCode_KnownCode_IncludesNumberAndMeaning. --- src/TUnit.Aspire.Core/AspireFixture.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/TUnit.Aspire.Core/AspireFixture.cs b/src/TUnit.Aspire.Core/AspireFixture.cs index c5369c35564..675ddd13686 100644 --- a/src/TUnit.Aspire.Core/AspireFixture.cs +++ b/src/TUnit.Aspire.Core/AspireFixture.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Collections.Immutable; using System.Diagnostics; +using System.Globalization; using System.Text; using Aspire.Hosting; using Aspire.Hosting.ApplicationModel; @@ -1705,9 +1706,14 @@ internal static string FormatExitCode(int? exitCode) return "(unknown)"; } - return DecodeExitCode(code) is { } meaning ? $"{code} ({meaning})" : code.ToString(); + var number = FormatExitCodeNumber(code); + return DecodeExitCode(code) is { } meaning ? $"{number} ({meaning})" : number; } + // Invariant: exit codes are identifiers to search for, and cultures such as sv-SE would otherwise + // print negative Windows codes with U+2212 MINUS SIGN. + private static string FormatExitCodeNumber(int code) => code.ToString(CultureInfo.InvariantCulture); + /// One-line human description of a resource's failure state for the concise exception message. internal static string DescribeState(in ResourceDiagnosis d) => d.Class switch { @@ -1807,7 +1813,7 @@ private static string SummarizeHealthReport(HealthReportSnapshot report) } return dep.Snapshot.ExitCode is int code - ? $"'{depName}' ({depState}, exit code {code})" + ? $"'{depName}' ({depState}, exit code {FormatExitCodeNumber(code)})" : $"'{depName}' ({depState})"; } @@ -1987,7 +1993,7 @@ private void AppendResourceDiagnosticBlock( sb.AppendLine($" State: {diagnosis.State}"); if (diagnosis.ExitCode is int code) { - sb.Append($" Exit code: {code}"); + sb.Append($" Exit code: {FormatExitCodeNumber(code)}"); if (DecodeExitCode(code) is { } meaning) { sb.Append($" ({meaning})"); @@ -2078,7 +2084,7 @@ private static void AppendDependencyChain(StringBuilder sb, ResourceNotification if (notificationService.TryGetCurrentState(depName, out var dep) && dep is not null) { var depState = dep.Snapshot.State?.Text ?? "unknown"; - description = dep.Snapshot.ExitCode is int code ? $"{depState}, exit code {code}" : depState; + description = dep.Snapshot.ExitCode is int code ? $"{depState}, exit code {FormatExitCodeNumber(code)}" : depState; } else { From 69f6cafd66f4c97af85bf186b5a4148919616677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sat, 17 Oct 2026 20:07:47 +0000 Subject: [PATCH 07/49] test: make bug-2136 test culture-invariant without changing its metadata An earlier commit in this branch marked GenericArgumentsTest [InvariantCulture], but TUnit.Core.SourceGenerator.Tests snapshots the generator output for this file and the generator emits the test's attributes (and line numbers), so the snapshot no longer matched. Format the value invariantly in the method body instead, which leaves the generated metadata unchanged. --- tests/TUnit.TestProject/Bugs/2136/Tests.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/TUnit.TestProject/Bugs/2136/Tests.cs b/tests/TUnit.TestProject/Bugs/2136/Tests.cs index a015211d62a..bbd68fc49b3 100644 --- a/tests/TUnit.TestProject/Bugs/2136/Tests.cs +++ b/tests/TUnit.TestProject/Bugs/2136/Tests.cs @@ -1,5 +1,4 @@ -using TUnit.Core.Executors; -using TUnit.TestProject.Attributes; +using TUnit.TestProject.Attributes; namespace TUnit.TestProject.Bugs._2136; @@ -7,7 +6,6 @@ namespace TUnit.TestProject.Bugs._2136; public class Tests { [Test] - [InvariantCulture] [Arguments(true, "True")] [Arguments(1, "1")] [Arguments(1.1, "1.1")] @@ -16,7 +14,8 @@ public class Tests public async Task GenericArgumentsTest(T value, string expected) { // Assert - await Assert.That(value?.ToString()).IsEqualTo(expected); + // Invariant: the expected strings use invariant formatting (e.g. "1.1"), whatever the machine culture. + await Assert.That(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture)).IsEqualTo(expected); } } From 34aaea581786c30a778bb8d7f2163b9a5264f60f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sat, 17 Oct 2026 20:07:47 +0000 Subject: [PATCH 08/49] test: drop unused xunit references from source generator test projects Both projects are TUnit-only (no xUnit tests or usings) but referenced xunit and xunit.runner.visualstudio, which also put a VSTest adapter into these Microsoft.Testing.Platform test apps. --- .../TUnit.Assertions.SourceGenerator.Tests.csproj | 5 ----- .../TUnit.Core.SourceGenerator.Tests.csproj | 5 ----- 2 files changed, 10 deletions(-) diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/TUnit.Assertions.SourceGenerator.Tests.csproj b/tests/TUnit.Assertions.SourceGenerator.Tests/TUnit.Assertions.SourceGenerator.Tests.csproj index 0f97d6bddcd..fed222aa83c 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/TUnit.Assertions.SourceGenerator.Tests.csproj +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/TUnit.Assertions.SourceGenerator.Tests.csproj @@ -32,11 +32,6 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/tests/TUnit.Core.SourceGenerator.Tests/TUnit.Core.SourceGenerator.Tests.csproj b/tests/TUnit.Core.SourceGenerator.Tests/TUnit.Core.SourceGenerator.Tests.csproj index ef364e55c2b..c6f29e9fe09 100644 --- a/tests/TUnit.Core.SourceGenerator.Tests/TUnit.Core.SourceGenerator.Tests.csproj +++ b/tests/TUnit.Core.SourceGenerator.Tests/TUnit.Core.SourceGenerator.Tests.csproj @@ -39,11 +39,6 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - From 8356d0dcceb109412d3cc938fba2d3c89283b3bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sat, 17 Oct 2026 20:14:42 +0000 Subject: [PATCH 09/49] test: make culture-sensitive tests culture-independent instead of pinning Replace the [InvariantCulture] pins added earlier in this branch, which only hid the tests' implicit en-US assumption, with expectations that hold in any culture: - CollectionAssertionTests: TUnit formats values in failure messages with the current culture, so build the expected "[-5]" the same way and keep testing what a user in that culture sees. - AsyncTests: compare against the message formatted like the throwing factory formats it. - WrapMultiParam fixture: culture is incidental, format invariantly. Verified under en-US, sv-SE, de-DE, tr-TR and ar-SA. --- tests/TUnit.Assertions.Tests/CollectionAssertionTests.cs | 8 +++----- tests/TUnit.Mocks.Tests/AsyncTests.cs | 5 ++--- tests/TUnit.Mocks.Tests/WrapNoParameterlessCtorTests.cs | 4 +--- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/TUnit.Assertions.Tests/CollectionAssertionTests.cs b/tests/TUnit.Assertions.Tests/CollectionAssertionTests.cs index 720a92e8ab9..782dea62771 100644 --- a/tests/TUnit.Assertions.Tests/CollectionAssertionTests.cs +++ b/tests/TUnit.Assertions.Tests/CollectionAssertionTests.cs @@ -1,5 +1,4 @@ using TUnit.Assertions.Extensions; -using TUnit.Core.Executors; namespace TUnit.Assertions.Tests; @@ -316,7 +315,6 @@ await Assert.That(names) } [Test] - [InvariantCulture] public async Task All_Predicate_Failure_Message_Contains_Index_And_Value() { var items = new[] { 2, 4, -5, 8 }; @@ -325,7 +323,8 @@ await Assert.That(async () => await Assert.That(items).All(x => x > 0) ).Throws() .WithMessageContaining("index 2") - .And.WithMessageContaining("[-5]"); + // Values are formatted with the current culture (e.g. U+2212 MINUS SIGN under sv-SE). + .And.WithMessageContaining($"[{-5}]"); } [Test] @@ -341,7 +340,6 @@ await Assert.That(names).All(x => !string.IsNullOrEmpty(x)) } [Test] - [InvariantCulture] public async Task All_Predicate_Failure_Message_Contains_First_Failing_Item() { var items = new[] { 1, 2, 3, -1, -2, -3 }; @@ -350,6 +348,6 @@ await Assert.That(async () => await Assert.That(items).All(x => x > 0) ).Throws() .WithMessageContaining("index 3") - .And.WithMessageContaining("[-1]"); + .And.WithMessageContaining($"[{-1}]"); } } diff --git a/tests/TUnit.Mocks.Tests/AsyncTests.cs b/tests/TUnit.Mocks.Tests/AsyncTests.cs index 093b09bff03..88a89d950c8 100644 --- a/tests/TUnit.Mocks.Tests/AsyncTests.cs +++ b/tests/TUnit.Mocks.Tests/AsyncTests.cs @@ -1,4 +1,3 @@ -using TUnit.Core.Executors; using TUnit.Mocks; using TUnit.Mocks.Arguments; @@ -430,7 +429,6 @@ public async Task Typed_Throws_On_Async_Task_Method() } [Test] - [InvariantCulture] public async Task Typed_Throws_On_Async_ValueTask_Method() { var mock = IAsyncService.Mock(); @@ -440,6 +438,7 @@ public async Task Typed_Throws_On_Async_ValueTask_Method() IAsyncService service = mock.Object; var ex = await Assert.ThrowsAsync(async () => await service.ComputeValueTaskAsync(-1)); - await Assert.That(ex.Message).Contains("Invalid input: -1"); + // Formatted the same way as the factory above, so it matches under any culture. + await Assert.That(ex.Message).Contains($"Invalid input: {-1}"); } } diff --git a/tests/TUnit.Mocks.Tests/WrapNoParameterlessCtorTests.cs b/tests/TUnit.Mocks.Tests/WrapNoParameterlessCtorTests.cs index 3aca9ca1d5b..cd2131a1fed 100644 --- a/tests/TUnit.Mocks.Tests/WrapNoParameterlessCtorTests.cs +++ b/tests/TUnit.Mocks.Tests/WrapNoParameterlessCtorTests.cs @@ -1,4 +1,3 @@ -using TUnit.Core.Executors; using TUnit.Mocks; using TUnit.Mocks.Arguments; using TUnit.Mocks.Verification; @@ -44,7 +43,7 @@ public WrapMultiParam(string label, int count, double factor) _count = count; _factor = factor; } - public virtual string Compute() => $"{_label}:{_count}:{_factor}"; + public virtual string Compute() => FormattableString.Invariant($"{_label}:{_count}:{_factor}"); } public class WrapParamsOnly @@ -175,7 +174,6 @@ public async Task Single_Value_Param() } [Test] - [InvariantCulture] public async Task Multiple_Params() { var mock = Mock.Wrap(new WrapMultiParam("widget", 3, 1.5)); From 573e8fc314e4b2cae01265012fd091a01f7ca22c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sat, 17 Oct 2026 20:24:56 +0000 Subject: [PATCH 10/49] test: migrate SourceGenerator.IncrementalTests from xUnit to TUnit The last non-TUnit test project in the repo. It now uses the shared eng/TestProject.props/.targets setup like every other test project, so it runs on the in-repo engine and analyzers. Attributes were converted with TUnit's own TUXU0001 code fixer; the assertions (which the fixer left as Xunit.Assert in these synchronous methods) were converted to awaited TUnit assertions, with the test methods and assertion helpers made async. Built with warnings as errors to rule out un-awaited helpers; a deliberately broken shared helper fails 8 tests, so assertions propagate. All 64 tests pass via `dotnet test` and as a plain executable. Drops an unused helper. --- .../AotConverterGeneratorIncrementalTests.cs | 48 +++---- ...rtionExtensionGeneratorIncrementalTests.cs | 32 ++--- ...ssertionMethodGeneratorIncrementalTests.cs | 50 +++---- .../DynamicTestsGeneratorIncrementalTests.cs | 40 +++--- .../Fixture.cs | 9 -- .../HookMetadataGeneratorIncrementalTests.cs | 88 ++++++------ ...InfrastructureGeneratorIncrementalTests.cs | 130 +++++++++--------- ...ethodAssertionGeneratorIncrementalTests.cs | 83 +++++------ ...njectionSourceGeneratorIncrementalTests.cs | 58 ++++---- ...houldExtensionGeneratorIncrementalTests.cs | 110 +++++++-------- ...InitializationGeneratorIncrementalTests.cs | 58 ++++---- ...it.SourceGenerator.IncrementalTests.csproj | 22 +-- .../TestHelper.cs | 18 +-- 13 files changed, 365 insertions(+), 381 deletions(-) diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/AotConverterGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/AotConverterGeneratorIncrementalTests.cs index c7bc6e687b4..3966d71525c 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/AotConverterGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/AotConverterGeneratorIncrementalTests.cs @@ -51,42 +51,42 @@ public void Test1(FooBar data) } """; - [Fact] - public void AddUnrelatedType_MethodShouldNotRegenerate() + [Test] + public async Task AddUnrelatedType_MethodShouldNotRegenerate() { var syntaxTree = CSharpSyntaxTree.ParseText(DefaultConverter, CSharpParseOptions.Default); var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New, 1); + await AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New, 1); var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}")); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached, 1); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached, 1); } - [Fact] - public void AddNewConverterShouldRegenerate() + [Test] + public async Task AddNewConverterShouldRegenerate() { var syntaxTree = CSharpSyntaxTree.ParseText(DefaultConverter, CSharpParseOptions.Default); var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New, 1); + await AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New, 1); var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SecondConverter)); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified, 2); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified, 2); } - [Fact] - public void ModifyOperatorShouldRegenerate() + [Test] + public async Task ModifyOperatorShouldRegenerate() { var syntaxTree = CSharpSyntaxTree.ParseText(DefaultConverter, CSharpParseOptions.Default); var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New, 1); + await AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New, 1); var compilation2 = TestHelper.ReplaceTypeDeclaration(compilation1, "Foo", """ @@ -98,32 +98,32 @@ public record Foo ); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified, 1); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified, 1); } - [Fact] - public void EditNonTestMethodShouldNotRegenerate() + [Test] + public async Task EditNonTestMethodShouldNotRegenerate() { var syntaxTree = CSharpSyntaxTree.ParseText(DefaultConverter, CSharpParseOptions.Default); var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New, 1); + await AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New, 1); var compilation2 = TestHelper.ReplaceMethodDeclaration(compilation1, "Data", "public static IEnumerable Data() => [new(), new()];"); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached, 1); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached, 1); } - [Fact] - public void ChangeTestParameterShouldRegenerate() + [Test] + public async Task ChangeTestParameterShouldRegenerate() { var syntaxTree = CSharpSyntaxTree.ParseText(DefaultConverter, CSharpParseOptions.Default); var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New, 1); + await AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New, 1); var compilation2 = TestHelper.ReplaceMethodDeclaration(compilation1, "Test1", """ @@ -133,10 +133,10 @@ public void Test1(int data) } """); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified, 0); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified, 0); } - private static void AssertRunReasons( + private static async Task AssertRunReasons( GeneratorDriver driver, IncrementalGeneratorRunReasons reasons, int conversionMetadataLength, @@ -146,8 +146,8 @@ private static void AssertRunReasons( var runResult = driver.GetRunResult().Results[0]; var runValue = runResult.TrackedSteps[AotConverterGenerator.ParseAotConverter][0].Outputs[0].Value; var runState = (ValueTuple, bool>)runValue; - Xunit.Assert.Equal(conversionMetadataLength, runState.Item1.Length); + await Assert.That(runState.Item1.Length).IsEqualTo(conversionMetadataLength); - TestHelper.AssertRunReason(runResult, AotConverterGenerator.ParseAotConverter, reasons.BuildStep, outputIndex); + await TestHelper.AssertRunReason(runResult, AotConverterGenerator.ParseAotConverter, reasons.BuildStep, outputIndex); } -} +} \ No newline at end of file diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/AssertionExtensionGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/AssertionExtensionGeneratorIncrementalTests.cs index 9327e55b8df..d5c2d1741c2 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/AssertionExtensionGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/AssertionExtensionGeneratorIncrementalTests.cs @@ -49,8 +49,8 @@ public void Method() { } } """; - [Fact] - public void AddUnrelatedTypeShouldNotRegenerate() + [Test] + public async Task AddUnrelatedTypeShouldNotRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource)); var driver1 = TestHelper.GenerateTracked(compilation1); @@ -59,13 +59,13 @@ public void AddUnrelatedTypeShouldNotRegenerate() var driver2 = driver1.RunGenerators(compilation2); var runResult = driver2.GetRunResult().Results[0]; - TestHelper.AssertAllRunReasons(runResult, AssertionExtensionGenerator.AssertionExtensionStep, + await TestHelper.AssertAllRunReasons(runResult, AssertionExtensionGenerator.AssertionExtensionStep, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); - TestHelper.AssertSourceOutputsCached(runResult); + await TestHelper.AssertSourceOutputsCached(runResult); } - [Fact] - public void EditUnrelatedTypeShouldNotRegenerate() + [Test] + public async Task EditUnrelatedTypeShouldNotRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource)); var driver1 = TestHelper.GenerateTracked(compilation1); @@ -74,13 +74,13 @@ public void EditUnrelatedTypeShouldNotRegenerate() var driver2 = driver1.RunGenerators(compilation2); var runResult = driver2.GetRunResult().Results[0]; - TestHelper.AssertAllRunReasons(runResult, AssertionExtensionGenerator.AssertionExtensionStep, + await TestHelper.AssertAllRunReasons(runResult, AssertionExtensionGenerator.AssertionExtensionStep, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); - TestHelper.AssertSourceOutputsCached(runResult); + await TestHelper.AssertSourceOutputsCached(runResult); } - [Fact] - public void ModifyAssertionClassShouldRegenerateOnlyThatClass() + [Test] + public async Task ModifyAssertionClassShouldRegenerateOnlyThatClass() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource)); var driver1 = TestHelper.GenerateTracked(compilation1); @@ -107,16 +107,16 @@ protected override Task CheckAsync(EvaluationMetadata m .SelectMany(x => x.Outputs) .Select(x => x.Reason) .ToArray(); - Xunit.Assert.Equal(2, reasons.Length); - Xunit.Assert.Contains(reasons[0], new[] { IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged }); - Xunit.Assert.Equal(IncrementalStepRunReason.Modified, reasons[1]); + await Assert.That(reasons.Length).IsEqualTo(2); + await Assert.That(new[] { IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged }).Contains(reasons[0]); + await Assert.That(reasons[1]).IsEqualTo(IncrementalStepRunReason.Modified); var sourceOutputReasons = runResult.TrackedOutputSteps .SelectMany(x => x.Value) .SelectMany(x => x.Outputs) .Select(x => x.Reason) .ToArray(); - Xunit.Assert.Single(sourceOutputReasons, IncrementalStepRunReason.Modified); - Xunit.Assert.Contains("IsBaz", runResult.GeneratedSources.Single(x => x.HintName == "BarAssertion.Extensions.g.cs").SourceText.ToString()); + await Assert.That(sourceOutputReasons).HasSingleItem(x => x == IncrementalStepRunReason.Modified); + await Assert.That(runResult.GeneratedSources.Single(x => x.HintName == "BarAssertion.Extensions.g.cs").SourceText.ToString()).Contains("IsBaz"); } -} +} \ No newline at end of file diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/AssertionMethodGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/AssertionMethodGeneratorIncrementalTests.cs index d2a4a40e06a..8c1395aafeb 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/AssertionMethodGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/AssertionMethodGeneratorIncrementalTests.cs @@ -28,20 +28,20 @@ public void Method() { } } """; - [Fact] - public void GenericAndNonGenericAttributesAreDiscovered() + [Test] + public async Task GenericAndNonGenericAttributesAreDiscovered() { var compilation = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource)); var driver = TestHelper.GenerateTracked(compilation); var hintNames = driver.GetRunResult().Results[0].GeneratedSources.Select(x => x.HintName).ToArray(); - Xunit.Assert.Contains("MyCharAssertionExtensions.g.cs", hintNames); - Xunit.Assert.Contains("MyStringAssertionExtensions.g.cs", hintNames); + await Assert.That(hintNames).Contains("MyCharAssertionExtensions.g.cs"); + await Assert.That(hintNames).Contains("MyStringAssertionExtensions.g.cs"); } - [Fact] - public void AddUnrelatedTypeShouldNotRegenerate() + [Test] + public async Task AddUnrelatedTypeShouldNotRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource)); var driver1 = TestHelper.GenerateTracked(compilation1); @@ -49,11 +49,11 @@ public void AddUnrelatedTypeShouldNotRegenerate() var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}")); var driver2 = driver1.RunGenerators(compilation2); - AssertUnchanged(driver2.GetRunResult().Results[0]); + await AssertUnchanged(driver2.GetRunResult().Results[0]); } - [Fact] - public void EditUnrelatedTypeShouldNotRegenerate() + [Test] + public async Task EditUnrelatedTypeShouldNotRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource)); var driver1 = TestHelper.GenerateTracked(compilation1); @@ -61,11 +61,11 @@ public void EditUnrelatedTypeShouldNotRegenerate() var compilation2 = TestHelper.ReplaceMethodDeclaration(compilation1, "Method", "public void Method() { var x = 1; }"); var driver2 = driver1.RunGenerators(compilation2); - AssertUnchanged(driver2.GetRunResult().Results[0]); + await AssertUnchanged(driver2.GetRunResult().Results[0]); } - [Fact] - public void ModifyGenericAttributeShouldRegenerate() + [Test] + public async Task ModifyGenericAttributeShouldRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource)); var driver1 = TestHelper.GenerateTracked(compilation1); @@ -80,14 +80,14 @@ public static partial class MyCharAssertionExtensions var driver2 = driver1.RunGenerators(compilation2); var runResult = driver2.GetRunResult().Results[0]; - TestHelper.AssertAnyRunReason(runResult, AssertionMethodGenerator.GenericAssertionFromStep, IncrementalStepRunReason.Modified); - TestHelper.AssertAllRunReasons(runResult, AssertionMethodGenerator.NonGenericAssertionFromStep, + await TestHelper.AssertAnyRunReason(runResult, AssertionMethodGenerator.GenericAssertionFromStep, IncrementalStepRunReason.Modified); + await TestHelper.AssertAllRunReasons(runResult, AssertionMethodGenerator.NonGenericAssertionFromStep, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); - Xunit.Assert.Contains("be a letter!", runResult.GeneratedSources.Single(x => x.HintName == "MyCharAssertionExtensions.g.cs").SourceText.ToString()); + await Assert.That(runResult.GeneratedSources.Single(x => x.HintName == "MyCharAssertionExtensions.g.cs").SourceText.ToString()).Contains("be a letter!"); } - [Fact] - public void ModifyNonGenericAttributeShouldRegenerate() + [Test] + public async Task ModifyNonGenericAttributeShouldRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource)); var driver1 = TestHelper.GenerateTracked(compilation1); @@ -102,18 +102,18 @@ public static partial class MyStringAssertionExtensions var driver2 = driver1.RunGenerators(compilation2); var runResult = driver2.GetRunResult().Results[0]; - TestHelper.AssertAnyRunReason(runResult, AssertionMethodGenerator.NonGenericAssertionFromStep, IncrementalStepRunReason.Modified); - TestHelper.AssertAllRunReasons(runResult, AssertionMethodGenerator.GenericAssertionFromStep, + await TestHelper.AssertAnyRunReason(runResult, AssertionMethodGenerator.NonGenericAssertionFromStep, IncrementalStepRunReason.Modified); + await TestHelper.AssertAllRunReasons(runResult, AssertionMethodGenerator.GenericAssertionFromStep, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); - Xunit.Assert.Contains("IsNullOrWhiteSpace", runResult.GeneratedSources.Single(x => x.HintName == "MyStringAssertionExtensions.g.cs").SourceText.ToString()); + await Assert.That(runResult.GeneratedSources.Single(x => x.HintName == "MyStringAssertionExtensions.g.cs").SourceText.ToString()).Contains("IsNullOrWhiteSpace"); } - private static void AssertUnchanged(GeneratorRunResult runResult) + private static async Task AssertUnchanged(GeneratorRunResult runResult) { - TestHelper.AssertAllRunReasons(runResult, AssertionMethodGenerator.GenericAssertionFromStep, + await TestHelper.AssertAllRunReasons(runResult, AssertionMethodGenerator.GenericAssertionFromStep, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); - TestHelper.AssertAllRunReasons(runResult, AssertionMethodGenerator.NonGenericAssertionFromStep, + await TestHelper.AssertAllRunReasons(runResult, AssertionMethodGenerator.NonGenericAssertionFromStep, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); - TestHelper.AssertSourceOutputsCached(runResult); + await TestHelper.AssertSourceOutputsCached(runResult); } -} +} \ No newline at end of file diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/DynamicTestsGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/DynamicTestsGeneratorIncrementalTests.cs index dc27055539b..0cfc8648daa 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/DynamicTestsGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/DynamicTestsGeneratorIncrementalTests.cs @@ -25,28 +25,28 @@ public void SecondBuildDynamicTests(DynamicTestBuilderContext context) } """; - [Fact] - public void AddUnrelatedType_MethodShouldNotRegenerate() + [Test] + public async Task AddUnrelatedType_MethodShouldNotRegenerate() { var syntaxTree = CSharpSyntaxTree.ParseText(DefaultConverter, CSharpParseOptions.Default); var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + await AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}")); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached); } - [Fact] - public void ModifyDynamicMethodShouldRegenerate() + [Test] + public async Task ModifyDynamicMethodShouldRegenerate() { var syntaxTree = CSharpSyntaxTree.ParseText(DefaultConverter, CSharpParseOptions.Default); var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New, 0); + await AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New, 0); var compilation2 = TestHelper.ReplaceMethodDeclaration(compilation1, "BuildDynamicTests", """ @@ -58,17 +58,17 @@ public static void BuildDynamicTests(DynamicTestBuilderContext context) ); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified); } - [Fact] - public void AddDynamicMethodShouldRegenerate() + [Test] + public async Task AddDynamicMethodShouldRegenerate() { var syntaxTree = CSharpSyntaxTree.ParseText(DefaultConverter, CSharpParseOptions.Default); var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + await AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); var compilation2 = TestHelper.ReplaceMethodDeclaration(compilation1, "SecondBuildDynamicTests", """ @@ -80,18 +80,18 @@ public void SecondBuildDynamicTests(DynamicTestBuilderContext context) ); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached, 0); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.New, 1); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached, 0); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.New, 1); } - [Fact] - public void MoveDynamicMethodToDifferentLine_ShouldRegenerate() + [Test] + public async Task MoveDynamicMethodToDifferentLine_ShouldRegenerate() { var syntaxTree = CSharpSyntaxTree.ParseText(DefaultConverter, CSharpParseOptions.Default); var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + await AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); // The [CallerLineNumber] captured by DynamicTestBuilder feeds the hint name and the // generated DynamicTestBuilderContext, so shifting the method must regenerate. @@ -100,10 +100,10 @@ public void MoveDynamicMethodToDifferentLine_ShouldRegenerate() CSharpSyntaxTree.ParseText(DefaultConverter.Replace("public class DynamicTestArgumentsTests", "\n\npublic class DynamicTestArgumentsTests"), CSharpParseOptions.Default)); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified); } - private static void AssertRunReasons( + private static async Task AssertRunReasons( GeneratorDriver driver, IncrementalGeneratorRunReasons reasons, int outputIndex = 0 @@ -111,6 +111,6 @@ private static void AssertRunReasons( { var runResult = driver.GetRunResult().Results[0]; - TestHelper.AssertRunReason(runResult, DynamicTestsGenerator.ParseDynamicTests, reasons.BuildStep, outputIndex); + await TestHelper.AssertRunReason(runResult, DynamicTestsGenerator.ParseDynamicTests, reasons.BuildStep, outputIndex); } -} +} \ No newline at end of file diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/Fixture.cs b/tests/TUnit.SourceGenerator.IncrementalTests/Fixture.cs index 563750461fd..569f42120cc 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/Fixture.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/Fixture.cs @@ -62,13 +62,4 @@ public static CSharpCompilation CreateLibrary(params SyntaxTree[] source) return compilation; } - - public static async Task SourceFromResourceFile(string file) - { - var currentDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - Assert.NotNull(currentDir); - var resourcesDir = Path.Combine(currentDir, "resources"); - - return await File.ReadAllTextAsync(Path.Combine(resourcesDir, file)); - } } diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/HookMetadataGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/HookMetadataGeneratorIncrementalTests.cs index 5d9249599ae..8f3c56fd117 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/HookMetadataGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/HookMetadataGeneratorIncrementalTests.cs @@ -20,22 +20,22 @@ public void Setup() } """; - [Fact] - public void AddUnrelatedType_ShouldNotRegenerate() + [Test] + public async Task AddUnrelatedType_ShouldNotRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default)); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReason(driver1, IncrementalStepRunReason.New); + await AssertRunReason(driver1, IncrementalStepRunReason.New); var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}")); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReason(driver2, IncrementalStepRunReason.Cached); - TestHelper.AssertRunReason(driver2.GetRunResult().Results[0], HookMetadataGenerator.HookClassGroups, IncrementalStepRunReason.Cached, 0); + await AssertRunReason(driver2, IncrementalStepRunReason.Cached); + await TestHelper.AssertRunReason(driver2.GetRunResult().Results[0], HookMetadataGenerator.HookClassGroups, IncrementalStepRunReason.Cached, 0); } - [Fact] - public void TypesWithSameSanitizedName_ShouldGenerateDistinctFilesAndMembers() + [Test] + public async Task TypesWithSameSanitizedName_ShouldGenerateDistinctFilesAndMembers() { // A_B.C and A.B_C both sanitize to A_B_C. Hint names and the members emitted into the shared // TUnit_HookRegistration partial class must still be unique. @@ -71,18 +71,18 @@ public void Setup() var driver = TestHelper.GenerateTracked(compilation); var runResult = driver.GetRunResult(); - Xunit.Assert.Empty(runResult.Diagnostics); - var generatorResult = Xunit.Assert.Single(runResult.Results); - Xunit.Assert.Null(generatorResult.Exception); - Xunit.Assert.Equal(2, generatorResult.GeneratedSources.Length); - Xunit.Assert.Equal(2, generatorResult.GeneratedSources.Select(static s => s.HintName).Distinct().Count()); + await Assert.That(runResult.Diagnostics).IsEmpty(); + var generatorResult = await Assert.That(runResult.Results).HasSingleItem(); + await Assert.That(generatorResult.Exception).IsNull(); + await Assert.That(generatorResult.GeneratedSources.Length).IsEqualTo(2); + await Assert.That(generatorResult.GeneratedSources.Select(static s => s.HintName).Distinct().Count()).IsEqualTo(2); var outputCompilation = compilation.AddSyntaxTrees(generatorResult.GeneratedSources.Select(static s => s.SyntaxTree)); - Xunit.Assert.Empty(outputCompilation.GetDiagnostics().Where(static d => d.Severity == DiagnosticSeverity.Error)); + await Assert.That(outputCompilation.GetDiagnostics().Where(static d => d.Severity == DiagnosticSeverity.Error)).IsEmpty(); } - [Fact] - public void ChangeHookInOneClass_ShouldOnlyModifyThatClassGroup() + [Test] + public async Task ChangeHookInOneClass_ShouldOnlyModifyThatClassGroup() { const string source = """ @@ -122,17 +122,17 @@ public async Task OtherSetup() var driver2 = driver1.RunGenerators(compilation2); var runResult = driver2.GetRunResult().Results[0]; - TestHelper.AssertRunReason(runResult, HookMetadataGenerator.HookClassGroups, IncrementalStepRunReason.Unchanged, 0); - TestHelper.AssertRunReason(runResult, HookMetadataGenerator.HookClassGroups, IncrementalStepRunReason.Modified, 1); + await TestHelper.AssertRunReason(runResult, HookMetadataGenerator.HookClassGroups, IncrementalStepRunReason.Unchanged, 0); + await TestHelper.AssertRunReason(runResult, HookMetadataGenerator.HookClassGroups, IncrementalStepRunReason.Modified, 1); } - [Fact] - public void MoveHookToDifferentLine_ShouldRegenerate() + [Test] + public async Task MoveHookToDifferentLine_ShouldRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default)); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReason(driver1, IncrementalStepRunReason.New); + await AssertRunReason(driver1, IncrementalStepRunReason.New); // Same hook shifted down two lines: the emitted LineNumber must follow it. var compilation2 = compilation1.ReplaceSyntaxTree( @@ -140,16 +140,16 @@ public void MoveHookToDifferentLine_ShouldRegenerate() CSharpSyntaxTree.ParseText(DefaultSource.Replace("public class HookTests", "\n\npublic class HookTests"), CSharpParseOptions.Default)); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReason(driver2, IncrementalStepRunReason.Modified); + await AssertRunReason(driver2, IncrementalStepRunReason.Modified); } - [Fact] - public void MakeHookAsync_ShouldRegenerate() + [Test] + public async Task MakeHookAsync_ShouldRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default)); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReason(driver1, IncrementalStepRunReason.New); + await AssertRunReason(driver1, IncrementalStepRunReason.New); var compilation2 = TestHelper.ReplaceMethodDeclaration(compilation1, "Setup", """ @@ -161,11 +161,11 @@ public async Task Setup() """); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReason(driver2, IncrementalStepRunReason.Modified); + await AssertRunReason(driver2, IncrementalStepRunReason.Modified); } - [Fact] - public void ClassGroups_AcrossFilesWithDisjointHookKinds_AreEmittedInCompilationOrder() + [Test] + public async Task ClassGroups_AcrossFilesWithDisjointHookKinds_AreEmittedInCompilationOrder() { // The first file's class has only a Before hook besides the shared BeforeEvery hook, and the second // file's class has only an After hook. No single hook kind orders the two files except BeforeEvery, @@ -202,18 +202,18 @@ public static void BeforeEveryAssembly(AssemblyHookContext context) { } CSharpSyntaxTree.ParseText(first, CSharpParseOptions.Default, path: "First.cs"), CSharpSyntaxTree.ParseText(second, CSharpParseOptions.Default, path: "Second.cs")); - AssertGeneratedOrder(compilation, "FirstFileHooks", "SecondFileHooks"); + await AssertGeneratedOrder(compilation, "FirstFileHooks", "SecondFileHooks"); // Reversing the files in the compilation reverses the emitted order. var reversed = Fixture.CreateLibrary( CSharpSyntaxTree.ParseText(second, CSharpParseOptions.Default, path: "Second.cs"), CSharpSyntaxTree.ParseText(first, CSharpParseOptions.Default, path: "First.cs")); - AssertGeneratedOrder(reversed, "SecondFileHooks", "FirstFileHooks"); + await AssertGeneratedOrder(reversed, "SecondFileHooks", "FirstFileHooks"); } - [Fact] - public void ClassGroups_DeclaredOnOneLine_AreEmittedInDeclarationOrder() + [Test] + public async Task ClassGroups_DeclaredOnOneLine_AreEmittedInDeclarationOrder() { const string source = """ @@ -224,11 +224,11 @@ public class ZHooks { [Before(HookType.Test)] public void Setup() { } } public c var compilation = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default)); - AssertGeneratedOrder(compilation, "ZHooks", "AHooks"); + await AssertGeneratedOrder(compilation, "ZHooks", "AHooks"); } - [Fact] - public void AddUnrelatedFile_ShouldNotChangeHookFileOrder() + [Test] + public async Task AddUnrelatedFile_ShouldNotChangeHookFileOrder() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default, path: "Hooks.cs")); @@ -238,29 +238,29 @@ public void AddUnrelatedFile_ShouldNotChangeHookFileOrder() var driver2 = driver1.RunGenerators(compilation2); var runResult = driver2.GetRunResult().Results[0]; - TestHelper.AssertRunReason(runResult, HookMetadataGenerator.HookFileOrder, IncrementalStepRunReason.Unchanged, 0); - TestHelper.AssertRunReason(runResult, HookMetadataGenerator.HookClassGroups, IncrementalStepRunReason.Cached, 0); + await TestHelper.AssertRunReason(runResult, HookMetadataGenerator.HookFileOrder, IncrementalStepRunReason.Unchanged, 0); + await TestHelper.AssertRunReason(runResult, HookMetadataGenerator.HookClassGroups, IncrementalStepRunReason.Cached, 0); } - private static void AssertGeneratedOrder(Compilation compilation, params string[] typeNames) + private static async Task AssertGeneratedOrder(Compilation compilation, params string[] typeNames) { var driver = TestHelper.GenerateTracked(compilation); - var generatorResult = Xunit.Assert.Single(driver.GetRunResult().Results); - Xunit.Assert.Null(generatorResult.Exception); + var generatorResult = await Assert.That(driver.GetRunResult().Results).HasSingleItem(); + await Assert.That(generatorResult.Exception).IsNull(); var hintNames = generatorResult.GeneratedSources.Select(static s => s.HintName).ToArray(); var indices = typeNames .Select(typeName => Array.FindIndex(hintNames, hintName => hintName.Contains(typeName))) .ToArray(); - Xunit.Assert.DoesNotContain(-1, indices); - Xunit.Assert.Equal(indices.OrderBy(static i => i), indices); + await Assert.That(indices).DoesNotContain(-1); + await Assert.That(indices).IsInOrder(); } - private static void AssertRunReason(GeneratorDriver driver, IncrementalStepRunReason reason) + private static async Task AssertRunReason(GeneratorDriver driver, IncrementalStepRunReason reason) { var runResult = driver.GetRunResult().Results[0]; - TestHelper.AssertRunReason(runResult, HookMetadataGenerator.ExtractBeforeHooks, reason, 0); + await TestHelper.AssertRunReason(runResult, HookMetadataGenerator.ExtractBeforeHooks, reason, 0); } -} +} \ No newline at end of file diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/InfrastructureGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/InfrastructureGeneratorIncrementalTests.cs index cc67303266c..48f0fb8d854 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/InfrastructureGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/InfrastructureGeneratorIncrementalTests.cs @@ -20,20 +20,20 @@ public void Test1() } """; - [Fact] - public void EditSource_ShouldNotRegenerate() + [Test] + public async Task EditSource_ShouldNotRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default)); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReason(driver1, IncrementalStepRunReason.New); + await AssertRunReason(driver1, IncrementalStepRunReason.New); // Syntax-only edits rerun the cheap Select, whose model is unchanged, so the generated source // stays cached. Memo_SyntaxOnlyEdits_ReuseModel checks that the reference walk is skipped. var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}")); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReason(driver2, IncrementalStepRunReason.Unchanged); - TestHelper.AssertSourceOutputsCached(driver2.GetRunResult().Results[0]); + await AssertRunReason(driver2, IncrementalStepRunReason.Unchanged); + await TestHelper.AssertSourceOutputsCached(driver2.GetRunResult().Results[0]); var compilation3 = TestHelper.ReplaceMethodDeclaration(compilation1, "Test1", """ @@ -44,12 +44,12 @@ public void Test1() } """); var driver3 = driver2.RunGenerators(compilation3); - AssertRunReason(driver3, IncrementalStepRunReason.Unchanged); - TestHelper.AssertSourceOutputsCached(driver3.GetRunResult().Results[0]); + await AssertRunReason(driver3, IncrementalStepRunReason.Unchanged); + await TestHelper.AssertSourceOutputsCached(driver3.GetRunResult().Results[0]); } - [Fact] - public void EditSource_DoesNotRetainPreviousCompilation() + [Test] + public async Task EditSource_DoesNotRetainPreviousCompilation() { var (driver, firstCompilation, latestCompilation) = RunSourceEdits(10); @@ -61,7 +61,7 @@ public void EditSource_DoesNotRetainPreviousCompilation() // A comparer that reports the new compilation as equal makes the input node keep the old // one, pinning its syntax trees and bound state for as long as the driver lives. - Xunit.Assert.False(firstCompilation.IsAlive, "The generator driver kept the first compilation alive after syntax-only edits."); + await Assert.That(firstCompilation.IsAlive).IsFalse().Because("The generator driver kept the first compilation alive after syntax-only edits."); GC.KeepAlive(driver); GC.KeepAlive(latestCompilation); } @@ -90,13 +90,13 @@ private static (GeneratorDriver Driver, WeakReference FirstCompilation, Compilat private static object? GetExtractedModel(GeneratorDriver driver) => driver.GetRunResult().Results[0].TrackedSteps[InfrastructureGenerator.ExtractAssemblyInfoStep][0].Outputs[0].Value; - [Fact] - public void AddReference_ShouldRegenerate() + [Test] + public async Task AddReference_ShouldRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default)); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReason(driver1, IncrementalStepRunReason.New); + await AssertRunReason(driver1, IncrementalStepRunReason.New); // A library that references TUnit.Core must be pre-loaded by TUnitInfrastructure.g.cs. var library = CSharpCompilation.Create( @@ -107,14 +107,14 @@ public void AddReference_ShouldRegenerate() var compilation2 = compilation1.AddReferences(library.ToMetadataReference()); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReason(driver2, IncrementalStepRunReason.Modified); + await AssertRunReason(driver2, IncrementalStepRunReason.Modified); var generated = driver2.GetRunResult().GeneratedTrees.Single(t => t.FilePath.EndsWith("TUnitInfrastructure.g.cs")); - Xunit.Assert.Contains("global::OtherTestLibrary.SharedHooks", generated.ToString()); + await Assert.That(generated.ToString()).Contains("global::OtherTestLibrary.SharedHooks"); } - [Fact] - public void EditProjectReference_ShouldRegenerate() + [Test] + public async Task EditProjectReference_ShouldRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default)); @@ -122,7 +122,7 @@ public void EditProjectReference_ShouldRegenerate() compilation1 = compilation1.AddReferences(library1.ToMetadataReference()); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReason(driver1, IncrementalStepRunReason.New); + await AssertRunReason(driver1, IncrementalStepRunReason.New); // In the IDE, editing a referenced project replaces its CompilationReference while the // assembly name stays the same. Renaming the only public type must reach the output, @@ -130,23 +130,23 @@ public void EditProjectReference_ShouldRegenerate() var library2 = CreateHookLibrary(compilation1, "RenamedHooks"); var compilation2 = compilation1.ReplaceReference(compilation1.References.Last(), library2.ToMetadataReference()); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReason(driver2, IncrementalStepRunReason.Modified); + await AssertRunReason(driver2, IncrementalStepRunReason.Modified); var generated = driver2.GetRunResult().GeneratedTrees.Single(t => t.FilePath.EndsWith("TUnitInfrastructure.g.cs")).ToString(); - Xunit.Assert.Contains("global::OtherTestLibrary.RenamedHooks", generated); - Xunit.Assert.DoesNotContain("global::OtherTestLibrary.SharedHooks", generated); + await Assert.That(generated).Contains("global::OtherTestLibrary.RenamedHooks"); + await Assert.That(generated).DoesNotContain("global::OtherTestLibrary.SharedHooks"); // An edit that does not change the extracted model reruns extraction but keeps the output cached. var library3 = CreateHookLibrary(compilation1, "RenamedHooks"); var compilation3 = compilation2.ReplaceReference(compilation2.References.Last(), library3.ToMetadataReference()); var driver3 = driver2.RunGenerators(compilation3); - AssertRunReason(driver3, IncrementalStepRunReason.Unchanged); + await AssertRunReason(driver3, IncrementalStepRunReason.Unchanged); } private const string ShadowingSource = "namespace OtherTestLibrary { public class FirstHooks { } }"; - [Fact] - public void FreshDriver_SameReferences_DoesNotShareMemo() + [Test] + public async Task FreshDriver_SameReferences_DoesNotShareMemo() { var compilation = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default)); @@ -154,55 +154,55 @@ public void FreshDriver_SameReferences_DoesNotShareMemo() // extracts fresh instead of reusing another driver's result. var first = GetExtractedModel(TestHelper.GenerateTracked(compilation)); var second = GetExtractedModel(TestHelper.GenerateTracked(compilation)); - Xunit.Assert.NotSame(first, second); - Xunit.Assert.Equal(first, second); + await Assert.That(second).IsNotSameReferenceAs(first); + await Assert.That(second).IsEqualTo(first); } - [Fact] - public void EditSource_SourceSensitiveSelectionIsNotReused() + [Test] + public async Task EditSource_SourceSensitiveSelectionIsNotReused() { var baseCompilation = CreateWithTwoTypeLibrary(); var shadowedCompilation = baseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(ShadowingSource)); // The shadowed run must not memoize its (source-dependent) choice for later compilations. var driver1 = TestHelper.GenerateTracked(shadowedCompilation); - Xunit.Assert.Contains("typeof(global::OtherTestLibrary.SecondHooks)", GenerateInfrastructure(driver1)); + await Assert.That(GenerateInfrastructure(driver1)).Contains("typeof(global::OtherTestLibrary.SecondHooks)"); var driver2 = driver1.RunGenerators(baseCompilation); - AssertRunReason(driver2, IncrementalStepRunReason.Modified); - Xunit.Assert.Contains("typeof(global::OtherTestLibrary.FirstHooks)", GenerateInfrastructure(driver2)); + await AssertRunReason(driver2, IncrementalStepRunReason.Modified); + await Assert.That(GenerateInfrastructure(driver2)).Contains("typeof(global::OtherTestLibrary.FirstHooks)"); } - [Fact] - public void EditSource_ShadowingSelectedType_ShouldRegenerate() + [Test] + public async Task EditSource_ShadowingSelectedType_ShouldRegenerate() { var compilation1 = CreateWithTwoTypeLibrary(); var driver1 = TestHelper.GenerateTracked(compilation1); - Xunit.Assert.Contains("typeof(global::OtherTestLibrary.FirstHooks)", GenerateInfrastructure(driver1)); + await Assert.That(GenerateInfrastructure(driver1)).Contains("typeof(global::OtherTestLibrary.FirstHooks)"); var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText(ShadowingSource)); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReason(driver2, IncrementalStepRunReason.Modified); - Xunit.Assert.Contains("typeof(global::OtherTestLibrary.SecondHooks)", GenerateInfrastructure(driver2)); + await AssertRunReason(driver2, IncrementalStepRunReason.Modified); + await Assert.That(GenerateInfrastructure(driver2)).Contains("typeof(global::OtherTestLibrary.SecondHooks)"); var driver3 = driver2.RunGenerators(compilation1); - AssertRunReason(driver3, IncrementalStepRunReason.Modified); - Xunit.Assert.Contains("typeof(global::OtherTestLibrary.FirstHooks)", GenerateInfrastructure(driver3)); + await AssertRunReason(driver3, IncrementalStepRunReason.Modified); + await Assert.That(GenerateInfrastructure(driver3)).Contains("typeof(global::OtherTestLibrary.FirstHooks)"); } // The driver keeps the previous output instance whenever a rerun produces an equal model, so // whether the walk was skipped is only observable on the memo itself: it returns the same // instance on a hit and a new one after re-extracting. - [Fact] - public void Memo_SyntaxOnlyEdits_ReuseModel() + [Test] + public async Task Memo_SyntaxOnlyEdits_ReuseModel() { var memo = new InfrastructureGenerator.AssemblyInfoMemo(); var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default)); var model1 = memo.GetAssemblyInfo(compilation1); var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}")); - Xunit.Assert.Same(model1, memo.GetAssemblyInfo(compilation2)); + await Assert.That(memo.GetAssemblyInfo(compilation2)).IsSameReferenceAs(model1); var compilation3 = TestHelper.ReplaceMethodDeclaration(compilation1, "Test1", """ @@ -212,11 +212,11 @@ public void Test1() var x = 1; } """); - Xunit.Assert.Same(model1, memo.GetAssemblyInfo(compilation3)); + await Assert.That(memo.GetAssemblyInfo(compilation3)).IsSameReferenceAs(model1); } - [Fact] - public void Memo_ReferenceChange_ReExtracts() + [Test] + public async Task Memo_ReferenceChange_ReExtracts() { var memo = new InfrastructureGenerator.AssemblyInfoMemo(); var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default)); @@ -224,12 +224,12 @@ public void Memo_ReferenceChange_ReExtracts() var compilation2 = compilation1.AddReferences(CreateHookLibrary(compilation1, "SharedHooks").ToMetadataReference()); var model2 = memo.GetAssemblyInfo(compilation2); - Xunit.Assert.NotSame(model1, model2); - Xunit.Assert.NotEqual(model1, model2); + await Assert.That(model2).IsNotSameReferenceAs(model1); + await Assert.That(model2).IsNotEqualTo(model1); } - [Fact] - public void Memo_OptionsOrAssemblyNameChange_ReExtracts() + [Test] + public async Task Memo_OptionsOrAssemblyNameChange_ReExtracts() { var memo = new InfrastructureGenerator.AssemblyInfoMemo(); var baseCompilation = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default)); @@ -239,29 +239,29 @@ public void Memo_OptionsOrAssemblyNameChange_ReExtracts() // so a new options instance must not reuse the model extracted under the old one. var withComparer = baseCompilation.WithOptions( baseCompilation.Options.WithAssemblyIdentityComparer(DesktopAssemblyIdentityComparer.Default)); - Xunit.Assert.True(baseCompilation.ExternalReferences == withComparer.ExternalReferences); + await Assert.That(baseCompilation.ExternalReferences == withComparer.ExternalReferences).IsTrue(); var comparerModel = memo.GetAssemblyInfo(withComparer); - Xunit.Assert.NotSame(baseModel, comparerModel); - Xunit.Assert.Equal(baseModel, comparerModel); + await Assert.That(comparerModel).IsNotSameReferenceAs(baseModel); + await Assert.That(comparerModel).IsEqualTo(baseModel); var withImportOptions = baseCompilation.WithOptions( baseCompilation.Options.WithMetadataImportOptions(MetadataImportOptions.All)); var importModel = memo.GetAssemblyInfo(withImportOptions); - Xunit.Assert.NotSame(comparerModel, importModel); + await Assert.That(importModel).IsNotSameReferenceAs(comparerModel); // Syntax-only edits under the new options reuse the memo again. - Xunit.Assert.Same(importModel, memo.GetAssemblyInfo( - withImportOptions.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}")))); + await Assert.That(memo.GetAssemblyInfo( + withImportOptions.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}")))).IsSameReferenceAs(importModel); var renamed = withImportOptions.WithAssemblyName("Renamed"); - Xunit.Assert.True(withImportOptions.ExternalReferences == renamed.ExternalReferences); + await Assert.That(withImportOptions.ExternalReferences == renamed.ExternalReferences).IsTrue(); var renamedModel = memo.GetAssemblyInfo(renamed); - Xunit.Assert.NotSame(importModel, renamedModel); - Xunit.Assert.Equal("Renamed", renamedModel.AssemblyName); + await Assert.That(renamedModel).IsNotSameReferenceAs(importModel); + await Assert.That(renamedModel.AssemblyName).IsEqualTo("Renamed"); } - [Fact] - public void Memo_ScriptCompilation_IsNeverMemoized() + [Test] + public async Task Memo_ScriptCompilation_IsNeverMemoized() { var memo = new InfrastructureGenerator.AssemblyInfoMemo(); var baseCompilation = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default)); @@ -274,14 +274,14 @@ public void Memo_ScriptCompilation_IsNeverMemoized() baseCompilation.AssemblyName!, CSharpSyntaxTree.ParseText("var x = 1;", CSharpParseOptions.Default.WithKind(SourceCodeKind.Script)), baseCompilation.ExternalReferences); - Xunit.Assert.True(baseCompilation.ExternalReferences == script.ExternalReferences); + await Assert.That(baseCompilation.ExternalReferences == script.ExternalReferences).IsTrue(); var first = memo.GetAssemblyInfo(script); var second = memo.GetAssemblyInfo(script.ReplaceSyntaxTree( script.SyntaxTrees.Single(), CSharpSyntaxTree.ParseText("var y = 2;", CSharpParseOptions.Default.WithKind(SourceCodeKind.Script)))); - Xunit.Assert.NotSame(baseModel, first); - Xunit.Assert.NotSame(first, second); + await Assert.That(first).IsNotSameReferenceAs(baseModel); + await Assert.That(second).IsNotSameReferenceAs(first); } private static CSharpCompilation CreateWithTwoTypeLibrary() @@ -305,10 +305,10 @@ private static CSharpCompilation CreateHookLibrary(Compilation consumer, string consumer.References.Where(r => r is not CompilationReference), new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - private static void AssertRunReason(GeneratorDriver driver, IncrementalStepRunReason reason) + private static async Task AssertRunReason(GeneratorDriver driver, IncrementalStepRunReason reason) { var runResult = driver.GetRunResult().Results[0]; - TestHelper.AssertRunReason(runResult, InfrastructureGenerator.ExtractAssemblyInfoStep, reason, 0); + await TestHelper.AssertRunReason(runResult, InfrastructureGenerator.ExtractAssemblyInfoStep, reason, 0); } -} +} \ No newline at end of file diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/MethodAssertionGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/MethodAssertionGeneratorIncrementalTests.cs index 9ad9c1c10d6..3c6b07d0b56 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/MethodAssertionGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/MethodAssertionGeneratorIncrementalTests.cs @@ -27,28 +27,28 @@ public static bool IsNegative(this int value) } """; - [Fact] - public void AddUnrelatedMethodShouldNotRegenerate() + [Test] + public async Task AddUnrelatedMethodShouldNotRegenerate() { var syntaxTree = CSharpSyntaxTree.ParseText(DefaultAssertion, CSharpParseOptions.Default); var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + await AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}")); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached); } - [Fact] - public void AddNewTypeAssertionShouldRegenerate() + [Test] + public async Task AddNewTypeAssertionShouldRegenerate() { var syntaxTree = CSharpSyntaxTree.ParseText(DefaultAssertion, CSharpParseOptions.Default); var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + await AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText( """ @@ -64,18 +64,18 @@ public static bool IsPositive(this long value) } """)); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached, 0); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.New, 1); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached, 0); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.New, 1); } - [Fact] - public void AddNewSameTypeAssertionShouldRegenerate() + [Test] + public async Task AddNewSameTypeAssertionShouldRegenerate() { var syntaxTree = CSharpSyntaxTree.ParseText(DefaultAssertion, CSharpParseOptions.Default); var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + await AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); var compilation2 = TestHelper.ReplaceMethodDeclaration(compilation1, "IsNegative", """ @@ -87,18 +87,18 @@ public static bool IsNegative(this int value) """ ); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached, 0); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.New, 1); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached, 0); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.New, 1); } - [Fact] - public void ModifyMessageShouldRegenerate() + [Test] + public async Task ModifyMessageShouldRegenerate() { var syntaxTree = CSharpSyntaxTree.ParseText(DefaultAssertion, CSharpParseOptions.Default); var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + await AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); var compilation2 = TestHelper.ReplaceMethodDeclaration(compilation1, "IsPositive", """ @@ -110,11 +110,11 @@ public static bool IsPositive(this int value) """ ); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified); } - [Fact] - public void ModifyOneContainingTypeShouldOnlyRegenerateThatType() + [Test] + public async Task ModifyOneContainingTypeShouldOnlyRegenerateThatType() { var compilation1 = Fixture.CreateLibrary( DefaultAssertion, @@ -150,19 +150,19 @@ public static bool IsPositive(this long value) var driver2 = driver1.RunGenerators(compilation2); var runResult = driver2.GetRunResult().Results[0]; - TestHelper.AssertRunReason(runResult, MethodAssertionGenerator.BuildAssertionGroup, IncrementalStepRunReason.Unchanged, 0); - TestHelper.AssertRunReason(runResult, MethodAssertionGenerator.BuildAssertionGroup, IncrementalStepRunReason.Modified, 1); + await TestHelper.AssertRunReason(runResult, MethodAssertionGenerator.BuildAssertionGroup, IncrementalStepRunReason.Unchanged, 0); + await TestHelper.AssertRunReason(runResult, MethodAssertionGenerator.BuildAssertionGroup, IncrementalStepRunReason.Modified, 1); var sourceOutputReasons = runResult.TrackedOutputSteps .SelectMany(x => x.Value) .SelectMany(x => x.Outputs) .Select(x => x.Reason) .ToArray(); - Xunit.Assert.Single(sourceOutputReasons, IncrementalStepRunReason.Modified); + await Assert.That(sourceOutputReasons).HasSingleItem(x => x == IncrementalStepRunReason.Modified); } - [Fact] - public void UnrelatedEditWithDiagnosticShouldNotRegenerate() + [Test] + public async Task UnrelatedEditWithDiagnosticShouldNotRegenerate() { var brokenTree = CSharpSyntaxTree.ParseText( """ @@ -180,13 +180,13 @@ public static partial class BrokenAssertionExtensions brokenTree); var driver1 = TestHelper.GenerateTracked(compilation1); - Xunit.Assert.Contains(driver1.GetRunResult().Diagnostics, d => d.Id == "TUNITGEN001"); + await Assert.That(driver1.GetRunResult().Diagnostics).Contains(d => d.Id == "TUNITGEN001"); var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}")); var driver2 = driver1.RunGenerators(compilation2); var runResult = driver2.GetRunResult().Results[0]; - TestHelper.AssertAllRunReasons(runResult, MethodAssertionGenerator.BuildAssertionGroup, + await TestHelper.AssertAllRunReasons(runResult, MethodAssertionGenerator.BuildAssertionGroup, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); // Only the diagnostic reporter re-runs (it is paired with the compilation); the generated file stays cached. @@ -195,18 +195,18 @@ public static partial class BrokenAssertionExtensions .SelectMany(x => x.Outputs) .Select(x => x.Reason) .ToArray(); - Xunit.Assert.Single(sourceOutputReasons, reason => reason != IncrementalStepRunReason.Cached); + await Assert.That(sourceOutputReasons).HasSingleItem(reason => reason != IncrementalStepRunReason.Cached); - var diagnostic = Xunit.Assert.Single(runResult.Diagnostics, d => d.Id == "TUNITGEN001"); - Xunit.Assert.Equal(4, diagnostic.Location.GetLineSpan().StartLinePosition.Line); + var diagnostic = await Assert.That(runResult.Diagnostics).HasSingleItem(d => d.Id == "TUNITGEN001"); + await Assert.That(diagnostic.Location.GetLineSpan().StartLinePosition.Line).IsEqualTo(4); // The location must stay tied to the syntax tree so #pragma and per-file severities apply. - Xunit.Assert.True(diagnostic.Location.IsInSource); - Xunit.Assert.Same(brokenTree, diagnostic.Location.SourceTree); + await Assert.That(diagnostic.Location.IsInSource).IsTrue(); + await Assert.That(diagnostic.Location.SourceTree).IsSameReferenceAs(brokenTree); } - [Fact] - public void DiagnosticInPathlessTreeKeepsSourceTree() + [Test] + public async Task DiagnosticInPathlessTreeKeepsSourceTree() { // Trees parsed without a path all share the empty path, so the path alone cannot identify the tree. var brokenTree = CSharpSyntaxTree.ParseText( @@ -222,17 +222,18 @@ public static partial class BrokenAssertionExtensions var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultAssertion), brokenTree); var driver1 = TestHelper.GenerateTracked(compilation1); - Xunit.Assert.Same(brokenTree, Xunit.Assert.Single(driver1.GetRunResult().Diagnostics, d => d.Id == "TUNITGEN001").Location.SourceTree); + var firstDiagnostic = await Assert.That(driver1.GetRunResult().Diagnostics).HasSingleItem(d => d.Id == "TUNITGEN001"); + await Assert.That(firstDiagnostic.Location.SourceTree).IsSameReferenceAs(brokenTree); var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}")); var driver2 = driver1.RunGenerators(compilation2); - var diagnostic = Xunit.Assert.Single(driver2.GetRunResult().Diagnostics, d => d.Id == "TUNITGEN001"); - Xunit.Assert.True(diagnostic.Location.IsInSource); - Xunit.Assert.Same(brokenTree, diagnostic.Location.SourceTree); + var diagnostic = await Assert.That(driver2.GetRunResult().Diagnostics).HasSingleItem(d => d.Id == "TUNITGEN001"); + await Assert.That(diagnostic.Location.IsInSource).IsTrue(); + await Assert.That(diagnostic.Location.SourceTree).IsSameReferenceAs(brokenTree); } - private static void AssertRunReasons( + private static async Task AssertRunReasons( GeneratorDriver driver, IncrementalGeneratorRunReasons reasons, int outputIndex = 0 @@ -240,6 +241,6 @@ private static void AssertRunReasons( { var runResult = driver.GetRunResult().Results[0]; - TestHelper.AssertRunReason(runResult, MethodAssertionGenerator.BuildAssertion, reasons.BuildStep, outputIndex); + await TestHelper.AssertRunReason(runResult, MethodAssertionGenerator.BuildAssertion, reasons.BuildStep, outputIndex); } -} +} \ No newline at end of file diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/PropertyInjectionSourceGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/PropertyInjectionSourceGeneratorIncrementalTests.cs index c16757624e8..4bc2b37e90c 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/PropertyInjectionSourceGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/PropertyInjectionSourceGeneratorIncrementalTests.cs @@ -52,13 +52,13 @@ public void Test1() PropertyInjectionSourceGenerator.ConcreteGenericTypesStep, ]; - [Fact] - public void AddUnrelatedTypeShouldNotRegenerate() + [Test] + public async Task AddUnrelatedTypeShouldNotRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default)); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertAllOutputs(driver1, IncrementalStepRunReason.New); + await AssertAllOutputs(driver1, IncrementalStepRunReason.New); var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText( """ @@ -72,24 +72,24 @@ public class Unrelated } """)); var driver2 = driver1.RunGenerators(compilation2); - AssertAllOutputs(driver2, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); + await AssertAllOutputs(driver2, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); } - [Fact] - public void EditUnrelatedMemberShouldNotRegenerate() + [Test] + public async Task EditUnrelatedMemberShouldNotRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default)); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertAllOutputs(driver1, IncrementalStepRunReason.New); + await AssertAllOutputs(driver1, IncrementalStepRunReason.New); var compilation2 = TestHelper.ReplacePropertyDeclaration(compilation1, "Plain", "public long Plain { get; set; }"); var driver2 = driver1.RunGenerators(compilation2); - AssertAllOutputs(driver2, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); + await AssertAllOutputs(driver2, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); } - [Fact] - public void EditUnrelatedMemberShouldNotRerunConcreteGenericTypes() + [Test] + public async Task EditUnrelatedMemberShouldNotRerunConcreteGenericTypes() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default)); @@ -99,12 +99,12 @@ public void EditUnrelatedMemberShouldNotRerunConcreteGenericTypes() // flattened step from re-running rather than merely re-producing equal values. var compilation2 = TestHelper.ReplacePropertyDeclaration(compilation1, "Plain", "public long Plain { get; set; }"); var driver2 = driver1.RunGenerators(compilation2); - TestHelper.AssertAllRunReasons(driver2.GetRunResult().Results[0], + await TestHelper.AssertAllRunReasons(driver2.GetRunResult().Results[0], PropertyInjectionSourceGenerator.ConcreteGenericTypesStep, IncrementalStepRunReason.Cached); } - [Fact] - public void ModifyDataSourcePropertyShouldRegenerate() + [Test] + public async Task ModifyDataSourcePropertyShouldRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default)); @@ -116,11 +116,11 @@ public void ModifyDataSourcePropertyShouldRegenerate() public Fixture? Injected { get; set; } """); var driver2 = driver1.RunGenerators(compilation2); - AssertAnyOutput(driver2, PropertyInjectionSourceGenerator.PropertyDataSourcesStep, IncrementalStepRunReason.Modified); + await AssertAnyOutput(driver2, PropertyInjectionSourceGenerator.PropertyDataSourcesStep, IncrementalStepRunReason.Modified); } - [Fact] - public void AddInitializerPropertyShouldRegenerate() + [Test] + public async Task AddInitializerPropertyShouldRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default)); @@ -137,11 +137,11 @@ public class Fixture : IAsyncInitializer } """); var driver2 = driver1.RunGenerators(compilation2); - AssertAnyOutput(driver2, PropertyInjectionSourceGenerator.AsyncInitializersStep, IncrementalStepRunReason.Modified); + await AssertAnyOutput(driver2, PropertyInjectionSourceGenerator.AsyncInitializersStep, IncrementalStepRunReason.Modified); } - [Fact] - public void AddConcreteGenericInstantiationShouldRegenerate() + [Test] + public async Task AddConcreteGenericInstantiationShouldRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default)); @@ -163,37 +163,37 @@ public void Test1() } """)); var driver2 = driver1.RunGenerators(compilation2); - AssertAnyOutput(driver2, PropertyInjectionSourceGenerator.ConcreteGenericTypesStep, IncrementalStepRunReason.New); + await AssertAnyOutput(driver2, PropertyInjectionSourceGenerator.ConcreteGenericTypesStep, IncrementalStepRunReason.New); var concreteTypes2 = driver2.GetRunResult().Results[0].TrackedSteps[PropertyInjectionSourceGenerator.ConcreteGenericTypesStep] .SelectMany(x => x.Outputs) .Count(); - Xunit.Assert.Equal(concreteTypes1 + 1, concreteTypes2); + await Assert.That(concreteTypes2).IsEqualTo(concreteTypes1 + 1); } - private static void AssertAllOutputs(GeneratorDriver driver, params IncrementalStepRunReason[] allowedReasons) + private static async Task AssertAllOutputs(GeneratorDriver driver, params IncrementalStepRunReason[] allowedReasons) { var runResult = driver.GetRunResult().Results[0]; foreach (var step in Steps) { var outputs = runResult.TrackedSteps[step].SelectMany(x => x.Outputs).ToArray(); - Xunit.Assert.NotEmpty(outputs); + await Assert.That(outputs).IsNotEmpty(); foreach (var output in outputs) { - Xunit.Assert.True(allowedReasons.Contains(output.Reason), - $"Step {step} produced an output with reason {output.Reason}; expected one of {string.Join(", ", allowedReasons)}."); + await Assert.That(allowedReasons).Contains(output.Reason) + .Because($"step {step} produced an output with reason {output.Reason}"); } } } - private static void AssertAnyOutput(GeneratorDriver driver, string step, IncrementalStepRunReason expectedReason) + private static async Task AssertAnyOutput(GeneratorDriver driver, string step, IncrementalStepRunReason expectedReason) { var runResult = driver.GetRunResult().Results[0]; var reasons = runResult.TrackedSteps[step].SelectMany(x => x.Outputs).Select(x => x.Reason).ToArray(); - Xunit.Assert.True(reasons.Contains(expectedReason), - $"Step {step} had no output with reason {expectedReason}; actual reasons: {string.Join(", ", reasons)}."); + await Assert.That(reasons).Contains(expectedReason) + .Because($"step {step} should have an output with reason {expectedReason}"); } -} +} \ No newline at end of file diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/ShouldExtensionGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/ShouldExtensionGeneratorIncrementalTests.cs index 07c99c61b78..34fe0aa3797 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/ShouldExtensionGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/ShouldExtensionGeneratorIncrementalTests.cs @@ -57,20 +57,20 @@ public void Method() { } } """; - [Fact] - public void GeneratesFromLocalContainersAndWrappers() + [Test] + public async Task GeneratesFromLocalContainersAndWrappers() { var compilation = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource)); var driver = TestHelper.GenerateTracked(compilation); var sources = driver.GetRunResult().Results[0].GeneratedSources; - Xunit.Assert.Contains("BeFoo", sources.Single(x => x.HintName == "ShouldFooExtensions.g.cs").SourceText.ToString()); - Xunit.Assert.Contains("BeFooToo", sources.Single(x => x.HintName == "FooWrapper.Generated.g.cs").SourceText.ToString()); + await Assert.That(sources.Single(x => x.HintName == "ShouldFooExtensions.g.cs").SourceText.ToString()).Contains("BeFoo"); + await Assert.That(sources.Single(x => x.HintName == "FooWrapper.Generated.g.cs").SourceText.ToString()).Contains("BeFooToo"); } - [Fact] - public void AddUnrelatedTypeShouldNotRegenerate() + [Test] + public async Task AddUnrelatedTypeShouldNotRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource)); var driver1 = TestHelper.GenerateTracked(compilation1); @@ -78,11 +78,11 @@ public void AddUnrelatedTypeShouldNotRegenerate() var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}")); var driver2 = driver1.RunGenerators(compilation2); - AssertUnchanged(driver2.GetRunResult().Results[0]); + await AssertUnchanged(driver2.GetRunResult().Results[0]); } - [Fact] - public void EditUnrelatedTypeShouldNotRegenerate() + [Test] + public async Task EditUnrelatedTypeShouldNotRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource)); var driver1 = TestHelper.GenerateTracked(compilation1); @@ -90,11 +90,11 @@ public void EditUnrelatedTypeShouldNotRegenerate() var compilation2 = TestHelper.ReplaceMethodDeclaration(compilation1, "Method", "public void Method() { var x = 1; }"); var driver2 = driver1.RunGenerators(compilation2); - AssertUnchanged(driver2.GetRunResult().Results[0]); + await AssertUnchanged(driver2.GetRunResult().Results[0]); } - [Fact] - public void ModifyExtensionMethodShouldRegenerate() + [Test] + public async Task ModifyExtensionMethodShouldRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource)); var driver1 = TestHelper.GenerateTracked(compilation1); @@ -107,15 +107,15 @@ public static FooAssertion IsFooish(this IAssertionSource source, int expec var driver2 = driver1.RunGenerators(compilation2); var runResult = driver2.GetRunResult().Results[0]; - TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalWrappersStep, + await TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalWrappersStep, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); - TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalDeclarationsStep, IncrementalStepRunReason.Modified); - TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.PayloadStep, IncrementalStepRunReason.Modified); - Xunit.Assert.Contains("BeFooish", runResult.GeneratedSources.Single(x => x.HintName == "ShouldFooExtensions.g.cs").SourceText.ToString()); + await TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalDeclarationsStep, IncrementalStepRunReason.Modified); + await TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.PayloadStep, IncrementalStepRunReason.Modified); + await Assert.That(runResult.GeneratedSources.Single(x => x.HintName == "ShouldFooExtensions.g.cs").SourceText.ToString()).Contains("BeFooish"); } - [Fact] - public void ModifyWrapperShouldRegenerate() + [Test] + public async Task ModifyWrapperShouldRegenerate() { var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource)); var driver1 = TestHelper.GenerateTracked(compilation1); @@ -131,18 +131,18 @@ public void BeFooToo(int expected) { } var driver2 = driver1.RunGenerators(compilation2); var runResult = driver2.GetRunResult().Results[0]; - TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalContainersStep, + await TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalContainersStep, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); - TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.PayloadStep, IncrementalStepRunReason.Modified); - Xunit.Assert.DoesNotContain(runResult.GeneratedSources, x => x.HintName == "FooWrapper.Generated.g.cs"); + await TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.PayloadStep, IncrementalStepRunReason.Modified); + await Assert.That(runResult.GeneratedSources).DoesNotContain(x => x.HintName == "FooWrapper.Generated.g.cs"); } - [Fact] - public void ModifyWrappedAssertionInOtherFileShouldRegenerateWrapper() + [Test] + public async Task ModifyWrappedAssertionInOtherFileShouldRegenerateWrapper() { var compilation1 = Fixture.CreateLibrary(SplitAssertionTree(SplitAssertion), SplitDeclarationsTree()); var driver1 = TestHelper.GenerateTracked(compilation1); - Xunit.Assert.Contains("BeFooToo", GetSource(driver1, "FooWrapper.Generated.g.cs")); + await Assert.That(GetSource(driver1, "FooWrapper.Generated.g.cs")).Contains("BeFooToo"); var compilation2 = compilation1.ReplaceSyntaxTree( compilation1.SyntaxTrees.First(), @@ -150,20 +150,20 @@ public void ModifyWrappedAssertionInOtherFileShouldRegenerateWrapper() var driver2 = driver1.RunGenerators(compilation2); var runResult = driver2.GetRunResult().Results[0]; - TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalWrappersStep, + await TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalWrappersStep, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); - TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalDeclarationsStep, IncrementalStepRunReason.Modified); + await TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalDeclarationsStep, IncrementalStepRunReason.Modified); var wrapperSource = GetSource(driver2, "FooWrapper.Generated.g.cs"); - Xunit.Assert.Contains("BeFooThree", wrapperSource); - Xunit.Assert.DoesNotContain("BeFooToo", wrapperSource); + await Assert.That(wrapperSource).Contains("BeFooThree"); + await Assert.That(wrapperSource).DoesNotContain("BeFooToo"); } - [Fact] - public void ModifyReturnedAssertionInOtherFileShouldRegenerateContainer() + [Test] + public async Task ModifyReturnedAssertionInOtherFileShouldRegenerateContainer() { var compilation1 = Fixture.CreateLibrary(SplitAssertionTree(SplitAssertion), SplitDeclarationsTree()); var driver1 = TestHelper.GenerateTracked(compilation1); - Xunit.Assert.Contains("BeFoo", GetSource(driver1, "ShouldFooExtensions.g.cs")); + await Assert.That(GetSource(driver1, "ShouldFooExtensions.g.cs")).Contains("BeFoo"); // The extension no longer maps onto a constructor, so its Should counterpart must disappear. var compilation2 = compilation1.ReplaceSyntaxTree( @@ -174,14 +174,14 @@ public void ModifyReturnedAssertionInOtherFileShouldRegenerateContainer() var driver2 = driver1.RunGenerators(compilation2); var runResult = driver2.GetRunResult().Results[0]; - TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalContainersStep, + await TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalContainersStep, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); - TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalDeclarationsStep, IncrementalStepRunReason.Modified); - Xunit.Assert.DoesNotContain(runResult.GeneratedSources, x => x.HintName == "ShouldFooExtensions.g.cs"); + await TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalDeclarationsStep, IncrementalStepRunReason.Modified); + await Assert.That(runResult.GeneratedSources).DoesNotContain(x => x.HintName == "ShouldFooExtensions.g.cs"); } - [Fact] - public void PartialContainerSplitAcrossFilesGeneratesAllMethodsOnce() + [Test] + public async Task PartialContainerSplitAcrossFilesGeneratesAllMethodsOnce() { var compilation1 = Fixture.CreateLibrary( SplitAssertionTree(SplitAssertion), @@ -191,8 +191,8 @@ public void PartialContainerSplitAcrossFilesGeneratesAllMethodsOnce() var driver1 = TestHelper.GenerateTracked(compilation1); var source = GetSource(driver1, "ShouldFooExtensions.g.cs"); - Xunit.Assert.Single(System.Text.RegularExpressions.Regex.Matches(source, @"> BeFoo\(")); - Xunit.Assert.Single(System.Text.RegularExpressions.Regex.Matches(source, @"> BeBar\(")); + await Assert.That(System.Text.RegularExpressions.Regex.Matches(source, @"> BeFoo\(")).HasSingleItem(); + await Assert.That(System.Text.RegularExpressions.Regex.Matches(source, @"> BeBar\(")).HasSingleItem(); // Editing the part without extension methods changes nothing the generator reads. var compilation2 = compilation1.ReplaceSyntaxTree(compilation1.SyntaxTrees.Last(), HelperPartTree("2")); @@ -200,17 +200,17 @@ public void PartialContainerSplitAcrossFilesGeneratesAllMethodsOnce() // No [ShouldGeneratePartial] here, so the wrappers step never runs and is not asserted. var runResult = driver2.GetRunResult().Results[0]; - TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalContainersStep, + await TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalContainersStep, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); - TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalDeclarationsStep, + await TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalDeclarationsStep, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); - TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.PayloadStep, + await TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.PayloadStep, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); - TestHelper.AssertSourceOutputsCached(runResult); + await TestHelper.AssertSourceOutputsCached(runResult); } - [Fact] - public void GenericAndNestedWrappersResolveByMetadataName() + [Test] + public async Task GenericAndNestedWrappersResolveByMetadataName() { // The syntax step hands the resolver hand-built metadata names; a wrong name makes // GetTypeByMetadataName return null and the wrapper silently disappears. Cover a @@ -242,10 +242,10 @@ public partial class NestedWrapper var driver = TestHelper.GenerateTracked(compilation); - Xunit.Assert.Contains("BeFooToo", GetSource(driver, "GenericWrapper.Generated.g.cs")); + await Assert.That(GetSource(driver, "GenericWrapper.Generated.g.cs")).Contains("BeFooToo"); var nestedSource = GetSource(driver, "NestedWrapper.Generated.g.cs"); - Xunit.Assert.Contains("BeFooToo", nestedSource); - Xunit.Assert.Contains("partial class Outer", nestedSource); + await Assert.That(nestedSource).Contains("BeFooToo"); + await Assert.That(nestedSource).Contains("partial class Outer"); } private static SyntaxTree HelperPartTree(string value) => @@ -338,18 +338,18 @@ public partial class FooWrapper private static string GetSource(GeneratorDriver driver, string hintName) => driver.GetRunResult().Results[0].GeneratedSources.Single(x => x.HintName == hintName).SourceText.ToString(); - private static void AssertUnchanged(GeneratorRunResult runResult) + private static async Task AssertUnchanged(GeneratorRunResult runResult) { - TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalContainersStep, + await TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalContainersStep, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); - TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalWrappersStep, + await TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalWrappersStep, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); - TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalDeclarationsStep, + await TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.LocalDeclarationsStep, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); - TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.CompilationDataStep, + await TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.CompilationDataStep, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); - TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.PayloadStep, + await TestHelper.AssertAllRunReasons(runResult, ShouldExtensionGenerator.PayloadStep, IncrementalStepRunReason.Cached, IncrementalStepRunReason.Unchanged); - TestHelper.AssertSourceOutputsCached(runResult); + await TestHelper.AssertSourceOutputsCached(runResult); } -} +} \ No newline at end of file diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/StaticPropertyInitializationGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/StaticPropertyInitializationGeneratorIncrementalTests.cs index b50c7b409c5..a040db0d6a4 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/StaticPropertyInitializationGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/StaticPropertyInitializationGeneratorIncrementalTests.cs @@ -28,29 +28,29 @@ public class StaticPropertyDataSourceTests } """; - [Fact] - public void AddUnrelatedMethodShouldNotRegenerate() + [Test] + public async Task AddUnrelatedMethodShouldNotRegenerate() { var syntaxTree = CSharpSyntaxTree.ParseText(DefaultProperties, CSharpParseOptions.Default); var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + await AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}")); var driver2 = driver1.RunGenerators(compilation2); - AssertRunParseLength(driver2,2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached); + await AssertRunParseLength(driver2,2); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached); } - [Fact] - public void AddUnrelatedClassWithBaseListShouldNotRerunParse() + [Test] + public async Task AddUnrelatedClassWithBaseListShouldNotRerunParse() { var syntaxTree = CSharpSyntaxTree.ParseText(DefaultProperties, CSharpParseOptions.Default); var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + await AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); // A class with a base list is a syntactic candidate, but its chain has no static // data-source property, so it must not reach the collected input of the parse step. @@ -62,18 +62,18 @@ public void Dispose() { } } """)); var driver2 = driver1.RunGenerators(compilation2); - AssertRunParseLength(driver2, 2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached); + await AssertRunParseLength(driver2, 2); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached); } - [Fact] - public void AddClassWithValidPropertyShouldRegenerate() + [Test] + public async Task AddClassWithValidPropertyShouldRegenerate() { var syntaxTree = CSharpSyntaxTree.ParseText(DefaultProperties, CSharpParseOptions.Default); var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + await AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText( """ @@ -88,18 +88,18 @@ public class ExtraStaticProperty } """)); var driver2 = driver1.RunGenerators(compilation2); - AssertRunParseLength(driver2,3); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedSource); + await AssertRunParseLength(driver2,3); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedSource); } - [Fact] - public void ModifyPropertyAttributeShouldRegenerate() + [Test] + public async Task ModifyPropertyAttributeShouldRegenerate() { var syntaxTree = CSharpSyntaxTree.ParseText(DefaultProperties, CSharpParseOptions.Default); var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + await AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText( """ @@ -114,13 +114,13 @@ public class ExtraStaticProperty } """)); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified); + await AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void HiddenBaseStaticPropertyIsInitializedRegardlessOfDeclarationOrder(bool derivedFirst) + [Test] + [Arguments(true)] + [Arguments(false)] + public async Task HiddenBaseStaticPropertyIsInitializedRegardlessOfDeclarationOrder(bool derivedFirst) { const string baseClass = """ @@ -153,20 +153,20 @@ public class HidingDerived : HidingBase var driver = TestHelper.GenerateTracked(compilation); // Both HidingDerived.Value and the hidden HidingBase.Value need initializing - AssertRunParseLength(driver, 2); + await AssertRunParseLength(driver, 2); } - private static void AssertRunReasons( + private static async Task AssertRunReasons( GeneratorDriver driver, IncrementalGeneratorRunReasons reasons, int outputIndex = 0 ) { var runResult = driver.GetRunResult().Results[0]; - TestHelper.AssertRunReason(runResult, StaticPropertyInitializationGenerator.ParseStaticProperties, reasons.BuildStep, outputIndex); + await TestHelper.AssertRunReason(runResult, StaticPropertyInitializationGenerator.ParseStaticProperties, reasons.BuildStep, outputIndex); } - private static void AssertRunParseLength( + private static async Task AssertRunParseLength( GeneratorDriver driver, int staticPropertyModelCount ) @@ -174,6 +174,6 @@ int staticPropertyModelCount var runResult = driver.GetRunResult().Results[0]; var runValue = runResult.TrackedSteps[StaticPropertyInitializationGenerator.ParseStaticProperties][0].Outputs[0].Value; var runState = (EquatableArray)runValue; - Xunit.Assert.Equal(staticPropertyModelCount, runState.Length); + await Assert.That(runState.Length).IsEqualTo(staticPropertyModelCount); } -} +} \ No newline at end of file diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/TUnit.SourceGenerator.IncrementalTests.csproj b/tests/TUnit.SourceGenerator.IncrementalTests/TUnit.SourceGenerator.IncrementalTests.csproj index 4f48a6529bd..aab42375c2a 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/TUnit.SourceGenerator.IncrementalTests.csproj +++ b/tests/TUnit.SourceGenerator.IncrementalTests/TUnit.SourceGenerator.IncrementalTests.csproj @@ -1,38 +1,30 @@  + + - net10.0 + net10.0 enable enable false - - Exe - true - false - true - $(NoWarn);MSB3277;xUnit1051 + $(NoWarn);MSB3277 TUnit.Assertions.SourceGenerator.IncrementalTests - - - - - - - + + + diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/TestHelper.cs b/tests/TUnit.SourceGenerator.IncrementalTests/TestHelper.cs index d3b71da0310..b8fea110b12 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/TestHelper.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/TestHelper.cs @@ -110,7 +110,7 @@ string newDeclaration /// Asserts every output of the tracked step has one of the /// , and that the step produced at least one output. /// - public static void AssertAllRunReasons( + public static async Task AssertAllRunReasons( GeneratorRunResult runResult, string stepName, params IncrementalStepRunReason[] allowedReasons @@ -120,17 +120,17 @@ params IncrementalStepRunReason[] allowedReasons .SelectMany(x => x.Outputs) .ToArray(); - Xunit.Assert.NotEmpty(outputs); + await Assert.That(outputs).IsNotEmpty(); foreach (var output in outputs) { - Xunit.Assert.Contains(output.Reason, allowedReasons); + await Assert.That(allowedReasons).Contains(output.Reason); } } /// /// Asserts at least one output of the tracked step has . /// - public static void AssertAnyRunReason( + public static async Task AssertAnyRunReason( GeneratorRunResult runResult, string stepName, IncrementalStepRunReason expectedReason @@ -141,27 +141,27 @@ IncrementalStepRunReason expectedReason .Select(x => x.Reason) .ToArray(); - Xunit.Assert.Contains(expectedReason, reasons); + await Assert.That(reasons).Contains(expectedReason); } /// /// Asserts no source output was re-run, i.e. nothing was regenerated. /// - public static void AssertSourceOutputsCached(GeneratorRunResult runResult) + public static async Task AssertSourceOutputsCached(GeneratorRunResult runResult) { var outputs = runResult.TrackedOutputSteps .SelectMany(x => x.Value) .SelectMany(x => x.Outputs) .ToArray(); - Xunit.Assert.NotEmpty(outputs); + await Assert.That(outputs).IsNotEmpty(); foreach (var output in outputs) { - Xunit.Assert.Equal(IncrementalStepRunReason.Cached, output.Reason); + await Assert.That(output.Reason).IsEqualTo(IncrementalStepRunReason.Cached); } } - public static void AssertRunReason( + public static async Task AssertRunReason( GeneratorRunResult runResult, string stepName, IncrementalStepRunReason expectedStepReason, From 0a2261b03a806dcf1db438a53c1141f320acdd8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sat, 17 Oct 2026 20:24:56 +0000 Subject: [PATCH 11/49] ci: run TUnit.SourceGenerator.IncrementalTests in the pipeline The project's 64 generator-caching tests were in TUnit.slnx only, so CI neither built nor ran them. Add it to TUnit.CI.slnx and a pipeline module that runs it like the other test projects (Release, net10.0). --- TUnit.CI.slnx | 1 + ...unSourceGeneratorIncrementalTestsModule.cs | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 tools/TUnit.Pipeline/Modules/RunSourceGeneratorIncrementalTestsModule.cs diff --git a/TUnit.CI.slnx b/TUnit.CI.slnx index dc649a60890..10f93a39731 100644 --- a/TUnit.CI.slnx +++ b/TUnit.CI.slnx @@ -87,6 +87,7 @@ + diff --git a/tools/TUnit.Pipeline/Modules/RunSourceGeneratorIncrementalTestsModule.cs b/tools/TUnit.Pipeline/Modules/RunSourceGeneratorIncrementalTestsModule.cs new file mode 100644 index 00000000000..8be00ef76b1 --- /dev/null +++ b/tools/TUnit.Pipeline/Modules/RunSourceGeneratorIncrementalTestsModule.cs @@ -0,0 +1,33 @@ +using ModularPipelines.Context; +using ModularPipelines.DotNet.Options; +using ModularPipelines.Extensions; +using ModularPipelines.Git.Extensions; +using ModularPipelines.Options; +using TUnit.Pipeline.Modules.Abstract; + +namespace TUnit.Pipeline.Modules; + +public class RunSourceGeneratorIncrementalTestsModule : TestBaseModule +{ + protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken) + { + var project = context.Git().RootDirectory.FindFile(x => x.Name == "TUnit.SourceGenerator.IncrementalTests.csproj").AssertExists(); + + return Task.FromResult<(DotNetRunOptions, CommandExecutionOptions?)>(( + new DotNetRunOptions + { + NoBuild = true, + Configuration = "Release", + Framework = framework, + }, + new CommandExecutionOptions + { + WorkingDirectory = project.Folder!.Path, + EnvironmentVariables = new Dictionary + { + ["DISABLE_GITHUB_REPORTER"] = "true", + } + } + )); + } +} From 0d30ca084672ca361f11b27e69feaf2372d0acbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 06:22:22 +0000 Subject: [PATCH 12/49] fix(assertions): say "to" in [AssertionFrom] custom expectation messages AssertionMethodGenerator rendered a custom ExpectationMessage as `{(negated ? "not " : "")} {message}`, so every [AssertionFrom] assertion with a custom message (e.g. string.IsNullOrEmpty, char IsUpper, Task IsCompleted; their messages omit the leading "to" by convention) failed with "Expected be null or empty" (double space, no "to"), and negated ones with "Expected not be ...". Emit `to {(negated ? "not " : "")}{message}` instead, matching the "to not ..." wording used elsewhere. Snapshots updated (only that line changes); Net4_7 snapshots received the identical substitution as they cannot be regenerated off Windows. Adds a message regression test. --- .../Generators/AssertionMethodGenerator.cs | 4 +- ...AssemblyAssertions.DotNet10_0.verified.txt | 4 +- ...sAssemblyAssertions.DotNet8_0.verified.txt | 4 +- ...sAssemblyAssertions.DotNet9_0.verified.txt | 4 +- ...atesAssemblyAssertions.Net4_7.verified.txt | 4 +- ...ionTokenAssertions.DotNet10_0.verified.txt | 4 +- ...tionTokenAssertions.DotNet8_0.verified.txt | 4 +- ...tionTokenAssertions.DotNet9_0.verified.txt | 4 +- ...llationTokenAssertions.Net4_7.verified.txt | 4 +- ...atesCharAssertions.DotNet10_0.verified.txt | 28 +++++------ ...ratesCharAssertions.DotNet8_0.verified.txt | 28 +++++------ ...ratesCharAssertions.DotNet9_0.verified.txt | 28 +++++------ ...eneratesCharAssertions.Net4_7.verified.txt | 28 +++++------ ...tureInfoAssertions.DotNet10_0.verified.txt | 4 +- ...ltureInfoAssertions.DotNet8_0.verified.txt | 4 +- ...ltureInfoAssertions.DotNet9_0.verified.txt | 4 +- ...sCultureInfoAssertions.Net4_7.verified.txt | 4 +- ...DateTimeAssertions.DotNet10_0.verified.txt | 2 +- ...sDateTimeAssertions.DotNet8_0.verified.txt | 2 +- ...sDateTimeAssertions.DotNet9_0.verified.txt | 2 +- ...atesDateTimeAssertions.Net4_7.verified.txt | 2 +- ...toryInfoAssertions.DotNet10_0.verified.txt | 2 +- ...ctoryInfoAssertions.DotNet8_0.verified.txt | 2 +- ...ctoryInfoAssertions.DotNet9_0.verified.txt | 2 +- ...irectoryInfoAssertions.Net4_7.verified.txt | 2 +- ...EncodingAssertions.DotNet10_0.verified.txt | 2 +- ...sEncodingAssertions.DotNet8_0.verified.txt | 2 +- ...sEncodingAssertions.DotNet9_0.verified.txt | 2 +- ...atesEncodingAssertions.Net4_7.verified.txt | 2 +- ...FileInfoAssertions.DotNet10_0.verified.txt | 4 +- ...sFileInfoAssertions.DotNet8_0.verified.txt | 4 +- ...sFileInfoAssertions.DotNet9_0.verified.txt | 4 +- ...atesFileInfoAssertions.Net4_7.verified.txt | 4 +- ...sProcessAssertions.DotNet10_0.verified.txt | 6 +-- ...esProcessAssertions.DotNet8_0.verified.txt | 6 +-- ...esProcessAssertions.DotNet9_0.verified.txt | 6 +-- ...ratesProcessAssertions.Net4_7.verified.txt | 6 +-- ...esStreamAssertions.DotNet10_0.verified.txt | 8 ++-- ...tesStreamAssertions.DotNet8_0.verified.txt | 8 ++-- ...tesStreamAssertions.DotNet9_0.verified.txt | 8 ++-- ...eratesStreamAssertions.Net4_7.verified.txt | 8 ++-- ...atesTaskAssertions.DotNet10_0.verified.txt | 6 +-- ...ratesTaskAssertions.DotNet8_0.verified.txt | 6 +-- ...ratesTaskAssertions.DotNet9_0.verified.txt | 6 +-- ...eneratesTaskAssertions.Net4_7.verified.txt | 6 +-- ...esThreadAssertions.DotNet10_0.verified.txt | 6 +-- ...tesThreadAssertions.DotNet8_0.verified.txt | 6 +-- ...tesThreadAssertions.DotNet9_0.verified.txt | 6 +-- ...eratesThreadAssertions.Net4_7.verified.txt | 6 +-- ...ZoneInfoAssertions.DotNet10_0.verified.txt | 2 +- ...eZoneInfoAssertions.DotNet8_0.verified.txt | 2 +- ...eZoneInfoAssertions.DotNet9_0.verified.txt | 2 +- ...TimeZoneInfoAssertions.Net4_7.verified.txt | 2 +- ...atesTypeAssertions.DotNet10_0.verified.txt | 46 +++++++++---------- ...ratesTypeAssertions.DotNet8_0.verified.txt | 46 +++++++++---------- ...ratesTypeAssertions.DotNet9_0.verified.txt | 46 +++++++++---------- ...eneratesTypeAssertions.Net4_7.verified.txt | 46 +++++++++---------- ...ratesUriAssertions.DotNet10_0.verified.txt | 12 ++--- ...eratesUriAssertions.DotNet8_0.verified.txt | 12 ++--- ...eratesUriAssertions.DotNet9_0.verified.txt | 12 ++--- ...GeneratesUriAssertions.Net4_7.verified.txt | 12 ++--- ...eferenceAssertions.DotNet10_0.verified.txt | 4 +- ...ReferenceAssertions.DotNet8_0.verified.txt | 4 +- ...ReferenceAssertions.DotNet9_0.verified.txt | 4 +- ...eakReferenceAssertions.Net4_7.verified.txt | 4 +- .../StringNullabilityAssertionTests.cs | 16 +++++++ 66 files changed, 298 insertions(+), 282 deletions(-) diff --git a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs index 3c0ce160e03..b449c5beac7 100644 --- a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs +++ b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs @@ -978,12 +978,12 @@ private static void GenerateAssertConditionClassForMethod(StringBuilder sourceBu if (parameters.Any()) { // Use interpolated string for parameter substitution - sourceBuilder.AppendLine($" return $\"{{(_negated ? \"not \" : \"\")}} {expectation}\";"); + sourceBuilder.AppendLine($" return $\"to {{(_negated ? \"not \" : \"\")}}{expectation}\";"); } else { // No parameters, just return the literal string with negation support - sourceBuilder.AppendLine($" return $\"{{(_negated ? \"not \" : \"\")}} {expectation}\";"); + sourceBuilder.AppendLine($" return $\"to {{(_negated ? \"not \" : \"\")}}{expectation}\";"); } } else diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/AssemblyAssertionGeneratorTests.GeneratesAssemblyAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/AssemblyAssertionGeneratorTests.GeneratesAssemblyAssertions.DotNet10_0.verified.txt index 4c5d4d312fb..9fd25097b08 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/AssemblyAssertionGeneratorTests.GeneratesAssemblyAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/AssemblyAssertionGeneratorTests.GeneratesAssemblyAssertions.DotNet10_0.verified.txt @@ -43,7 +43,7 @@ public class AssemblyIsDynamicAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be dynamic"; + return $"to {(_negated ? "not " : "")}be dynamic"; } } @@ -79,7 +79,7 @@ public class AssemblyIsFullyTrustedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be dynamic"; + return $"to {(_negated ? "not " : "")}be dynamic"; } } @@ -79,7 +79,7 @@ public class AssemblyIsFullyTrustedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be dynamic"; + return $"to {(_negated ? "not " : "")}be dynamic"; } } @@ -79,7 +79,7 @@ public class AssemblyIsFullyTrustedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be dynamic"; + return $"to {(_negated ? "not " : "")}be dynamic"; } } @@ -79,7 +79,7 @@ public class AssemblyIsFullyTrustedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a letter"; + return $"to {(_negated ? "not " : "")}be a letter"; } } @@ -69,7 +69,7 @@ public class CharIsDigitWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a digit"; + return $"to {(_negated ? "not " : "")}be a digit"; } } @@ -100,7 +100,7 @@ public class CharIsWhiteSpaceWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be whitespace"; + return $"to {(_negated ? "not " : "")}be whitespace"; } } @@ -131,7 +131,7 @@ public class CharIsUpperWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be uppercase"; + return $"to {(_negated ? "not " : "")}be uppercase"; } } @@ -162,7 +162,7 @@ public class CharIsLowerWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be lowercase"; + return $"to {(_negated ? "not " : "")}be lowercase"; } } @@ -193,7 +193,7 @@ public class CharIsControlWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a control character"; + return $"to {(_negated ? "not " : "")}be a control character"; } } @@ -224,7 +224,7 @@ public class CharIsPunctuationWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be punctuation"; + return $"to {(_negated ? "not " : "")}be punctuation"; } } @@ -255,7 +255,7 @@ public class CharIsSymbolWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a symbol"; + return $"to {(_negated ? "not " : "")}be a symbol"; } } @@ -286,7 +286,7 @@ public class CharIsNumberWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a number"; + return $"to {(_negated ? "not " : "")}be a number"; } } @@ -317,7 +317,7 @@ public class CharIsSeparatorWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a separator"; + return $"to {(_negated ? "not " : "")}be a separator"; } } @@ -348,7 +348,7 @@ public class CharIsSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a surrogate"; + return $"to {(_negated ? "not " : "")}be a surrogate"; } } @@ -379,7 +379,7 @@ public class CharIsHighSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a high surrogate"; + return $"to {(_negated ? "not " : "")}be a high surrogate"; } } @@ -410,7 +410,7 @@ public class CharIsLowSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a low surrogate"; + return $"to {(_negated ? "not " : "")}be a low surrogate"; } } @@ -441,7 +441,7 @@ public class CharIsLetterOrDigitWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a letter or digit"; + return $"to {(_negated ? "not " : "")}be a letter or digit"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.DotNet8_0.verified.txt index 4d320d531d6..3e7877ad611 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.DotNet8_0.verified.txt @@ -38,7 +38,7 @@ public class CharIsLetterWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a letter"; + return $"to {(_negated ? "not " : "")}be a letter"; } } @@ -69,7 +69,7 @@ public class CharIsDigitWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a digit"; + return $"to {(_negated ? "not " : "")}be a digit"; } } @@ -100,7 +100,7 @@ public class CharIsWhiteSpaceWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be whitespace"; + return $"to {(_negated ? "not " : "")}be whitespace"; } } @@ -131,7 +131,7 @@ public class CharIsUpperWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be uppercase"; + return $"to {(_negated ? "not " : "")}be uppercase"; } } @@ -162,7 +162,7 @@ public class CharIsLowerWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be lowercase"; + return $"to {(_negated ? "not " : "")}be lowercase"; } } @@ -193,7 +193,7 @@ public class CharIsControlWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a control character"; + return $"to {(_negated ? "not " : "")}be a control character"; } } @@ -224,7 +224,7 @@ public class CharIsPunctuationWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be punctuation"; + return $"to {(_negated ? "not " : "")}be punctuation"; } } @@ -255,7 +255,7 @@ public class CharIsSymbolWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a symbol"; + return $"to {(_negated ? "not " : "")}be a symbol"; } } @@ -286,7 +286,7 @@ public class CharIsNumberWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a number"; + return $"to {(_negated ? "not " : "")}be a number"; } } @@ -317,7 +317,7 @@ public class CharIsSeparatorWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a separator"; + return $"to {(_negated ? "not " : "")}be a separator"; } } @@ -348,7 +348,7 @@ public class CharIsSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a surrogate"; + return $"to {(_negated ? "not " : "")}be a surrogate"; } } @@ -379,7 +379,7 @@ public class CharIsHighSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a high surrogate"; + return $"to {(_negated ? "not " : "")}be a high surrogate"; } } @@ -410,7 +410,7 @@ public class CharIsLowSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a low surrogate"; + return $"to {(_negated ? "not " : "")}be a low surrogate"; } } @@ -441,7 +441,7 @@ public class CharIsLetterOrDigitWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a letter or digit"; + return $"to {(_negated ? "not " : "")}be a letter or digit"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.DotNet9_0.verified.txt index 4d320d531d6..3e7877ad611 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.DotNet9_0.verified.txt @@ -38,7 +38,7 @@ public class CharIsLetterWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a letter"; + return $"to {(_negated ? "not " : "")}be a letter"; } } @@ -69,7 +69,7 @@ public class CharIsDigitWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a digit"; + return $"to {(_negated ? "not " : "")}be a digit"; } } @@ -100,7 +100,7 @@ public class CharIsWhiteSpaceWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be whitespace"; + return $"to {(_negated ? "not " : "")}be whitespace"; } } @@ -131,7 +131,7 @@ public class CharIsUpperWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be uppercase"; + return $"to {(_negated ? "not " : "")}be uppercase"; } } @@ -162,7 +162,7 @@ public class CharIsLowerWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be lowercase"; + return $"to {(_negated ? "not " : "")}be lowercase"; } } @@ -193,7 +193,7 @@ public class CharIsControlWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a control character"; + return $"to {(_negated ? "not " : "")}be a control character"; } } @@ -224,7 +224,7 @@ public class CharIsPunctuationWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be punctuation"; + return $"to {(_negated ? "not " : "")}be punctuation"; } } @@ -255,7 +255,7 @@ public class CharIsSymbolWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a symbol"; + return $"to {(_negated ? "not " : "")}be a symbol"; } } @@ -286,7 +286,7 @@ public class CharIsNumberWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a number"; + return $"to {(_negated ? "not " : "")}be a number"; } } @@ -317,7 +317,7 @@ public class CharIsSeparatorWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a separator"; + return $"to {(_negated ? "not " : "")}be a separator"; } } @@ -348,7 +348,7 @@ public class CharIsSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a surrogate"; + return $"to {(_negated ? "not " : "")}be a surrogate"; } } @@ -379,7 +379,7 @@ public class CharIsHighSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a high surrogate"; + return $"to {(_negated ? "not " : "")}be a high surrogate"; } } @@ -410,7 +410,7 @@ public class CharIsLowSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a low surrogate"; + return $"to {(_negated ? "not " : "")}be a low surrogate"; } } @@ -441,7 +441,7 @@ public class CharIsLetterOrDigitWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a letter or digit"; + return $"to {(_negated ? "not " : "")}be a letter or digit"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.Net4_7.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.Net4_7.verified.txt index 4d320d531d6..3e7877ad611 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.Net4_7.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.Net4_7.verified.txt @@ -38,7 +38,7 @@ public class CharIsLetterWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a letter"; + return $"to {(_negated ? "not " : "")}be a letter"; } } @@ -69,7 +69,7 @@ public class CharIsDigitWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a digit"; + return $"to {(_negated ? "not " : "")}be a digit"; } } @@ -100,7 +100,7 @@ public class CharIsWhiteSpaceWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be whitespace"; + return $"to {(_negated ? "not " : "")}be whitespace"; } } @@ -131,7 +131,7 @@ public class CharIsUpperWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be uppercase"; + return $"to {(_negated ? "not " : "")}be uppercase"; } } @@ -162,7 +162,7 @@ public class CharIsLowerWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be lowercase"; + return $"to {(_negated ? "not " : "")}be lowercase"; } } @@ -193,7 +193,7 @@ public class CharIsControlWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a control character"; + return $"to {(_negated ? "not " : "")}be a control character"; } } @@ -224,7 +224,7 @@ public class CharIsPunctuationWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be punctuation"; + return $"to {(_negated ? "not " : "")}be punctuation"; } } @@ -255,7 +255,7 @@ public class CharIsSymbolWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a symbol"; + return $"to {(_negated ? "not " : "")}be a symbol"; } } @@ -286,7 +286,7 @@ public class CharIsNumberWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a number"; + return $"to {(_negated ? "not " : "")}be a number"; } } @@ -317,7 +317,7 @@ public class CharIsSeparatorWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a separator"; + return $"to {(_negated ? "not " : "")}be a separator"; } } @@ -348,7 +348,7 @@ public class CharIsSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a surrogate"; + return $"to {(_negated ? "not " : "")}be a surrogate"; } } @@ -379,7 +379,7 @@ public class CharIsHighSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a high surrogate"; + return $"to {(_negated ? "not " : "")}be a high surrogate"; } } @@ -410,7 +410,7 @@ public class CharIsLowSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a low surrogate"; + return $"to {(_negated ? "not " : "")}be a low surrogate"; } } @@ -441,7 +441,7 @@ public class CharIsLetterOrDigitWithCharAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a letter or digit"; + return $"to {(_negated ? "not " : "")}be a letter or digit"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/CultureInfoAssertionGeneratorTests.GeneratesCultureInfoAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/CultureInfoAssertionGeneratorTests.GeneratesCultureInfoAssertions.DotNet10_0.verified.txt index da206ed108b..6d98165ab97 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/CultureInfoAssertionGeneratorTests.GeneratesCultureInfoAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/CultureInfoAssertionGeneratorTests.GeneratesCultureInfoAssertions.DotNet10_0.verified.txt @@ -43,7 +43,7 @@ public class CultureInfoIsNeutralCultureAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be during daylight saving time"; + return $"to {(_negated ? "not " : "")}be during daylight saving time"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.DotNet8_0.verified.txt index f29fad142d7..16c6521b8fc 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.DotNet8_0.verified.txt @@ -38,7 +38,7 @@ public class DateTimeIsDaylightSavingTimeAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be during daylight saving time"; + return $"to {(_negated ? "not " : "")}be during daylight saving time"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.DotNet9_0.verified.txt index f29fad142d7..16c6521b8fc 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.DotNet9_0.verified.txt @@ -38,7 +38,7 @@ public class DateTimeIsDaylightSavingTimeAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be during daylight saving time"; + return $"to {(_negated ? "not " : "")}be during daylight saving time"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.Net4_7.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.Net4_7.verified.txt index f29fad142d7..16c6521b8fc 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.Net4_7.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.Net4_7.verified.txt @@ -38,7 +38,7 @@ public class DateTimeIsDaylightSavingTimeAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be during daylight saving time"; + return $"to {(_negated ? "not " : "")}be during daylight saving time"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet10_0.verified.txt index 499386d8aa7..d81d25faf67 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet10_0.verified.txt @@ -43,7 +43,7 @@ public class DirectoryInfoExistsAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} exist"; + return $"to {(_negated ? "not " : "")}exist"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet8_0.verified.txt index 499386d8aa7..d81d25faf67 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet8_0.verified.txt @@ -43,7 +43,7 @@ public class DirectoryInfoExistsAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} exist"; + return $"to {(_negated ? "not " : "")}exist"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet9_0.verified.txt index 499386d8aa7..d81d25faf67 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet9_0.verified.txt @@ -43,7 +43,7 @@ public class DirectoryInfoExistsAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} exist"; + return $"to {(_negated ? "not " : "")}exist"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.Net4_7.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.Net4_7.verified.txt index 499386d8aa7..d81d25faf67 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.Net4_7.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.Net4_7.verified.txt @@ -43,7 +43,7 @@ public class DirectoryInfoExistsAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} exist"; + return $"to {(_negated ? "not " : "")}exist"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet10_0.verified.txt index 88212af472f..576e8eefe7f 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet10_0.verified.txt @@ -43,7 +43,7 @@ public class EncodingIsSingleByteAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be single-byte encoding"; + return $"to {(_negated ? "not " : "")}be single-byte encoding"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet8_0.verified.txt index 88212af472f..576e8eefe7f 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet8_0.verified.txt @@ -43,7 +43,7 @@ public class EncodingIsSingleByteAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be single-byte encoding"; + return $"to {(_negated ? "not " : "")}be single-byte encoding"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet9_0.verified.txt index 88212af472f..576e8eefe7f 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet9_0.verified.txt @@ -43,7 +43,7 @@ public class EncodingIsSingleByteAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be single-byte encoding"; + return $"to {(_negated ? "not " : "")}be single-byte encoding"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.Net4_7.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.Net4_7.verified.txt index 88212af472f..576e8eefe7f 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.Net4_7.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.Net4_7.verified.txt @@ -43,7 +43,7 @@ public class EncodingIsSingleByteAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be single-byte encoding"; + return $"to {(_negated ? "not " : "")}be single-byte encoding"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet10_0.verified.txt index 3a4ee3a2b75..7648459f207 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet10_0.verified.txt @@ -43,7 +43,7 @@ public class FileInfoExistsAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} exist"; + return $"to {(_negated ? "not " : "")}exist"; } } @@ -79,7 +79,7 @@ public class FileInfoIsReadOnlyAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be read-only"; + return $"to {(_negated ? "not " : "")}be read-only"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet8_0.verified.txt index 3a4ee3a2b75..7648459f207 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet8_0.verified.txt @@ -43,7 +43,7 @@ public class FileInfoExistsAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} exist"; + return $"to {(_negated ? "not " : "")}exist"; } } @@ -79,7 +79,7 @@ public class FileInfoIsReadOnlyAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be read-only"; + return $"to {(_negated ? "not " : "")}be read-only"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet9_0.verified.txt index 3a4ee3a2b75..7648459f207 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet9_0.verified.txt @@ -43,7 +43,7 @@ public class FileInfoExistsAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} exist"; + return $"to {(_negated ? "not " : "")}exist"; } } @@ -79,7 +79,7 @@ public class FileInfoIsReadOnlyAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be read-only"; + return $"to {(_negated ? "not " : "")}be read-only"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.Net4_7.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.Net4_7.verified.txt index 3a4ee3a2b75..7648459f207 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.Net4_7.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.Net4_7.verified.txt @@ -43,7 +43,7 @@ public class FileInfoExistsAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} exist"; + return $"to {(_negated ? "not " : "")}exist"; } } @@ -79,7 +79,7 @@ public class FileInfoIsReadOnlyAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be read-only"; + return $"to {(_negated ? "not " : "")}be read-only"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/ProcessAssertionGeneratorTests.GeneratesProcessAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/ProcessAssertionGeneratorTests.GeneratesProcessAssertions.DotNet10_0.verified.txt index a9a587071ba..983e73965c4 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/ProcessAssertionGeneratorTests.GeneratesProcessAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/ProcessAssertionGeneratorTests.GeneratesProcessAssertions.DotNet10_0.verified.txt @@ -43,7 +43,7 @@ public class ProcessHasExitedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} have exited"; + return $"to {(_negated ? "not " : "")}have exited"; } } @@ -79,7 +79,7 @@ public class ProcessRespondingAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be responding"; + return $"to {(_negated ? "not " : "")}be responding"; } } @@ -115,7 +115,7 @@ public class ProcessEnableRaisingEventsAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} have exited"; + return $"to {(_negated ? "not " : "")}have exited"; } } @@ -79,7 +79,7 @@ public class ProcessRespondingAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be responding"; + return $"to {(_negated ? "not " : "")}be responding"; } } @@ -115,7 +115,7 @@ public class ProcessEnableRaisingEventsAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} have exited"; + return $"to {(_negated ? "not " : "")}have exited"; } } @@ -79,7 +79,7 @@ public class ProcessRespondingAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be responding"; + return $"to {(_negated ? "not " : "")}be responding"; } } @@ -115,7 +115,7 @@ public class ProcessEnableRaisingEventsAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} have exited"; + return $"to {(_negated ? "not " : "")}have exited"; } } @@ -79,7 +79,7 @@ public class ProcessRespondingAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be responding"; + return $"to {(_negated ? "not " : "")}be responding"; } } @@ -115,7 +115,7 @@ public class ProcessEnableRaisingEventsAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be readable"; + return $"to {(_negated ? "not " : "")}be readable"; } } @@ -79,7 +79,7 @@ public class StreamCanWriteAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be writable"; + return $"to {(_negated ? "not " : "")}be writable"; } } @@ -115,7 +115,7 @@ public class StreamCanSeekAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be seekable"; + return $"to {(_negated ? "not " : "")}be seekable"; } } @@ -151,7 +151,7 @@ public class StreamCanTimeoutAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} support timeout"; + return $"to {(_negated ? "not " : "")}support timeout"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.DotNet8_0.verified.txt index 145efb8cc08..0fa0d712ce0 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.DotNet8_0.verified.txt @@ -43,7 +43,7 @@ public class StreamCanReadAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be readable"; + return $"to {(_negated ? "not " : "")}be readable"; } } @@ -79,7 +79,7 @@ public class StreamCanWriteAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be writable"; + return $"to {(_negated ? "not " : "")}be writable"; } } @@ -115,7 +115,7 @@ public class StreamCanSeekAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be seekable"; + return $"to {(_negated ? "not " : "")}be seekable"; } } @@ -151,7 +151,7 @@ public class StreamCanTimeoutAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} support timeout"; + return $"to {(_negated ? "not " : "")}support timeout"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.DotNet9_0.verified.txt index 145efb8cc08..0fa0d712ce0 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.DotNet9_0.verified.txt @@ -43,7 +43,7 @@ public class StreamCanReadAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be readable"; + return $"to {(_negated ? "not " : "")}be readable"; } } @@ -79,7 +79,7 @@ public class StreamCanWriteAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be writable"; + return $"to {(_negated ? "not " : "")}be writable"; } } @@ -115,7 +115,7 @@ public class StreamCanSeekAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be seekable"; + return $"to {(_negated ? "not " : "")}be seekable"; } } @@ -151,7 +151,7 @@ public class StreamCanTimeoutAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} support timeout"; + return $"to {(_negated ? "not " : "")}support timeout"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.Net4_7.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.Net4_7.verified.txt index 145efb8cc08..0fa0d712ce0 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.Net4_7.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.Net4_7.verified.txt @@ -43,7 +43,7 @@ public class StreamCanReadAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be readable"; + return $"to {(_negated ? "not " : "")}be readable"; } } @@ -79,7 +79,7 @@ public class StreamCanWriteAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be writable"; + return $"to {(_negated ? "not " : "")}be writable"; } } @@ -115,7 +115,7 @@ public class StreamCanSeekAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be seekable"; + return $"to {(_negated ? "not " : "")}be seekable"; } } @@ -151,7 +151,7 @@ public class StreamCanTimeoutAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} support timeout"; + return $"to {(_negated ? "not " : "")}support timeout"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet10_0.verified.txt index c675e696189..cb6e1d1307b 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet10_0.verified.txt @@ -39,7 +39,7 @@ public class TaskIsCompletedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be completed"; + return $"to {(_negated ? "not " : "")}be completed"; } } @@ -71,7 +71,7 @@ public class TaskIsCanceledAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be canceled"; + return $"to {(_negated ? "not " : "")}be canceled"; } } @@ -103,7 +103,7 @@ public class TaskIsFaultedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be faulted"; + return $"to {(_negated ? "not " : "")}be faulted"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet8_0.verified.txt index c675e696189..cb6e1d1307b 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet8_0.verified.txt @@ -39,7 +39,7 @@ public class TaskIsCompletedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be completed"; + return $"to {(_negated ? "not " : "")}be completed"; } } @@ -71,7 +71,7 @@ public class TaskIsCanceledAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be canceled"; + return $"to {(_negated ? "not " : "")}be canceled"; } } @@ -103,7 +103,7 @@ public class TaskIsFaultedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be faulted"; + return $"to {(_negated ? "not " : "")}be faulted"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet9_0.verified.txt index c675e696189..cb6e1d1307b 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet9_0.verified.txt @@ -39,7 +39,7 @@ public class TaskIsCompletedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be completed"; + return $"to {(_negated ? "not " : "")}be completed"; } } @@ -71,7 +71,7 @@ public class TaskIsCanceledAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be canceled"; + return $"to {(_negated ? "not " : "")}be canceled"; } } @@ -103,7 +103,7 @@ public class TaskIsFaultedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be faulted"; + return $"to {(_negated ? "not " : "")}be faulted"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.Net4_7.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.Net4_7.verified.txt index c675e696189..cb6e1d1307b 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.Net4_7.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.Net4_7.verified.txt @@ -39,7 +39,7 @@ public class TaskIsCompletedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be completed"; + return $"to {(_negated ? "not " : "")}be completed"; } } @@ -71,7 +71,7 @@ public class TaskIsCanceledAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be canceled"; + return $"to {(_negated ? "not " : "")}be canceled"; } } @@ -103,7 +103,7 @@ public class TaskIsFaultedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be faulted"; + return $"to {(_negated ? "not " : "")}be faulted"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/ThreadAssertionGeneratorTests.GeneratesThreadAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/ThreadAssertionGeneratorTests.GeneratesThreadAssertions.DotNet10_0.verified.txt index b414d544488..c8ff87e3ead 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/ThreadAssertionGeneratorTests.GeneratesThreadAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/ThreadAssertionGeneratorTests.GeneratesThreadAssertions.DotNet10_0.verified.txt @@ -43,7 +43,7 @@ public class ThreadIsAliveAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be alive"; + return $"to {(_negated ? "not " : "")}be alive"; } } @@ -79,7 +79,7 @@ public class ThreadIsBackgroundAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a background thread"; + return $"to {(_negated ? "not " : "")}be a background thread"; } } @@ -115,7 +115,7 @@ public class ThreadIsThreadPoolThreadAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be alive"; + return $"to {(_negated ? "not " : "")}be alive"; } } @@ -79,7 +79,7 @@ public class ThreadIsBackgroundAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a background thread"; + return $"to {(_negated ? "not " : "")}be a background thread"; } } @@ -115,7 +115,7 @@ public class ThreadIsThreadPoolThreadAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be alive"; + return $"to {(_negated ? "not " : "")}be alive"; } } @@ -79,7 +79,7 @@ public class ThreadIsBackgroundAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a background thread"; + return $"to {(_negated ? "not " : "")}be a background thread"; } } @@ -115,7 +115,7 @@ public class ThreadIsThreadPoolThreadAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be alive"; + return $"to {(_negated ? "not " : "")}be alive"; } } @@ -79,7 +79,7 @@ public class ThreadIsBackgroundAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a background thread"; + return $"to {(_negated ? "not " : "")}be a background thread"; } } @@ -115,7 +115,7 @@ public class ThreadIsThreadPoolThreadAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a class"; + return $"to {(_negated ? "not " : "")}be a class"; } } @@ -79,7 +79,7 @@ public class TypeIsInterfaceAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be an interface"; + return $"to {(_negated ? "not " : "")}be an interface"; } } @@ -115,7 +115,7 @@ public class TypeIsAbstractAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be abstract"; + return $"to {(_negated ? "not " : "")}be abstract"; } } @@ -151,7 +151,7 @@ public class TypeIsSealedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be sealed"; + return $"to {(_negated ? "not " : "")}be sealed"; } } @@ -187,7 +187,7 @@ public class TypeIsValueTypeAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a value type"; + return $"to {(_negated ? "not " : "")}be a value type"; } } @@ -223,7 +223,7 @@ public class TypeIsEnumAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be an enum"; + return $"to {(_negated ? "not " : "")}be an enum"; } } @@ -259,7 +259,7 @@ public class TypeIsPrimitiveAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a primitive type"; + return $"to {(_negated ? "not " : "")}be a primitive type"; } } @@ -295,7 +295,7 @@ public class TypeIsPublicAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be public"; + return $"to {(_negated ? "not " : "")}be public"; } } @@ -331,7 +331,7 @@ public class TypeIsGenericTypeAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a generic type"; + return $"to {(_negated ? "not " : "")}be a generic type"; } } @@ -367,7 +367,7 @@ public class TypeIsGenericTypeDefinitionAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a generic type definition"; + return $"to {(_negated ? "not " : "")}be a generic type definition"; } } @@ -403,7 +403,7 @@ public class TypeIsArrayAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be an array"; + return $"to {(_negated ? "not " : "")}be an array"; } } @@ -439,7 +439,7 @@ public class TypeIsByRefAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a by-ref type"; + return $"to {(_negated ? "not " : "")}be a by-ref type"; } } @@ -475,7 +475,7 @@ public class TypeIsPointerAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a pointer type"; + return $"to {(_negated ? "not " : "")}be a pointer type"; } } @@ -511,7 +511,7 @@ public class TypeIsNestedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested type"; + return $"to {(_negated ? "not " : "")}be a nested type"; } } @@ -547,7 +547,7 @@ public class TypeIsNestedPublicAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested public type"; + return $"to {(_negated ? "not " : "")}be a nested public type"; } } @@ -583,7 +583,7 @@ public class TypeIsNestedPrivateAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested private type"; + return $"to {(_negated ? "not " : "")}be a nested private type"; } } @@ -619,7 +619,7 @@ public class TypeIsNestedAssemblyAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested assembly type"; + return $"to {(_negated ? "not " : "")}be a nested assembly type"; } } @@ -655,7 +655,7 @@ public class TypeIsNestedFamilyAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested family type"; + return $"to {(_negated ? "not " : "")}be a nested family type"; } } @@ -691,7 +691,7 @@ public class TypeIsVisibleAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be visible"; + return $"to {(_negated ? "not " : "")}be visible"; } } @@ -727,7 +727,7 @@ public class TypeIsConstructedGenericTypeAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a constructed generic type"; + return $"to {(_negated ? "not " : "")}be a constructed generic type"; } } @@ -763,7 +763,7 @@ public class TypeContainsGenericParametersAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} contain generic parameters"; + return $"to {(_negated ? "not " : "")}contain generic parameters"; } } @@ -799,7 +799,7 @@ public class TypeIsSerializableAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be serializable"; + return $"to {(_negated ? "not " : "")}be serializable"; } } @@ -835,7 +835,7 @@ public class TypeIsCOMObjectAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a COM object"; + return $"to {(_negated ? "not " : "")}be a COM object"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.DotNet8_0.verified.txt index bc6c2118114..aba36d8286e 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.DotNet8_0.verified.txt @@ -43,7 +43,7 @@ public class TypeIsClassAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a class"; + return $"to {(_negated ? "not " : "")}be a class"; } } @@ -79,7 +79,7 @@ public class TypeIsInterfaceAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be an interface"; + return $"to {(_negated ? "not " : "")}be an interface"; } } @@ -115,7 +115,7 @@ public class TypeIsAbstractAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be abstract"; + return $"to {(_negated ? "not " : "")}be abstract"; } } @@ -151,7 +151,7 @@ public class TypeIsSealedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be sealed"; + return $"to {(_negated ? "not " : "")}be sealed"; } } @@ -187,7 +187,7 @@ public class TypeIsValueTypeAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a value type"; + return $"to {(_negated ? "not " : "")}be a value type"; } } @@ -223,7 +223,7 @@ public class TypeIsEnumAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be an enum"; + return $"to {(_negated ? "not " : "")}be an enum"; } } @@ -259,7 +259,7 @@ public class TypeIsPrimitiveAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a primitive type"; + return $"to {(_negated ? "not " : "")}be a primitive type"; } } @@ -295,7 +295,7 @@ public class TypeIsPublicAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be public"; + return $"to {(_negated ? "not " : "")}be public"; } } @@ -331,7 +331,7 @@ public class TypeIsGenericTypeAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a generic type"; + return $"to {(_negated ? "not " : "")}be a generic type"; } } @@ -367,7 +367,7 @@ public class TypeIsGenericTypeDefinitionAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a generic type definition"; + return $"to {(_negated ? "not " : "")}be a generic type definition"; } } @@ -403,7 +403,7 @@ public class TypeIsArrayAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be an array"; + return $"to {(_negated ? "not " : "")}be an array"; } } @@ -439,7 +439,7 @@ public class TypeIsByRefAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a by-ref type"; + return $"to {(_negated ? "not " : "")}be a by-ref type"; } } @@ -475,7 +475,7 @@ public class TypeIsPointerAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a pointer type"; + return $"to {(_negated ? "not " : "")}be a pointer type"; } } @@ -511,7 +511,7 @@ public class TypeIsNestedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested type"; + return $"to {(_negated ? "not " : "")}be a nested type"; } } @@ -547,7 +547,7 @@ public class TypeIsNestedPublicAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested public type"; + return $"to {(_negated ? "not " : "")}be a nested public type"; } } @@ -583,7 +583,7 @@ public class TypeIsNestedPrivateAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested private type"; + return $"to {(_negated ? "not " : "")}be a nested private type"; } } @@ -619,7 +619,7 @@ public class TypeIsNestedAssemblyAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested assembly type"; + return $"to {(_negated ? "not " : "")}be a nested assembly type"; } } @@ -655,7 +655,7 @@ public class TypeIsNestedFamilyAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested family type"; + return $"to {(_negated ? "not " : "")}be a nested family type"; } } @@ -691,7 +691,7 @@ public class TypeIsVisibleAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be visible"; + return $"to {(_negated ? "not " : "")}be visible"; } } @@ -727,7 +727,7 @@ public class TypeIsConstructedGenericTypeAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a constructed generic type"; + return $"to {(_negated ? "not " : "")}be a constructed generic type"; } } @@ -763,7 +763,7 @@ public class TypeContainsGenericParametersAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} contain generic parameters"; + return $"to {(_negated ? "not " : "")}contain generic parameters"; } } @@ -799,7 +799,7 @@ public class TypeIsSerializableAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be serializable"; + return $"to {(_negated ? "not " : "")}be serializable"; } } @@ -835,7 +835,7 @@ public class TypeIsCOMObjectAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a COM object"; + return $"to {(_negated ? "not " : "")}be a COM object"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.DotNet9_0.verified.txt index bc6c2118114..aba36d8286e 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.DotNet9_0.verified.txt @@ -43,7 +43,7 @@ public class TypeIsClassAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a class"; + return $"to {(_negated ? "not " : "")}be a class"; } } @@ -79,7 +79,7 @@ public class TypeIsInterfaceAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be an interface"; + return $"to {(_negated ? "not " : "")}be an interface"; } } @@ -115,7 +115,7 @@ public class TypeIsAbstractAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be abstract"; + return $"to {(_negated ? "not " : "")}be abstract"; } } @@ -151,7 +151,7 @@ public class TypeIsSealedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be sealed"; + return $"to {(_negated ? "not " : "")}be sealed"; } } @@ -187,7 +187,7 @@ public class TypeIsValueTypeAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a value type"; + return $"to {(_negated ? "not " : "")}be a value type"; } } @@ -223,7 +223,7 @@ public class TypeIsEnumAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be an enum"; + return $"to {(_negated ? "not " : "")}be an enum"; } } @@ -259,7 +259,7 @@ public class TypeIsPrimitiveAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a primitive type"; + return $"to {(_negated ? "not " : "")}be a primitive type"; } } @@ -295,7 +295,7 @@ public class TypeIsPublicAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be public"; + return $"to {(_negated ? "not " : "")}be public"; } } @@ -331,7 +331,7 @@ public class TypeIsGenericTypeAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a generic type"; + return $"to {(_negated ? "not " : "")}be a generic type"; } } @@ -367,7 +367,7 @@ public class TypeIsGenericTypeDefinitionAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a generic type definition"; + return $"to {(_negated ? "not " : "")}be a generic type definition"; } } @@ -403,7 +403,7 @@ public class TypeIsArrayAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be an array"; + return $"to {(_negated ? "not " : "")}be an array"; } } @@ -439,7 +439,7 @@ public class TypeIsByRefAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a by-ref type"; + return $"to {(_negated ? "not " : "")}be a by-ref type"; } } @@ -475,7 +475,7 @@ public class TypeIsPointerAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a pointer type"; + return $"to {(_negated ? "not " : "")}be a pointer type"; } } @@ -511,7 +511,7 @@ public class TypeIsNestedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested type"; + return $"to {(_negated ? "not " : "")}be a nested type"; } } @@ -547,7 +547,7 @@ public class TypeIsNestedPublicAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested public type"; + return $"to {(_negated ? "not " : "")}be a nested public type"; } } @@ -583,7 +583,7 @@ public class TypeIsNestedPrivateAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested private type"; + return $"to {(_negated ? "not " : "")}be a nested private type"; } } @@ -619,7 +619,7 @@ public class TypeIsNestedAssemblyAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested assembly type"; + return $"to {(_negated ? "not " : "")}be a nested assembly type"; } } @@ -655,7 +655,7 @@ public class TypeIsNestedFamilyAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested family type"; + return $"to {(_negated ? "not " : "")}be a nested family type"; } } @@ -691,7 +691,7 @@ public class TypeIsVisibleAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be visible"; + return $"to {(_negated ? "not " : "")}be visible"; } } @@ -727,7 +727,7 @@ public class TypeIsConstructedGenericTypeAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a constructed generic type"; + return $"to {(_negated ? "not " : "")}be a constructed generic type"; } } @@ -763,7 +763,7 @@ public class TypeContainsGenericParametersAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} contain generic parameters"; + return $"to {(_negated ? "not " : "")}contain generic parameters"; } } @@ -799,7 +799,7 @@ public class TypeIsSerializableAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be serializable"; + return $"to {(_negated ? "not " : "")}be serializable"; } } @@ -835,7 +835,7 @@ public class TypeIsCOMObjectAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a COM object"; + return $"to {(_negated ? "not " : "")}be a COM object"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.Net4_7.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.Net4_7.verified.txt index bc6c2118114..aba36d8286e 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.Net4_7.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.Net4_7.verified.txt @@ -43,7 +43,7 @@ public class TypeIsClassAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a class"; + return $"to {(_negated ? "not " : "")}be a class"; } } @@ -79,7 +79,7 @@ public class TypeIsInterfaceAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be an interface"; + return $"to {(_negated ? "not " : "")}be an interface"; } } @@ -115,7 +115,7 @@ public class TypeIsAbstractAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be abstract"; + return $"to {(_negated ? "not " : "")}be abstract"; } } @@ -151,7 +151,7 @@ public class TypeIsSealedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be sealed"; + return $"to {(_negated ? "not " : "")}be sealed"; } } @@ -187,7 +187,7 @@ public class TypeIsValueTypeAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a value type"; + return $"to {(_negated ? "not " : "")}be a value type"; } } @@ -223,7 +223,7 @@ public class TypeIsEnumAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be an enum"; + return $"to {(_negated ? "not " : "")}be an enum"; } } @@ -259,7 +259,7 @@ public class TypeIsPrimitiveAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a primitive type"; + return $"to {(_negated ? "not " : "")}be a primitive type"; } } @@ -295,7 +295,7 @@ public class TypeIsPublicAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be public"; + return $"to {(_negated ? "not " : "")}be public"; } } @@ -331,7 +331,7 @@ public class TypeIsGenericTypeAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a generic type"; + return $"to {(_negated ? "not " : "")}be a generic type"; } } @@ -367,7 +367,7 @@ public class TypeIsGenericTypeDefinitionAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a generic type definition"; + return $"to {(_negated ? "not " : "")}be a generic type definition"; } } @@ -403,7 +403,7 @@ public class TypeIsArrayAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be an array"; + return $"to {(_negated ? "not " : "")}be an array"; } } @@ -439,7 +439,7 @@ public class TypeIsByRefAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a by-ref type"; + return $"to {(_negated ? "not " : "")}be a by-ref type"; } } @@ -475,7 +475,7 @@ public class TypeIsPointerAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a pointer type"; + return $"to {(_negated ? "not " : "")}be a pointer type"; } } @@ -511,7 +511,7 @@ public class TypeIsNestedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested type"; + return $"to {(_negated ? "not " : "")}be a nested type"; } } @@ -547,7 +547,7 @@ public class TypeIsNestedPublicAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested public type"; + return $"to {(_negated ? "not " : "")}be a nested public type"; } } @@ -583,7 +583,7 @@ public class TypeIsNestedPrivateAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested private type"; + return $"to {(_negated ? "not " : "")}be a nested private type"; } } @@ -619,7 +619,7 @@ public class TypeIsNestedAssemblyAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested assembly type"; + return $"to {(_negated ? "not " : "")}be a nested assembly type"; } } @@ -655,7 +655,7 @@ public class TypeIsNestedFamilyAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a nested family type"; + return $"to {(_negated ? "not " : "")}be a nested family type"; } } @@ -691,7 +691,7 @@ public class TypeIsVisibleAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be visible"; + return $"to {(_negated ? "not " : "")}be visible"; } } @@ -727,7 +727,7 @@ public class TypeIsConstructedGenericTypeAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a constructed generic type"; + return $"to {(_negated ? "not " : "")}be a constructed generic type"; } } @@ -763,7 +763,7 @@ public class TypeContainsGenericParametersAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} contain generic parameters"; + return $"to {(_negated ? "not " : "")}contain generic parameters"; } } @@ -799,7 +799,7 @@ public class TypeIsSerializableAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be serializable"; + return $"to {(_negated ? "not " : "")}be serializable"; } } @@ -835,7 +835,7 @@ public class TypeIsCOMObjectAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a COM object"; + return $"to {(_negated ? "not " : "")}be a COM object"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet10_0.verified.txt index 4dbf3aa8313..d51d59a2add 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet10_0.verified.txt @@ -43,7 +43,7 @@ public class UriIsAbsoluteUriAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be an absolute URI"; + return $"to {(_negated ? "not " : "")}be an absolute URI"; } } @@ -79,7 +79,7 @@ public class UriIsFileAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a file URI"; + return $"to {(_negated ? "not " : "")}be a file URI"; } } @@ -115,7 +115,7 @@ public class UriIsUncAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a UNC URI"; + return $"to {(_negated ? "not " : "")}be a UNC URI"; } } @@ -151,7 +151,7 @@ public class UriIsLoopbackAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a loopback URI"; + return $"to {(_negated ? "not " : "")}be a loopback URI"; } } @@ -187,7 +187,7 @@ public class UriIsDefaultPortAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} use the default port"; + return $"to {(_negated ? "not " : "")}use the default port"; } } @@ -223,7 +223,7 @@ public class UriUserEscapedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be user-escaped"; + return $"to {(_negated ? "not " : "")}be user-escaped"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet8_0.verified.txt index 4dbf3aa8313..d51d59a2add 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet8_0.verified.txt @@ -43,7 +43,7 @@ public class UriIsAbsoluteUriAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be an absolute URI"; + return $"to {(_negated ? "not " : "")}be an absolute URI"; } } @@ -79,7 +79,7 @@ public class UriIsFileAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a file URI"; + return $"to {(_negated ? "not " : "")}be a file URI"; } } @@ -115,7 +115,7 @@ public class UriIsUncAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a UNC URI"; + return $"to {(_negated ? "not " : "")}be a UNC URI"; } } @@ -151,7 +151,7 @@ public class UriIsLoopbackAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a loopback URI"; + return $"to {(_negated ? "not " : "")}be a loopback URI"; } } @@ -187,7 +187,7 @@ public class UriIsDefaultPortAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} use the default port"; + return $"to {(_negated ? "not " : "")}use the default port"; } } @@ -223,7 +223,7 @@ public class UriUserEscapedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be user-escaped"; + return $"to {(_negated ? "not " : "")}be user-escaped"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet9_0.verified.txt index 4dbf3aa8313..d51d59a2add 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet9_0.verified.txt @@ -43,7 +43,7 @@ public class UriIsAbsoluteUriAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be an absolute URI"; + return $"to {(_negated ? "not " : "")}be an absolute URI"; } } @@ -79,7 +79,7 @@ public class UriIsFileAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a file URI"; + return $"to {(_negated ? "not " : "")}be a file URI"; } } @@ -115,7 +115,7 @@ public class UriIsUncAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a UNC URI"; + return $"to {(_negated ? "not " : "")}be a UNC URI"; } } @@ -151,7 +151,7 @@ public class UriIsLoopbackAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a loopback URI"; + return $"to {(_negated ? "not " : "")}be a loopback URI"; } } @@ -187,7 +187,7 @@ public class UriIsDefaultPortAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} use the default port"; + return $"to {(_negated ? "not " : "")}use the default port"; } } @@ -223,7 +223,7 @@ public class UriUserEscapedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be user-escaped"; + return $"to {(_negated ? "not " : "")}be user-escaped"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.Net4_7.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.Net4_7.verified.txt index 4dbf3aa8313..d51d59a2add 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.Net4_7.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.Net4_7.verified.txt @@ -43,7 +43,7 @@ public class UriIsAbsoluteUriAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be an absolute URI"; + return $"to {(_negated ? "not " : "")}be an absolute URI"; } } @@ -79,7 +79,7 @@ public class UriIsFileAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a file URI"; + return $"to {(_negated ? "not " : "")}be a file URI"; } } @@ -115,7 +115,7 @@ public class UriIsUncAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a UNC URI"; + return $"to {(_negated ? "not " : "")}be a UNC URI"; } } @@ -151,7 +151,7 @@ public class UriIsLoopbackAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be a loopback URI"; + return $"to {(_negated ? "not " : "")}be a loopback URI"; } } @@ -187,7 +187,7 @@ public class UriIsDefaultPortAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} use the default port"; + return $"to {(_negated ? "not " : "")}use the default port"; } } @@ -223,7 +223,7 @@ public class UriUserEscapedAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be user-escaped"; + return $"to {(_negated ? "not " : "")}be user-escaped"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/WeakReferenceAssertionGeneratorTests.GeneratesWeakReferenceAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/WeakReferenceAssertionGeneratorTests.GeneratesWeakReferenceAssertions.DotNet10_0.verified.txt index 073d551b3c2..e61820f053f 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/WeakReferenceAssertionGeneratorTests.GeneratesWeakReferenceAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/WeakReferenceAssertionGeneratorTests.GeneratesWeakReferenceAssertions.DotNet10_0.verified.txt @@ -43,7 +43,7 @@ public class WeakReferenceIsAliveAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be alive"; + return $"to {(_negated ? "not " : "")}be alive"; } } @@ -79,7 +79,7 @@ public class WeakReferenceTrackResurrectionAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be alive"; + return $"to {(_negated ? "not " : "")}be alive"; } } @@ -79,7 +79,7 @@ public class WeakReferenceTrackResurrectionAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be alive"; + return $"to {(_negated ? "not " : "")}be alive"; } } @@ -79,7 +79,7 @@ public class WeakReferenceTrackResurrectionAssertion : Assertion protected override string GetExpectation() { - return $"{(_negated ? "not " : "")} be alive"; + return $"to {(_negated ? "not " : "")}be alive"; } } @@ -79,7 +79,7 @@ public class WeakReferenceTrackResurrectionAssertion : Assertion await Assert.That(emptyString).IsNotNullOrEmpty()) .Throws(); } + [Test] + public async Task IsNullOrEmpty_Failure_Messages_Read_As_Sentences() + { + // [AssertionFrom] expectation messages omit the leading "to"; the generated assertion adds it. + var value = "Foo"; + var emptyString = ""; + + await Assert.That(async () => await Assert.That(value).IsNullOrEmpty()) + .Throws() + .WithMessageContaining("Expected to be null or empty"); + + await Assert.That(async () => await Assert.That(emptyString).IsNotNullOrEmpty()) + .Throws() + .WithMessageContaining("Expected to not be null or empty"); + } + [Test] public async Task IsNotNullOrEmpty_WithNonEmptyString_Passes() { From c15bb516fa0fd72e8196d15211d983a87968daa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 06:22:22 +0000 Subject: [PATCH 13/49] test(assertions): revive tests disabled for APIs that now exist - RangeAssertionTests: HasEndFromBeginning and IsFullRange (successor of the obsolete IsAll) had no active tests, only commented-out ones saying the generator did not produce them yet. - AssertionGroupTests: the And/Or grouping tests over IsNullOrEmpty were commented out as "not available"; reviving them exposed the message bug fixed in the previous commit. - Old/StringAssertionTests: drop commented-out IsNullOrEmpty / IsNullOrWhiteSpace tests that StringNullabilityAssertionTests already covers, and their stale "not available" note. --- .../AssertionGroupTests.cs | 59 +++++++++---------- .../Old/StringAssertionTests.cs | 59 ------------------- .../RangeAssertionTests.cs | 51 ++++++++-------- 3 files changed, 52 insertions(+), 117 deletions(-) diff --git a/tests/TUnit.Assertions.Tests/AssertionGroupTests.cs b/tests/TUnit.Assertions.Tests/AssertionGroupTests.cs index bbee2b1ec6e..5cfa5bb2c9f 100644 --- a/tests/TUnit.Assertions.Tests/AssertionGroupTests.cs +++ b/tests/TUnit.Assertions.Tests/AssertionGroupTests.cs @@ -32,38 +32,35 @@ await Assert.That(value) .IsEqualTo("Foo"); } - // IsNullOrEmpty is not available in current API - // These tests are commented out as they test deprecated functionality - - //[Test] - //public async Task Complex_Or_With_Delegates() - //{ - // // Test: "Foo" should match (IsNullOrEmpty AND EqualTo("Foo")) OR (IsNullOrEmpty OR EqualTo("Foo")) - // // Second condition passes because EqualTo("Foo") is true - // var value = "Foo"; + [Test] + public async Task Complex_Or_With_Delegates() + { + // Test: "Foo" should match (IsNullOrEmpty AND EqualTo("Foo")) OR (IsNullOrEmpty OR EqualTo("Foo")) + // Second condition passes because EqualTo("Foo") is true + var value = "Foo"; - // // Try first assertion, if it fails try second - // try - // { - // await Assert.That(value).IsNullOrEmpty().And.IsEqualTo("Foo"); - // } - // catch (AssertionException) - // { - // await Assert.That(value).IsNullOrEmpty().Or.IsEqualTo("Foo"); - // } - //} + // Try first assertion, if it fails try second + try + { + await Assert.That(value).IsNullOrEmpty().And.IsEqualTo("Foo"); + } + catch (AssertionException) + { + await Assert.That(value).IsNullOrEmpty().Or.IsEqualTo("Foo"); + } + } - //[Test] - //public async Task And_Condition_Throws_As_Expected() - //{ - // var value = "Foo"; + [Test] + public async Task And_Condition_Throws_As_Expected() + { + var value = "Foo"; - // await Assert.That(async () => - // await Assert.That(value).IsNullOrEmpty().And.IsEqualTo("Foo") - // ).Throws() - // .And - // .HasMessageContaining("to be null or empty") - // .And - // .HasMessageContaining("Foo"); - //} + await Assert.That(async () => + await Assert.That(value).IsNullOrEmpty().And.IsEqualTo("Foo") + ).Throws() + .And + .HasMessageContaining("to be null or empty") + .And + .HasMessageContaining("Foo"); + } } diff --git a/tests/TUnit.Assertions.Tests/Old/StringAssertionTests.cs b/tests/TUnit.Assertions.Tests/Old/StringAssertionTests.cs index 183c8b994a9..9dd75f47667 100644 --- a/tests/TUnit.Assertions.Tests/Old/StringAssertionTests.cs +++ b/tests/TUnit.Assertions.Tests/Old/StringAssertionTests.cs @@ -4,65 +4,6 @@ namespace TUnit.Assertions.Tests.Old; public class StringAssertionTests { - // IsNullOrEmpty and IsNullOrWhiteSpace are not available in current API - // These tests are commented out as they test deprecated functionality - - //[Test] - //public async Task NullOrEmpty_String() - //{ - // var str = "Hello"; - // await TUnitAssert.ThrowsAsync(async () => await TUnitAssert.That(str).IsNullOrEmpty()); - //} - - //[Test] - //public async Task NullOrEmpty_Whitespace() - //{ - // var str = " "; - // await TUnitAssert.ThrowsAsync(async () => await TUnitAssert.That(str).IsNullOrEmpty()); - //} - - //[Test] - //public async Task NullOrEmpty_Null() - //{ - // string? str = null; - // await TUnitAssert.That(str).IsNullOrEmpty(); - //} - - //[Test] - //public async Task NullOrEmpty_Empty() - //{ - // var str = ""; - // await TUnitAssert.That(str).IsNullOrEmpty(); - //} - - //[Test] - //public async Task NullOrWhitespace_String() - //{ - // var str = "Hello"; - // await TUnitAssert.ThrowsAsync(async () => await TUnitAssert.That(str).IsNullOrWhiteSpace()); - //} - - //[Test] - //public async Task NullOrWhitespace_Whitespace() - //{ - // var str = " "; - // await TUnitAssert.That(str).IsNullOrWhiteSpace(); - //} - - //[Test] - //public async Task NullOrWhitespace_Null() - //{ - // string? str = null; - // await TUnitAssert.That(str).IsNullOrWhiteSpace(); - //} - - //[Test] - //public async Task NullOrWhitespace_Empty() - //{ - // var str = ""; - // await TUnitAssert.That(str).IsNullOrWhiteSpace(); - //} - [Test] public async Task Null_String() { diff --git a/tests/TUnit.Assertions.Tests/RangeAssertionTests.cs b/tests/TUnit.Assertions.Tests/RangeAssertionTests.cs index 6d6ef7f7ff8..0ef3acdfb3d 100644 --- a/tests/TUnit.Assertions.Tests/RangeAssertionTests.cs +++ b/tests/TUnit.Assertions.Tests/RangeAssertionTests.cs @@ -33,35 +33,32 @@ public async Task Test_Range_HasStartFromBeginning_Mixed() await Assert.That(range).HasStartFromBeginning(); } - // Note: HasEndFromBeginning and IsAll are not yet generated by the source generator - // These tests are commented out until the .NET 6+ type generation issue is resolved - - // [Test] - // public async Task Test_Range_HasEndFromBeginning() - // { - // Range range = 0..10; // From start to index 10 - // await Assert.That(range).HasEndFromBeginning(); - // } + [Test] + public async Task Test_Range_HasEndFromBeginning() + { + Range range = 0..10; // From start to index 10 + await Assert.That(range).HasEndFromBeginning(); + } - // [Test] - // public async Task Test_Range_HasEndFromBeginning_Mixed() - // { - // Range range = ^5..10; // From 5th from end to index 10 - // await Assert.That(range).HasEndFromBeginning(); - // } + [Test] + public async Task Test_Range_HasEndFromBeginning_Mixed() + { + Range range = ^5..10; // From 5th from end to index 10 + await Assert.That(range).HasEndFromBeginning(); + } - // [Test] - // public async Task Test_Range_IsAll() - // { - // Range range = ..; // All elements - // await Assert.That(range).IsAll(); - // } + [Test] + public async Task Test_Range_IsFullRange() + { + Range range = ..; // All elements + await Assert.That(range).IsFullRange(); + } - // [Test] - // public async Task Test_Range_IsAll_Explicit() - // { - // var range = Range.All; - // await Assert.That(range).IsAll(); - // } + [Test] + public async Task Test_Range_IsFullRange_Explicit() + { + var range = Range.All; + await Assert.That(range).IsFullRange(); + } } #endif From aefa1faa96b89907eef4622a94f3dd39c7eb9564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 06:23:50 +0000 Subject: [PATCH 14/49] test(mocks): start the design-time build with the running .NET installation DesignTimeProjectReferenceTests started `dotnet build` via a bare "dotnet" from PATH, like the RpcTests/Engine tests fixed earlier in this branch. Use the shared DotNetCli resolver so it uses the same installation. --- .../DesignTimeProjectReferenceTests.cs | 3 ++- .../TUnit.Mocks.InternalsAccess.Tests.csproj | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/TUnit.Mocks.InternalsAccess.Tests/DesignTimeProjectReferenceTests.cs b/tests/TUnit.Mocks.InternalsAccess.Tests/DesignTimeProjectReferenceTests.cs index 66c0efbc43a..d02f0bb22c9 100644 --- a/tests/TUnit.Mocks.InternalsAccess.Tests/DesignTimeProjectReferenceTests.cs +++ b/tests/TUnit.Mocks.InternalsAccess.Tests/DesignTimeProjectReferenceTests.cs @@ -2,6 +2,7 @@ using System.Security; using System.Text; using System.Text.Json; +using TUnit.Tests.Shared; using Task = System.Threading.Tasks.Task; namespace TUnit.Mocks.InternalsAccess.Tests; @@ -230,7 +231,7 @@ private static List ArgumentsFor(bool designTimeBuild) private static async Task RunAsync(string verb, string project, params string[] arguments) { - var startInfo = new ProcessStartInfo("dotnet") + var startInfo = new ProcessStartInfo(DotNetCli.Path) { RedirectStandardOutput = true, RedirectStandardError = true, diff --git a/tests/TUnit.Mocks.InternalsAccess.Tests/TUnit.Mocks.InternalsAccess.Tests.csproj b/tests/TUnit.Mocks.InternalsAccess.Tests/TUnit.Mocks.InternalsAccess.Tests.csproj index c9a606d0eb6..1f418da88f1 100644 --- a/tests/TUnit.Mocks.InternalsAccess.Tests/TUnit.Mocks.InternalsAccess.Tests.csproj +++ b/tests/TUnit.Mocks.InternalsAccess.Tests/TUnit.Mocks.InternalsAccess.Tests.csproj @@ -8,6 +8,10 @@ true + + + + From 584d2e3db6efbd8f90db7a033236c656825756eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 06:30:06 +0000 Subject: [PATCH 15/49] fix(analyzers): don't flag interface implementations with TUnit0014 TUnit0014 ("public method missing [Test]; add the attribute or make it private") special-cased Dispose/DisposeAsync/InitializeAsync by name but not interface implementations in general, so a test class implementing e.g. ITestStartEventReceiver got a warning on OnTestStart that it could not follow without breaking the interface (seen in TUnit.UnitTests/PlaywrightCompositionLifecycleTests). Skip methods that implement an interface member of the containing type. A same-named public method that does not implement the member is still reported. --- ...ublicMethodMissingTestAttributeAnalyzer.cs | 21 ++++++ ...MethodMissingTestAttributeAnalyzerTests.cs | 75 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/src/TUnit.Analyzers/PublicMethodMissingTestAttributeAnalyzer.cs b/src/TUnit.Analyzers/PublicMethodMissingTestAttributeAnalyzer.cs index fed67935f3d..014db2b5ffb 100644 --- a/src/TUnit.Analyzers/PublicMethodMissingTestAttributeAnalyzer.cs +++ b/src/TUnit.Analyzers/PublicMethodMissingTestAttributeAnalyzer.cs @@ -54,6 +54,7 @@ private void AnalyzeSymbol(SymbolAnalysisContext context) .Where(x => !IsDisposableDispose(x)) .Where(x => !IsAsyncDisposableDispose(x)) .Where(x => !IsInitializeAsync(x)) + .Where(x => !ImplementsInterfaceMember(x)) .Where(x => !IsTestDataSource(x, testMethods))) { context.ReportDiagnostic(Diagnostic.Create(Rules.PublicMethodMissingTestAttribute, method.Locations.FirstOrDefault())); @@ -74,6 +75,26 @@ private bool IsTestDataSource(IMethodSymbol methodSymbol, IMethodSymbol[] testMe return false; } + // A public method that implicitly implements an interface member (e.g. ITestStartEventReceiver.OnTestStart) + // cannot be made private, so the diagnostic's advice would not apply. + private static bool ImplementsInterfaceMember(IMethodSymbol method) + { + var containingType = method.ContainingType; + + foreach (var @interface in containingType.AllInterfaces) + { + foreach (var member in @interface.GetMembers(method.Name)) + { + if (SymbolEqualityComparer.Default.Equals(containingType.FindImplementationForInterfaceMember(member), method)) + { + return true; + } + } + } + + return false; + } + private bool IsDisposableDispose(IMethodSymbol method) { return method is { ReturnsVoid: true, Name: "Dispose" } && diff --git a/tests/TUnit.Analyzers.Tests/PublicMethodMissingTestAttributeAnalyzerTests.cs b/tests/TUnit.Analyzers.Tests/PublicMethodMissingTestAttributeAnalyzerTests.cs index f09738aa610..2ee35f52daf 100644 --- a/tests/TUnit.Analyzers.Tests/PublicMethodMissingTestAttributeAnalyzerTests.cs +++ b/tests/TUnit.Analyzers.Tests/PublicMethodMissingTestAttributeAnalyzerTests.cs @@ -153,6 +153,81 @@ public ValueTask DisposeAsync() ); } + [Test] + public async Task Interface_Implementation_No_Error() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System.Threading.Tasks; + using TUnit.Core; + using TUnit.Core.Interfaces; + + public interface IGreeter + { + T Greet(); + } + + public class MyClass : ITestStartEventReceiver, IGreeter + { + public int Order => 0; + + [Test] + public void MyTest() + { + } + + public ValueTask OnTestStart(TestContext context) + { + return default; + } + + public string Greet() + { + return "Hello"; + } + } + """ + ); + } + + [Test] + public async Task Same_Name_As_Interface_Member_But_Not_Implementing_It_Error() + { + var expected = Verifier.Diagnostic(Rules.PublicMethodMissingTestAttribute).WithLocation(0); + + await Verifier + .VerifyAnalyzerAsync( + """ + using TUnit.Core; + + public interface IGreeter + { + string Greet(); + } + + public class MyClass : IGreeter + { + [Test] + public void MyTest() + { + } + + string IGreeter.Greet() + { + return "Hello"; + } + + public string {|#0:Greet|}(string name) + { + return name; + } + } + """, + expected + ); + } + [Test] public async Task Property_Getter_Setter_No_Error() { From 15a2d86844d78eabd1ace1713492901e5ffd2ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 06:32:12 +0000 Subject: [PATCH 16/49] ci: run the assertions generator snapshot tests and core net9.0 snapshots - TUnit.Assertions.SourceGenerator.Tests (70 snapshot tests per TFM) had no pipeline module, so CI never ran it; that is how a wrong HttpStatusCode snapshot and the "Expected be ..." message bug went unnoticed. Add a module mirroring RunSourceGeneratorTestsModule. - RunSourceGeneratorTestsModule says to stay aligned with the project's TargetFrameworks but skipped net9.0, leaving the DotNet9_0 snapshots unchecked. Add it. --- ...RunAssertionsSourceGeneratorTestsModule.cs | 52 +++++++++++++++++++ .../Modules/RunSourceGeneratorTestsModule.cs | 1 + 2 files changed, 53 insertions(+) create mode 100644 tools/TUnit.Pipeline/Modules/RunAssertionsSourceGeneratorTestsModule.cs diff --git a/tools/TUnit.Pipeline/Modules/RunAssertionsSourceGeneratorTestsModule.cs b/tools/TUnit.Pipeline/Modules/RunAssertionsSourceGeneratorTestsModule.cs new file mode 100644 index 00000000000..55460f337c4 --- /dev/null +++ b/tools/TUnit.Pipeline/Modules/RunAssertionsSourceGeneratorTestsModule.cs @@ -0,0 +1,52 @@ +using ModularPipelines.Attributes; +using ModularPipelines.Context; +using ModularPipelines.DotNet.Options; +using ModularPipelines.Extensions; +using ModularPipelines.Git.Extensions; +using ModularPipelines.Options; +using TUnit.Pipeline.Modules.Abstract; + +namespace TUnit.Pipeline.Modules; + +[NotInParallel("SnapshotTests")] +public class RunAssertionsSourceGeneratorTestsModule : TestBaseModule +{ + // Generator output snapshots verify behaviour across consumer TFMs — keep aligned with + // in TUnit.Assertions.SourceGenerator.Tests.csproj. + protected override IEnumerable TestableFrameworks + { + get + { + yield return "net10.0"; + yield return "net9.0"; + yield return "net8.0"; + + if (IsWindows) + { + yield return "net472"; + } + } + } + + protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken) + { + var project = context.Git().RootDirectory.FindFile(x => x.Name == "TUnit.Assertions.SourceGenerator.Tests.csproj").AssertExists(); + + return Task.FromResult<(DotNetRunOptions, CommandExecutionOptions?)>(( + new DotNetRunOptions + { + NoBuild = true, + Configuration = "Release", + Framework = framework, + }, + new CommandExecutionOptions + { + WorkingDirectory = project.Folder!.Path, + EnvironmentVariables = new Dictionary + { + ["DISABLE_GITHUB_REPORTER"] = "true", + } + } + )); + } +} diff --git a/tools/TUnit.Pipeline/Modules/RunSourceGeneratorTestsModule.cs b/tools/TUnit.Pipeline/Modules/RunSourceGeneratorTestsModule.cs index 33d670bacf0..a69661249ad 100644 --- a/tools/TUnit.Pipeline/Modules/RunSourceGeneratorTestsModule.cs +++ b/tools/TUnit.Pipeline/Modules/RunSourceGeneratorTestsModule.cs @@ -18,6 +18,7 @@ protected override IEnumerable TestableFrameworks get { yield return "net10.0"; + yield return "net9.0"; yield return "net8.0"; if (IsWindows) From da3b1391668459fb15a40da954a975f3a9566f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 06:45:12 +0000 Subject: [PATCH 17/49] test: reference the test host's deployment, not its loaded assemblies Five generator test projects built their in-memory compilations from AppDomain.CurrentDomain.GetAssemblies(), i.e. whatever the process had loaded so far. That set depends on how the tests are launched (IDE and `dotnet test` server-mode hosts load assemblies a plain run does not), which is how HttpStatusCode resolved in one mode and not the other (see the HttpStatusCode snapshot fix earlier in this branch). The Should tests' comment even assumed it reflected TRUSTED_PLATFORM_ASSEMBLIES; it does not. Add a shared TestHostReferences helper: on .NET it returns the TRUSTED_PLATFORM_ASSEMBLIES list (framework + deps.json, fixed by the build); .NET Framework has no such list and keeps the loaded assemblies, so the Windows-only Net4_7 snapshots are unaffected. Drops the now redundant explicit HttpStatusCode reference. No snapshot changes; all five projects pass on net8/9/10 via `dotnet test` and as plain runs. --- tests/SharedTestHelpers/TestHostReferences.cs | 34 +++++++++++++++++++ .../ShouldExtensionGeneratorTests.cs | 14 ++------ ...rtions.Should.SourceGenerator.Tests.csproj | 4 +++ .../ReferencesHelper.cs | 9 ++--- ...it.Assertions.SourceGenerator.Tests.csproj | 4 +++ .../ReferencesHelper.cs | 6 ++-- .../TUnit.Core.SourceGenerator.Tests.csproj | 4 +++ .../SnapshotTestBase.cs | 11 +++--- .../TUnit.Mocks.SourceGenerator.Tests.csproj | 4 +++ .../Fixture.cs | 24 +++++-------- ...it.SourceGenerator.IncrementalTests.csproj | 4 +++ 11 files changed, 77 insertions(+), 41 deletions(-) create mode 100644 tests/SharedTestHelpers/TestHostReferences.cs diff --git a/tests/SharedTestHelpers/TestHostReferences.cs b/tests/SharedTestHelpers/TestHostReferences.cs new file mode 100644 index 00000000000..80ca7c23222 --- /dev/null +++ b/tests/SharedTestHelpers/TestHostReferences.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace TUnit.Tests.Shared; + +/// +/// Paths of the assemblies that in-memory test compilations (generator and analyzer tests) reference. +/// +/// +/// AppDomain.CurrentDomain.GetAssemblies() only returns what the process has loaded so far, which depends on +/// how the test host was launched: IDE and dotnet test server-mode hosts load assemblies (e.g. System.Net.*) +/// that a plain run does not, so types such as System.Net.HttpStatusCode resolved or not and generator output +/// differed between launches. On .NET, TRUSTED_PLATFORM_ASSEMBLIES lists the app's whole deployment +/// (framework plus deps.json) up front. .NET Framework has no such list, so it keeps the loaded assemblies. +/// +internal static class TestHostReferences +{ + public static IReadOnlyList AssemblyPaths { get; } = Resolve(); + + private static string[] Resolve() + { + if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string trustedPlatformAssemblies) + { + return trustedPlatformAssemblies.Split(new[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries); + } + + return AppDomain.CurrentDomain.GetAssemblies() + .Where(assembly => !assembly.IsDynamic && !string.IsNullOrWhiteSpace(assembly.Location)) + .Select(assembly => assembly.Location) + .ToArray(); + } +} diff --git a/tests/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.cs b/tests/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.cs index 3d51d753678..2477f45b35a 100644 --- a/tests/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.cs +++ b/tests/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.cs @@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using TUnit.Assertions.Core; +using TUnit.Tests.Shared; namespace TUnit.Assertions.Should.SourceGenerator.Tests; @@ -473,18 +474,9 @@ await Verify(combined) private static IEnumerable GetReferences() { - // Mirror the loaded assemblies of the test process. Works on both .NET Core - // (where TRUSTED_PLATFORM_ASSEMBLIES populates this set) and .NET Framework, - // unlike AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") which returns - // null on .NET Framework and would leave the in-memory compilation without BCL - // references — making symbol resolution silently fail in the generator. - foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + foreach (var path in TestHostReferences.AssemblyPaths) { - if (asm.IsDynamic || string.IsNullOrWhiteSpace(asm.Location)) - { - continue; - } - yield return MetadataReference.CreateFromFile(asm.Location); + yield return MetadataReference.CreateFromFile(path); } yield return MetadataReference.CreateFromFile(typeof(Assertion<>).Assembly.Location); diff --git a/tests/TUnit.Assertions.Should.SourceGenerator.Tests/TUnit.Assertions.Should.SourceGenerator.Tests.csproj b/tests/TUnit.Assertions.Should.SourceGenerator.Tests/TUnit.Assertions.Should.SourceGenerator.Tests.csproj index adf9e190815..4da6334b299 100644 --- a/tests/TUnit.Assertions.Should.SourceGenerator.Tests/TUnit.Assertions.Should.SourceGenerator.Tests.csproj +++ b/tests/TUnit.Assertions.Should.SourceGenerator.Tests/TUnit.Assertions.Should.SourceGenerator.Tests.csproj @@ -17,6 +17,10 @@ $(NoWarn);MSB3277 + + + + diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/ReferencesHelper.cs b/tests/TUnit.Assertions.SourceGenerator.Tests/ReferencesHelper.cs index c0d8cf916ea..55779c08397 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/ReferencesHelper.cs +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/ReferencesHelper.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; +using TUnit.Tests.Shared; namespace TUnit.Assertions.SourceGenerator.Tests; @@ -7,9 +8,8 @@ namespace TUnit.Assertions.SourceGenerator.Tests; internal class ReferencesHelper { public static readonly List References = - AppDomain.CurrentDomain.GetAssemblies() - .Where(x => !x.IsDynamic && !string.IsNullOrWhiteSpace(x.Location)) - .Select(x => MetadataReference.CreateFromFile(x.Location)) + TestHostReferences.AssemblyPaths + .Select(path => MetadataReference.CreateFromFile(path)) .Concat([ // add your app/lib specifics, e.g.: MetadataReference.CreateFromFile(typeof(Attribute).Assembly.Location), @@ -17,9 +17,6 @@ internal class ReferencesHelper MetadataReference.CreateFromFile(typeof(Polyfill).Assembly.Location), MetadataReference.CreateFromFile(typeof(System.Runtime.CompilerServices.CallerArgumentExpressionAttribute).Assembly.Location), MetadataReference.CreateFromFile(typeof(Uri).Assembly.Location), - // Not loaded by a plain test run, but loaded by IDE/`dotnet test` server-mode hosts, so it must - // be explicit or HttpStatusCode resolves differently depending on how the tests are launched. - MetadataReference.CreateFromFile(typeof(System.Net.HttpStatusCode).Assembly.Location), ]) .ToList(); } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/TUnit.Assertions.SourceGenerator.Tests.csproj b/tests/TUnit.Assertions.SourceGenerator.Tests/TUnit.Assertions.SourceGenerator.Tests.csproj index fed222aa83c..134fdc2ab4d 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/TUnit.Assertions.SourceGenerator.Tests.csproj +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/TUnit.Assertions.SourceGenerator.Tests.csproj @@ -13,6 +13,10 @@ $(NoWarn);MSB3277 + + + + diff --git a/tests/TUnit.Core.SourceGenerator.Tests/ReferencesHelper.cs b/tests/TUnit.Core.SourceGenerator.Tests/ReferencesHelper.cs index bb6cd3706dd..3823028505c 100644 --- a/tests/TUnit.Core.SourceGenerator.Tests/ReferencesHelper.cs +++ b/tests/TUnit.Core.SourceGenerator.Tests/ReferencesHelper.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; +using TUnit.Tests.Shared; namespace TUnit.Core.SourceGenerator.Tests; @@ -7,9 +8,8 @@ namespace TUnit.Core.SourceGenerator.Tests; internal class ReferencesHelper { public static readonly List References = - AppDomain.CurrentDomain.GetAssemblies() - .Where(x => !x.IsDynamic && !string.IsNullOrWhiteSpace(x.Location)) - .Select(x => MetadataReference.CreateFromFile(x.Location)) + TestHostReferences.AssemblyPaths + .Select(path => MetadataReference.CreateFromFile(path)) .Concat([ // add your app/lib specifics, e.g.: MetadataReference.CreateFromFile(typeof(Attribute).Assembly.Location), diff --git a/tests/TUnit.Core.SourceGenerator.Tests/TUnit.Core.SourceGenerator.Tests.csproj b/tests/TUnit.Core.SourceGenerator.Tests/TUnit.Core.SourceGenerator.Tests.csproj index c6f29e9fe09..ac6312ae986 100644 --- a/tests/TUnit.Core.SourceGenerator.Tests/TUnit.Core.SourceGenerator.Tests.csproj +++ b/tests/TUnit.Core.SourceGenerator.Tests/TUnit.Core.SourceGenerator.Tests.csproj @@ -13,6 +13,10 @@ $(NoWarn);MSB3277 + + + + diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/SnapshotTestBase.cs b/tests/TUnit.Mocks.SourceGenerator.Tests/SnapshotTestBase.cs index 99eb4a14c32..e66f0cea7c1 100644 --- a/tests/TUnit.Mocks.SourceGenerator.Tests/SnapshotTestBase.cs +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/SnapshotTestBase.cs @@ -2,6 +2,7 @@ using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using TUnit.Tests.Shared; namespace TUnit.Mocks.SourceGenerator.Tests; @@ -16,7 +17,7 @@ public abstract class SnapshotTestBase /// /// Returns the shared, lazily-loaded set of metadata references used by the test compilations - /// (current AppDomain assemblies, with exactly one TUnit.Mocks reference). Derived test classes + /// (the test host's assemblies, with exactly one TUnit.Mocks reference). Derived test classes /// should reuse this instead of re-discovering references per test invocation. /// protected static IEnumerable GetCachedReferences() => _references.Value; @@ -38,10 +39,10 @@ private static List LoadReferences() "No TUnit.Mocks reference available to the test compilations — the generator would silently produce nothing."); } - var refs = AppDomain.CurrentDomain.GetAssemblies() - .Where(a => !a.IsDynamic && !string.IsNullOrWhiteSpace(a.Location)) - .Where(a => a != mocksAssembly) - .Select(a => MetadataReference.CreateFromFile(a.Location)) + var mocksFileName = Path.GetFileName(mocksAssembly.Location); + var refs = TestHostReferences.AssemblyPaths + .Where(path => !string.Equals(Path.GetFileName(path), mocksFileName, StringComparison.OrdinalIgnoreCase)) + .Select(path => MetadataReference.CreateFromFile(path)) .ToList(); refs.Add(MetadataReference.CreateFromFile(mocksAssembly.Location)); diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/TUnit.Mocks.SourceGenerator.Tests.csproj b/tests/TUnit.Mocks.SourceGenerator.Tests/TUnit.Mocks.SourceGenerator.Tests.csproj index f495e7553e9..c2856892c32 100644 --- a/tests/TUnit.Mocks.SourceGenerator.Tests/TUnit.Mocks.SourceGenerator.Tests.csproj +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/TUnit.Mocks.SourceGenerator.Tests.csproj @@ -2,6 +2,10 @@ + + + + diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/Fixture.cs b/tests/TUnit.SourceGenerator.IncrementalTests/Fixture.cs index 569f42120cc..6bd7f9eecde 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/Fixture.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/Fixture.cs @@ -4,6 +4,7 @@ using Microsoft.CodeAnalysis.CSharp; using TUnit.Assertions.Attributes; using TUnit.Core; +using TUnit.Tests.Shared; namespace TUnit.Assertions.SourceGenerator.IncrementalTests; @@ -19,13 +20,10 @@ public static class Fixture typeof(TestAttribute).Assembly, }; - public static Assembly[] AssemblyReferencesForCodegen => - AppDomain - .CurrentDomain.GetAssemblies() - .Concat(ImportantAssemblies) - .Distinct() - .Where(a => !a.IsDynamic) - .ToArray(); + private static readonly string[] AssemblyPathsForCodegen = TestHostReferences.AssemblyPaths + .Concat(ImportantAssemblies.Select(static assembly => assembly.Location)) + .Distinct() + .ToArray(); public static DirectoryInfo GetSolutionDirectoryInfo() { @@ -43,15 +41,9 @@ public static CSharpCompilation CreateLibrary(params string[] source) => public static CSharpCompilation CreateLibrary(params SyntaxTree[] source) { - var references = new List(); - var assemblies = AssemblyReferencesForCodegen; - foreach (Assembly assembly in assemblies) - { - if (!assembly.IsDynamic) - { - references.Add(MetadataReference.CreateFromFile(assembly.Location)); - } - } + var references = AssemblyPathsForCodegen + .Select(static path => (MetadataReference)MetadataReference.CreateFromFile(path)) + .ToList(); var compilation = CSharpCompilation.Create( "Library", diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/TUnit.SourceGenerator.IncrementalTests.csproj b/tests/TUnit.SourceGenerator.IncrementalTests/TUnit.SourceGenerator.IncrementalTests.csproj index aab42375c2a..6d63e87c9b1 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/TUnit.SourceGenerator.IncrementalTests.csproj +++ b/tests/TUnit.SourceGenerator.IncrementalTests/TUnit.SourceGenerator.IncrementalTests.csproj @@ -12,6 +12,10 @@ TUnit.Assertions.SourceGenerator.IncrementalTests + + + + From cac343efde597dc072278bf39bef8bd225d85e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 06:45:43 +0000 Subject: [PATCH 18/49] ci: run test projects on every framework they target Most test modules ran net10.0 only (TestBaseModule's default), Mocks.Http, Mocks.Logging and AspNetCore.Tests ran net10.0+net8.0, and the five analyzer modules ran a single hard-coded TFM via `dotnet test` (net8.0, or net9.0 for AspNetCore analyzers), although their projects build net8.0/net9.0/net10.0 and some contain TFM-specific tests (e.g. TUnit.Assertions.Tests has a different test set on net8.0). - TestBaseModule now defaults to net10.0, net9.0 and net8.0; frameworks a project did not build are already skipped, so net10.0-only projects still run once. - Drop the net10.0+net8.0 overrides. - Move the analyzer modules onto TestBaseModule, keeping their hang dump options with a per-framework dump file name. --- .../Modules/Abstract/TestBaseModule.cs | 12 +++++- .../Modules/RunAnalyzersTestsModule.cs | 41 ++++++++----------- .../RunAspNetCoreAnalyzersTestsModule.cs | 39 ++++++++---------- .../Modules/RunAspNetCoreTestsModule.cs | 9 ---- .../RunAssertionsAnalyzersTestsModule.cs | 41 ++++++++----------- .../RunAssertionsCodeFixersTestsModule.cs | 41 ++++++++----------- .../RunInternalAnalyzersTestsModule.cs | 41 ++++++++----------- .../Modules/RunMockHttpTestsModule.cs | 9 ---- .../Modules/RunMockLoggingTestsModule.cs | 9 ---- 9 files changed, 95 insertions(+), 147 deletions(-) diff --git a/tools/TUnit.Pipeline/Modules/Abstract/TestBaseModule.cs b/tools/TUnit.Pipeline/Modules/Abstract/TestBaseModule.cs index df297f5e2b8..c846859e2e6 100644 --- a/tools/TUnit.Pipeline/Modules/Abstract/TestBaseModule.cs +++ b/tools/TUnit.Pipeline/Modules/Abstract/TestBaseModule.cs @@ -11,9 +11,19 @@ namespace TUnit.Pipeline.Modules.Abstract; public abstract class TestBaseModule : Module> { + /// + /// Frameworks to run. Defaults to every modern TFM the repo targets; frameworks the project did not + /// build are skipped (see ), so net10.0-only projects run once. + /// Override only to add a legacy TFM (net472 on Windows) or to deliberately narrow the set. + /// protected virtual IEnumerable TestableFrameworks { - get { yield return "net10.0"; } + get + { + yield return "net10.0"; + yield return "net9.0"; + yield return "net8.0"; + } } /// True on Windows, where legacy .NET Framework TFMs (net4xx) can be tested. diff --git a/tools/TUnit.Pipeline/Modules/RunAnalyzersTestsModule.cs b/tools/TUnit.Pipeline/Modules/RunAnalyzersTestsModule.cs index d487d0e7db5..18a6275aa13 100644 --- a/tools/TUnit.Pipeline/Modules/RunAnalyzersTestsModule.cs +++ b/tools/TUnit.Pipeline/Modules/RunAnalyzersTestsModule.cs @@ -1,41 +1,34 @@ -using ModularPipelines.Attributes; -using ModularPipelines.Context; -using ModularPipelines.DotNet.Extensions; +using ModularPipelines.Context; using ModularPipelines.DotNet.Options; using ModularPipelines.Extensions; using ModularPipelines.Git.Extensions; -using ModularPipelines.Models; -using ModularPipelines.Modules; using ModularPipelines.Options; +using TUnit.Pipeline.Modules.Abstract; namespace TUnit.Pipeline.Modules; -public class RunAnalyzersTestsModule : Module +public class RunAnalyzersTestsModule : TestBaseModule { - protected override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken) { var project = context.Git().RootDirectory.FindFile(x => x.Name == "TUnit.Analyzers.Tests.csproj").AssertExists(); - return await context.DotNet().Test(new DotNetTestOptions - { - NoBuild = true, - Configuration = "Release", - Framework = "net8.0", - Arguments = ["--", "--hangdump", "--hangdump-filename", "hangdump.analyzers-tests.dmp", "--hangdump-timeout", "5m"], - }, new CommandExecutionOptions - { - WorkingDirectory = project.Folder!.Path, - EnvironmentVariables = new Dictionary + return Task.FromResult<(DotNetRunOptions, CommandExecutionOptions?)>(( + new DotNetRunOptions { - ["DISABLE_GITHUB_REPORTER"] = "true", + NoBuild = true, + Configuration = "Release", + Framework = framework, + Arguments = ["--hangdump", "--hangdump-filename", $"hangdump.analyzers-tests-{framework}.dmp", "--hangdump-timeout", "5m"], }, - LogSettings = new CommandLoggingOptions + new CommandExecutionOptions { - ShowCommandArguments = true, - ShowStandardError = true, - ShowExecutionTime = true, - ShowExitCode = true + WorkingDirectory = project.Folder!.Path, + EnvironmentVariables = new Dictionary + { + ["DISABLE_GITHUB_REPORTER"] = "true", + } } - }, cancellationToken); + )); } } diff --git a/tools/TUnit.Pipeline/Modules/RunAspNetCoreAnalyzersTestsModule.cs b/tools/TUnit.Pipeline/Modules/RunAspNetCoreAnalyzersTestsModule.cs index b1e399ab4d1..7589a8b9567 100644 --- a/tools/TUnit.Pipeline/Modules/RunAspNetCoreAnalyzersTestsModule.cs +++ b/tools/TUnit.Pipeline/Modules/RunAspNetCoreAnalyzersTestsModule.cs @@ -1,41 +1,34 @@ -using ModularPipelines.Attributes; using ModularPipelines.Context; -using ModularPipelines.DotNet.Extensions; using ModularPipelines.DotNet.Options; using ModularPipelines.Extensions; using ModularPipelines.Git.Extensions; -using ModularPipelines.Models; -using ModularPipelines.Modules; using ModularPipelines.Options; +using TUnit.Pipeline.Modules.Abstract; namespace TUnit.Pipeline.Modules; -public class RunAspNetCoreAnalyzersTestsModule : Module +public class RunAspNetCoreAnalyzersTestsModule : TestBaseModule { - protected override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken) { var project = context.Git().RootDirectory.FindFile(x => x.Name == "TUnit.AspNetCore.Analyzers.Tests.csproj").AssertExists(); - return await context.DotNet().Test(new DotNetTestOptions - { - NoBuild = true, - Configuration = "Release", - Framework = "net9.0", - Arguments = ["--", "--hangdump", "--hangdump-filename", "hangdump.aspnetcore-analyzers-tests.dmp", "--hangdump-timeout", "5m"], - }, new CommandExecutionOptions - { - WorkingDirectory = project.Folder!.Path, - EnvironmentVariables = new Dictionary + return Task.FromResult<(DotNetRunOptions, CommandExecutionOptions?)>(( + new DotNetRunOptions { - ["DISABLE_GITHUB_REPORTER"] = "true", + NoBuild = true, + Configuration = "Release", + Framework = framework, + Arguments = ["--hangdump", "--hangdump-filename", $"hangdump.aspnetcore-analyzers-tests-{framework}.dmp", "--hangdump-timeout", "5m"], }, - LogSettings = new CommandLoggingOptions + new CommandExecutionOptions { - ShowCommandArguments = true, - ShowStandardError = true, - ShowExecutionTime = true, - ShowExitCode = true + WorkingDirectory = project.Folder!.Path, + EnvironmentVariables = new Dictionary + { + ["DISABLE_GITHUB_REPORTER"] = "true", + } } - }, cancellationToken); + )); } } diff --git a/tools/TUnit.Pipeline/Modules/RunAspNetCoreTestsModule.cs b/tools/TUnit.Pipeline/Modules/RunAspNetCoreTestsModule.cs index f4c3187bcf5..8eba3e73b51 100644 --- a/tools/TUnit.Pipeline/Modules/RunAspNetCoreTestsModule.cs +++ b/tools/TUnit.Pipeline/Modules/RunAspNetCoreTestsModule.cs @@ -9,15 +9,6 @@ namespace TUnit.Pipeline.Modules; public class RunAspNetCoreTestsModule : TestBaseModule { - protected override IEnumerable TestableFrameworks - { - get - { - yield return "net10.0"; - yield return "net8.0"; - } - } - protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken) { var project = context.Git().RootDirectory.FindFile(x => x.Name == "TUnit.AspNetCore.Tests.csproj").AssertExists(); diff --git a/tools/TUnit.Pipeline/Modules/RunAssertionsAnalyzersTestsModule.cs b/tools/TUnit.Pipeline/Modules/RunAssertionsAnalyzersTestsModule.cs index bd51ada274e..70b3565d8fd 100644 --- a/tools/TUnit.Pipeline/Modules/RunAssertionsAnalyzersTestsModule.cs +++ b/tools/TUnit.Pipeline/Modules/RunAssertionsAnalyzersTestsModule.cs @@ -1,41 +1,34 @@ -using ModularPipelines.Attributes; -using ModularPipelines.Context; -using ModularPipelines.DotNet.Extensions; +using ModularPipelines.Context; using ModularPipelines.DotNet.Options; using ModularPipelines.Extensions; using ModularPipelines.Git.Extensions; -using ModularPipelines.Models; -using ModularPipelines.Modules; using ModularPipelines.Options; +using TUnit.Pipeline.Modules.Abstract; namespace TUnit.Pipeline.Modules; -public class RunAssertionsAnalyzersTestsModule : Module +public class RunAssertionsAnalyzersTestsModule : TestBaseModule { - protected override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken) { var project = context.Git().RootDirectory.FindFile(x => x.Name == "TUnit.Assertions.Analyzers.Tests.csproj").AssertExists(); - return await context.DotNet().Test(new DotNetTestOptions - { - NoBuild = true, - Configuration = "Release", - Framework = "net8.0", - Arguments = ["--", "--hangdump", "--hangdump-filename", "hangdump.assertions-analyzers-tests.dmp", "--hangdump-timeout", "5m"], - }, new CommandExecutionOptions - { - WorkingDirectory = project.Folder!.Path, - EnvironmentVariables = new Dictionary + return Task.FromResult<(DotNetRunOptions, CommandExecutionOptions?)>(( + new DotNetRunOptions { - ["DISABLE_GITHUB_REPORTER"] = "true", + NoBuild = true, + Configuration = "Release", + Framework = framework, + Arguments = ["--hangdump", "--hangdump-filename", $"hangdump.assertions-analyzers-tests-{framework}.dmp", "--hangdump-timeout", "5m"], }, - LogSettings = new CommandLoggingOptions + new CommandExecutionOptions { - ShowCommandArguments = true, - ShowStandardError = true, - ShowExecutionTime = true, - ShowExitCode = true + WorkingDirectory = project.Folder!.Path, + EnvironmentVariables = new Dictionary + { + ["DISABLE_GITHUB_REPORTER"] = "true", + } } - }, cancellationToken); + )); } } diff --git a/tools/TUnit.Pipeline/Modules/RunAssertionsCodeFixersTestsModule.cs b/tools/TUnit.Pipeline/Modules/RunAssertionsCodeFixersTestsModule.cs index 55076284676..059d3b5ddc2 100644 --- a/tools/TUnit.Pipeline/Modules/RunAssertionsCodeFixersTestsModule.cs +++ b/tools/TUnit.Pipeline/Modules/RunAssertionsCodeFixersTestsModule.cs @@ -1,41 +1,34 @@ -using ModularPipelines.Attributes; -using ModularPipelines.Context; -using ModularPipelines.DotNet.Extensions; +using ModularPipelines.Context; using ModularPipelines.DotNet.Options; using ModularPipelines.Extensions; using ModularPipelines.Git.Extensions; -using ModularPipelines.Models; -using ModularPipelines.Modules; using ModularPipelines.Options; +using TUnit.Pipeline.Modules.Abstract; namespace TUnit.Pipeline.Modules; -public class RunAssertionsCodeFixersTestsModule : Module +public class RunAssertionsCodeFixersTestsModule : TestBaseModule { - protected override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken) { var project = context.Git().RootDirectory.FindFile(x => x.Name == "TUnit.Assertions.Analyzers.CodeFixers.Tests.csproj").AssertExists(); - return await context.DotNet().Test(new DotNetTestOptions - { - NoBuild = true, - Configuration = "Release", - Framework = "net8.0", - Arguments = ["--", "--hangdump", "--hangdump-filename", "hangdump.assertions-codefixers-tests.dmp", "--hangdump-timeout", "5m"], - }, new CommandExecutionOptions - { - WorkingDirectory = project.Folder!.Path, - EnvironmentVariables = new Dictionary + return Task.FromResult<(DotNetRunOptions, CommandExecutionOptions?)>(( + new DotNetRunOptions { - ["DISABLE_GITHUB_REPORTER"] = "true", + NoBuild = true, + Configuration = "Release", + Framework = framework, + Arguments = ["--hangdump", "--hangdump-filename", $"hangdump.assertions-codefixers-tests-{framework}.dmp", "--hangdump-timeout", "5m"], }, - LogSettings = new CommandLoggingOptions + new CommandExecutionOptions { - ShowCommandArguments = true, - ShowStandardError = true, - ShowExecutionTime = true, - ShowExitCode = true + WorkingDirectory = project.Folder!.Path, + EnvironmentVariables = new Dictionary + { + ["DISABLE_GITHUB_REPORTER"] = "true", + } } - }, cancellationToken); + )); } } diff --git a/tools/TUnit.Pipeline/Modules/RunInternalAnalyzersTestsModule.cs b/tools/TUnit.Pipeline/Modules/RunInternalAnalyzersTestsModule.cs index 5d9014a0944..8dc99b5f58b 100644 --- a/tools/TUnit.Pipeline/Modules/RunInternalAnalyzersTestsModule.cs +++ b/tools/TUnit.Pipeline/Modules/RunInternalAnalyzersTestsModule.cs @@ -1,41 +1,34 @@ -using ModularPipelines.Attributes; -using ModularPipelines.Context; -using ModularPipelines.DotNet.Extensions; +using ModularPipelines.Context; using ModularPipelines.DotNet.Options; using ModularPipelines.Extensions; using ModularPipelines.Git.Extensions; -using ModularPipelines.Models; -using ModularPipelines.Modules; using ModularPipelines.Options; +using TUnit.Pipeline.Modules.Abstract; namespace TUnit.Pipeline.Modules; -public class RunInternalAnalyzersTestsModule : Module +public class RunInternalAnalyzersTestsModule : TestBaseModule { - protected override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken) { var project = context.Git().RootDirectory.FindFile(x => x.Name == "TUnit.Analyzers.Internal.Tests.csproj").AssertExists(); - return await context.DotNet().Test(new DotNetTestOptions - { - NoBuild = true, - Configuration = "Release", - Framework = "net8.0", - Arguments = ["--", "--hangdump", "--hangdump-filename", "hangdump.internal-analyzers-tests.dmp", "--hangdump-timeout", "5m"], - }, new CommandExecutionOptions - { - WorkingDirectory = project.Folder!.Path, - EnvironmentVariables = new Dictionary + return Task.FromResult<(DotNetRunOptions, CommandExecutionOptions?)>(( + new DotNetRunOptions { - ["DISABLE_GITHUB_REPORTER"] = "true", + NoBuild = true, + Configuration = "Release", + Framework = framework, + Arguments = ["--hangdump", "--hangdump-filename", $"hangdump.internal-analyzers-tests-{framework}.dmp", "--hangdump-timeout", "5m"], }, - LogSettings = new CommandLoggingOptions + new CommandExecutionOptions { - ShowCommandArguments = true, - ShowStandardError = true, - ShowExecutionTime = true, - ShowExitCode = true + WorkingDirectory = project.Folder!.Path, + EnvironmentVariables = new Dictionary + { + ["DISABLE_GITHUB_REPORTER"] = "true", + } } - }, cancellationToken); + )); } } diff --git a/tools/TUnit.Pipeline/Modules/RunMockHttpTestsModule.cs b/tools/TUnit.Pipeline/Modules/RunMockHttpTestsModule.cs index d8798b6c173..ae0bd8f8792 100644 --- a/tools/TUnit.Pipeline/Modules/RunMockHttpTestsModule.cs +++ b/tools/TUnit.Pipeline/Modules/RunMockHttpTestsModule.cs @@ -9,15 +9,6 @@ namespace TUnit.Pipeline.Modules; public class RunMockHttpTestsModule : TestBaseModule { - protected override IEnumerable TestableFrameworks - { - get - { - yield return "net10.0"; - yield return "net8.0"; - } - } - protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken) { var project = context.Git().RootDirectory.FindFile(x => x.Name == "TUnit.Mocks.Http.Tests.csproj").AssertExists(); diff --git a/tools/TUnit.Pipeline/Modules/RunMockLoggingTestsModule.cs b/tools/TUnit.Pipeline/Modules/RunMockLoggingTestsModule.cs index 022f9a5dad2..c7cd93cb544 100644 --- a/tools/TUnit.Pipeline/Modules/RunMockLoggingTestsModule.cs +++ b/tools/TUnit.Pipeline/Modules/RunMockLoggingTestsModule.cs @@ -9,15 +9,6 @@ namespace TUnit.Pipeline.Modules; public class RunMockLoggingTestsModule : TestBaseModule { - protected override IEnumerable TestableFrameworks - { - get - { - yield return "net10.0"; - yield return "net8.0"; - } - } - protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken) { var project = context.Git().RootDirectory.FindFile(x => x.Name == "TUnit.Mocks.Logging.Tests.csproj").AssertExists(); From 52c4341439248d638cecf8de983bdbcddd8cb74c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 14:02:26 +0000 Subject: [PATCH 19/49] test: move orphaned ArgumentFormatter tests into TUnit.UnitTests, drop dead files - tests/TUnit.Core.Tests and tests/TUnit.IntegrationTests held test files but no project, so they were never compiled or run (the former used NUnit's Assert.That(x, Is.EqualTo(...)) and an invalid `var` tuple with a null element). ArgumentFormatter had no other tests: port its 9 tests to TUnit.UnitTests, with expectations matching current output (bools render as ToString(), "True"; numbers in the current culture). Drop CleanArchitectureTests, whose tests waited 100 ms for another test to have run. - tests/Snapshots/TUnit._.verified held two BOM-only std-stream files that no test produces. --- .../TUnit._.verified/std-streams/stderr.txt | 1 - .../TUnit._.verified/std-streams/stdout.txt | 1 - .../Helpers/ArgumentFormatterTests.cs | 96 ------ .../CleanArchitectureTests.cs | 279 ------------------ .../TUnit.UnitTests/ArgumentFormatterTests.cs | 97 ++++++ 5 files changed, 97 insertions(+), 377 deletions(-) delete mode 100644 tests/Snapshots/TUnit._.verified/std-streams/stderr.txt delete mode 100644 tests/Snapshots/TUnit._.verified/std-streams/stdout.txt delete mode 100644 tests/TUnit.Core.Tests/Helpers/ArgumentFormatterTests.cs delete mode 100644 tests/TUnit.IntegrationTests/CleanArchitectureTests.cs create mode 100644 tests/TUnit.UnitTests/ArgumentFormatterTests.cs diff --git a/tests/Snapshots/TUnit._.verified/std-streams/stderr.txt b/tests/Snapshots/TUnit._.verified/std-streams/stderr.txt deleted file mode 100644 index 5f282702bb0..00000000000 --- a/tests/Snapshots/TUnit._.verified/std-streams/stderr.txt +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/tests/Snapshots/TUnit._.verified/std-streams/stdout.txt b/tests/Snapshots/TUnit._.verified/std-streams/stdout.txt deleted file mode 100644 index 5f282702bb0..00000000000 --- a/tests/Snapshots/TUnit._.verified/std-streams/stdout.txt +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/tests/TUnit.Core.Tests/Helpers/ArgumentFormatterTests.cs b/tests/TUnit.Core.Tests/Helpers/ArgumentFormatterTests.cs deleted file mode 100644 index e0df369cf34..00000000000 --- a/tests/TUnit.Core.Tests/Helpers/ArgumentFormatterTests.cs +++ /dev/null @@ -1,96 +0,0 @@ -using TUnit.Core.Helpers; - -namespace TUnit.Core.Tests.Helpers; - -public class ArgumentFormatterTests -{ - [Test] - public void FormatDefault_ValueTuple_FormatsCorrectly() - { - var tuple = (1, "test", true); - - var result = ArgumentFormatter.Format(tuple, []); - - Assert.That(result, Is.EqualTo("(1, test, true)")); - } - - [Test] - public void FormatDefault_NestedValueTuple_FormatsCorrectly() - { - var tuple = (1, (2, "inner"), true); - - var result = ArgumentFormatter.Format(tuple, []); - - Assert.That(result, Is.EqualTo("(1, (2, inner), true)")); - } - - [Test] - public void FormatDefault_SystemTuple_FormatsCorrectly() - { - var tuple = Tuple.Create(1, "test", false); - - var result = ArgumentFormatter.Format(tuple, []); - - Assert.That(result, Is.EqualTo("(1, test, false)")); - } - - [Test] - public void FormatArguments_WithTuples_FormatsCorrectly() - { - var args = new object?[] { (1, "a"), 42, (true, 3.14) }; - - var result = ArgumentFormatter.FormatArguments(args); - - Assert.That(result, Is.EqualTo("(1, a), 42, (true, 3.14)")); - } - - [Test] - public void FormatDefault_LargeTuple_FormatsAllElements() - { - var tuple = (1, 2, 3, 4, 5, 6, 7, 8); - - var result = ArgumentFormatter.Format(tuple, []); - - Assert.That(result, Is.EqualTo("(1, 2, 3, 4, 5, 6, 7, 8)")); - } - - [Test] - public void FormatDefault_TupleWithNull_HandlesNullCorrectly() - { - var tuple = (1, null, "test"); - - var result = ArgumentFormatter.Format(tuple, []); - - Assert.That(result, Is.EqualTo("(1, null, test)")); - } - - [Test] - public void FormatDefault_StringWithDots_EscapesDotsWithMiddleDot() - { - var stringWithDots = "1.2.3"; - - var result = ArgumentFormatter.Format(stringWithDots, []); - - Assert.That(result, Is.EqualTo("1·2·3")); - } - - [Test] - public void FormatDefault_StringWithoutDots_ReturnsUnchanged() - { - var stringWithoutDots = "hello world"; - - var result = ArgumentFormatter.Format(stringWithoutDots, []); - - Assert.That(result, Is.EqualTo("hello world")); - } - - [Test] - public void FormatArguments_WithStringsContainingDots_EscapesDots() - { - var args = new object?[] { "hello", "with.dot", "1.2.3" }; - - var result = ArgumentFormatter.FormatArguments(args); - - Assert.That(result, Is.EqualTo("hello, with·dot, 1·2·3")); - } -} \ No newline at end of file diff --git a/tests/TUnit.IntegrationTests/CleanArchitectureTests.cs b/tests/TUnit.IntegrationTests/CleanArchitectureTests.cs deleted file mode 100644 index 1e02201da74..00000000000 --- a/tests/TUnit.IntegrationTests/CleanArchitectureTests.cs +++ /dev/null @@ -1,279 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using TUnit.Assertions; -using TUnit.Core; - -namespace TUnit.IntegrationTests; - -/// -/// Integration tests to verify the clean architecture with TestMetadataGenerator and TestBuilder works correctly. -/// -public class CleanArchitectureTests -{ - [Test] - public async Task SimpleTest_ExecutesCorrectly() - { - // This test verifies that a simple test is discovered and executed - var executed = false; - SimpleTestMarker.WasExecuted = false; - - // The test should have been discovered by TestMetadataGenerator - // and executed by TestBuilder - await Task.Delay(100); // Give time for test execution - - await Assert.That(SimpleTestMarker.WasExecuted).IsTrue(); - } - - [Test] - [Arguments(1, 2, 3)] - [Arguments(4, 5, 6)] - public async Task ParameterizedTest_ReceivesCorrectArguments(int a, int b, int expected) - { - var sum = a + b; - await Assert.That(sum).IsEqualTo(expected); - } - - [Test] - [MethodDataSource(nameof(GetTestData))] - public async Task MethodDataSourceTest_ReceivesData(string input, int expected) - { - await Assert.That(input.Length).IsEqualTo(expected); - } - - public static IEnumerable<(string, int)> GetTestData() - { - yield return ("hello", 5); - yield return ("world", 5); - yield return ("test", 4); - } - - [Test] - [Repeat(3)] - public async Task RepeatedTest_ExecutesMultipleTimes() - { - RepeatTestCounter.Count++; - await Assert.That(RepeatTestCounter.Count).IsGreaterThanOrEqualTo(1); - } - - [Test] - [Skip("Testing skip functionality")] - public async Task SkippedTest_ShouldNotExecute() - { - SkipTestMarker.WasExecuted = true; - await Assert.That(false).IsTrue(); // Should never reach here - } - - [Test] - [Timeout(1000)] - public async Task TimeoutTest_HasCorrectTimeout() - { - // This test should complete within the timeout - await Task.Delay(100); - await Assert.That(true).IsTrue(); - } - - // Helper classes for test state - private static class SimpleTestMarker - { - public static bool WasExecuted { get; set; } - } - - private static class RepeatTestCounter - { - public static int Count { get; set; } - } - - private static class SkipTestMarker - { - public static bool WasExecuted { get; set; } - } -} - -public class SimpleTestClass -{ - [Test] - public void SimpleTest() - { - CleanArchitectureTests.SimpleTestMarker.WasExecuted = true; - } -} - -/// -/// Tests for class-level data sources and constructor injection. -/// -[ClassDataSource(typeof(ClassTestData))] -public class ClassDataSourceTests -{ - private readonly int _value; - - public ClassDataSourceTests(int value) - { - _value = value; - } - - [Test] - public async Task ClassConstructor_ReceivesData() - { - await Assert.That(_value).IsGreaterThan(0); - } - - public class ClassTestData : IEnumerable - { - public IEnumerator GetEnumerator() - { - yield return 1; - yield return 2; - yield return 3; - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } -} - -/// -/// Tests for property injection with data sources. -/// -public class PropertyInjectionTests -{ - [Arguments("injected")] - public string? TestProperty { get; set; } - - [Test] - public async Task PropertyInjection_Works() - { - await Assert.That(TestProperty).IsNotNull(); - await Assert.That(TestProperty).IsEqualTo("injected"); - } -} - -/// -/// Tests for async test methods. -/// -public class AsyncTests -{ - [Test] - public async Task AsyncTest_ExecutesCorrectly() - { - await Task.Delay(10); - await Assert.That(true).IsTrue(); - } - - [Test] - public async ValueTask ValueTaskTest_ExecutesCorrectly() - { - await Task.Delay(10); - await Assert.That(true).IsTrue(); - } -} - -/// -/// Tests for complex scenarios combining multiple features. -/// -public class ComplexScenarioTests -{ - [Test] - [Arguments(1, "one")] - [Arguments(2, "two")] - [Repeat(2)] - public async Task ComplexTest_WithMultipleFeatures(int number, string text) - { - await Assert.That(number).IsGreaterThan(0); - await Assert.That(text).IsNotNull(); - await Assert.That(text.Length).IsGreaterThan(0); - } - - [Test] - [MethodDataSource(nameof(GetComplexData))] - [Timeout(2000)] - public async Task ComplexDataDrivenTest(ComplexTestData data) - { - await Assert.That(data).IsNotNull(); - await Assert.That(data.Id).IsGreaterThan(0); - await Assert.That(data.Name).IsNotNull(); - await Task.Delay(100); // Simulate some work - } - - public static IEnumerable GetComplexData() - { - yield return new ComplexTestData { Id = 1, Name = "First" }; - yield return new ComplexTestData { Id = 2, Name = "Second" }; - } - - public class ComplexTestData - { - public int Id { get; set; } - public string Name { get; set; } = ""; - } -} - -/// -/// Tests to verify the source generator handles edge cases correctly. -/// -public class EdgeCaseTests -{ - [Test] - public void VoidReturnType_Works() - { - // Void return type should work - } - - [Test] - public Task TaskReturnType_Works() - { - // Task return type should work - return Task.CompletedTask; - } - - [Test] - public ValueTask ValueTaskReturnType_Works() - { - // ValueTask return type should work - return ValueTask.CompletedTask; - } - - [Test] - [Arguments(null, null)] - public async Task NullArguments_HandleCorrectly(string? arg1, object? arg2) - { - await Assert.That(arg1).IsNull(); - await Assert.That(arg2).IsNull(); - } - - [Test] - [Arguments(new[] { 1, 2, 3 })] - public async Task ArrayArguments_Work(int[] values) - { - await Assert.That(values).Count().IsEqualTo(3); - await Assert.That(values[0]).IsEqualTo(1); - } -} - -/// -/// Verification tests to ensure the clean architecture is working. -/// -public class ArchitectureVerificationTests -{ - [Test] - public async Task TestMetadataIsGenerated_NotComplexCode() - { - // This test verifies that TestMetadataGenerator is being used - // If complex code generation was happening, we'd see different behavior - - // The fact that all our tests work proves TestMetadata + TestBuilder is functioning - await Assert.That(true).IsTrue(); - } - - [Test] - public async Task TestBuilder_HandlesAllComplexLogic() - { - // TestBuilder should handle: - // - Data source enumeration - // - Tuple unwrapping - // - Property injection - // - Test instance creation - - // The fact that our data-driven tests work proves this - await Assert.That(true).IsTrue(); - } -} \ No newline at end of file diff --git a/tests/TUnit.UnitTests/ArgumentFormatterTests.cs b/tests/TUnit.UnitTests/ArgumentFormatterTests.cs new file mode 100644 index 00000000000..f1059727e95 --- /dev/null +++ b/tests/TUnit.UnitTests/ArgumentFormatterTests.cs @@ -0,0 +1,97 @@ +using TUnit.Core.Helpers; + +namespace TUnit.UnitTests; + +public class ArgumentFormatterTests +{ + [Test] + public async Task FormatDefault_ValueTuple_FormatsCorrectly() + { + var tuple = (1, "test", true); + + var result = ArgumentFormatter.Format(tuple, []); + + await Assert.That(result).IsEqualTo("(1, test, True)"); + } + + [Test] + public async Task FormatDefault_NestedValueTuple_FormatsCorrectly() + { + var tuple = (1, (2, "inner"), true); + + var result = ArgumentFormatter.Format(tuple, []); + + await Assert.That(result).IsEqualTo("(1, (2, inner), True)"); + } + + [Test] + public async Task FormatDefault_SystemTuple_FormatsCorrectly() + { + var tuple = Tuple.Create(1, "test", false); + + var result = ArgumentFormatter.Format(tuple, []); + + await Assert.That(result).IsEqualTo("(1, test, False)"); + } + + [Test] + public async Task FormatArguments_WithTuples_FormatsCorrectly() + { + var args = new object?[] { (1, "a"), 42, (true, 3.14) }; + + var result = ArgumentFormatter.FormatArguments(args); + + // Primitives are formatted with ToString(), i.e. the current culture (3,14 under sv-SE). + await Assert.That(result).IsEqualTo($"(1, a), 42, (True, {3.14})"); + } + + [Test] + public async Task FormatDefault_LargeTuple_FormatsAllElements() + { + var tuple = (1, 2, 3, 4, 5, 6, 7, 8); + + var result = ArgumentFormatter.Format(tuple, []); + + await Assert.That(result).IsEqualTo("(1, 2, 3, 4, 5, 6, 7, 8)"); + } + + [Test] + public async Task FormatDefault_TupleWithNull_HandlesNullCorrectly() + { + (int, string?, string) tuple = (1, null, "test"); + + var result = ArgumentFormatter.Format(tuple, []); + + await Assert.That(result).IsEqualTo("(1, null, test)"); + } + + [Test] + public async Task FormatDefault_StringWithDots_EscapesDotsWithMiddleDot() + { + var stringWithDots = "1.2.3"; + + var result = ArgumentFormatter.Format(stringWithDots, []); + + await Assert.That(result).IsEqualTo("1·2·3"); + } + + [Test] + public async Task FormatDefault_StringWithoutDots_ReturnsUnchanged() + { + var stringWithoutDots = "hello world"; + + var result = ArgumentFormatter.Format(stringWithoutDots, []); + + await Assert.That(result).IsEqualTo("hello world"); + } + + [Test] + public async Task FormatArguments_WithStringsContainingDots_EscapesDots() + { + var args = new object?[] { "hello", "with.dot", "1.2.3" }; + + var result = ArgumentFormatter.FormatArguments(args); + + await Assert.That(result).IsEqualTo("hello, with·dot, 1·2·3"); + } +} \ No newline at end of file From 1c396f4f635689ba7a3759ce5324c47fd11d9dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 14:02:35 +0000 Subject: [PATCH 20/49] test: put OpenTelemetry.Tests and Aspire.Tests on the shared test project setup Both hand-rolled a subset of eng/TestProject.props/.targets (engine props, engine/assertions/generator/analyzer references) and so lacked the assertion analyzers every other test project gets, e.g. the one that flags an un-awaited Assert.That, which silently checks nothing. Import the shared props/targets like the other test projects. No new diagnostics; all tests pass via `dotnet test` and as plain runs. --- tests/TUnit.Aspire.Tests/TUnit.Aspire.Tests.csproj | 9 +++------ .../TUnit.OpenTelemetry.Tests.csproj | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/tests/TUnit.Aspire.Tests/TUnit.Aspire.Tests.csproj b/tests/TUnit.Aspire.Tests/TUnit.Aspire.Tests.csproj index 45563f0c7d1..4fd89e03519 100644 --- a/tests/TUnit.Aspire.Tests/TUnit.Aspire.Tests.csproj +++ b/tests/TUnit.Aspire.Tests/TUnit.Aspire.Tests.csproj @@ -1,6 +1,6 @@ - + net10.0 @@ -11,17 +11,14 @@ - - - - - + + diff --git a/tests/TUnit.OpenTelemetry.Tests/TUnit.OpenTelemetry.Tests.csproj b/tests/TUnit.OpenTelemetry.Tests/TUnit.OpenTelemetry.Tests.csproj index 6166f253494..ce0633489bd 100644 --- a/tests/TUnit.OpenTelemetry.Tests/TUnit.OpenTelemetry.Tests.csproj +++ b/tests/TUnit.OpenTelemetry.Tests/TUnit.OpenTelemetry.Tests.csproj @@ -1,6 +1,6 @@ - + net10.0 @@ -11,17 +11,14 @@ - - - - - + + From 0d3ba67b0c9cd8073a47ad47f1052426d1f42152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 14:04:24 +0000 Subject: [PATCH 21/49] test: name SourceGenerator.IncrementalTests' namespace after the project The project's RootNamespace and namespaces were TUnit.Assertions.SourceGenerator.IncrementalTests although it covers the core generators too (hooks, infrastructure, dynamic tests, property injection, ...), which made it show up under a misleading node in test explorers. Use the project name. --- .../AotConverterGeneratorIncrementalTests.cs | 2 +- .../AssertionExtensionGeneratorIncrementalTests.cs | 2 +- .../AssertionMethodGeneratorIncrementalTests.cs | 2 +- .../DynamicTestsGeneratorIncrementalTests.cs | 2 +- tests/TUnit.SourceGenerator.IncrementalTests/Fixture.cs | 2 +- .../HookMetadataGeneratorIncrementalTests.cs | 2 +- .../InfrastructureGeneratorIncrementalTests.cs | 2 +- .../MethodAssertionGeneratorIncrementalTests.cs | 2 +- .../PropertyInjectionSourceGeneratorIncrementalTests.cs | 2 +- .../ShouldExtensionGeneratorIncrementalTests.cs | 2 +- .../StaticPropertyInitializationGeneratorIncrementalTests.cs | 2 +- .../TUnit.SourceGenerator.IncrementalTests.csproj | 1 - tests/TUnit.SourceGenerator.IncrementalTests/TestHelper.cs | 2 +- 13 files changed, 12 insertions(+), 13 deletions(-) diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/AotConverterGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/AotConverterGeneratorIncrementalTests.cs index 3966d71525c..4cc8d3fd24d 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/AotConverterGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/AotConverterGeneratorIncrementalTests.cs @@ -3,7 +3,7 @@ using TUnit.Core.SourceGenerator.Generators; using TUnit.Core.SourceGenerator.Models; -namespace TUnit.Assertions.SourceGenerator.IncrementalTests; +namespace TUnit.SourceGenerator.IncrementalTests; public class AotConverterGeneratorIncrementalTests { diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/AssertionExtensionGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/AssertionExtensionGeneratorIncrementalTests.cs index d5c2d1741c2..aa9b6595573 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/AssertionExtensionGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/AssertionExtensionGeneratorIncrementalTests.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis.CSharp; using TUnit.Assertions.SourceGenerator.Generators; -namespace TUnit.Assertions.SourceGenerator.IncrementalTests; +namespace TUnit.SourceGenerator.IncrementalTests; public class AssertionExtensionGeneratorIncrementalTests { diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/AssertionMethodGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/AssertionMethodGeneratorIncrementalTests.cs index 8c1395aafeb..ce6c29dd36b 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/AssertionMethodGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/AssertionMethodGeneratorIncrementalTests.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis.CSharp; using TUnit.Assertions.SourceGenerator.Generators; -namespace TUnit.Assertions.SourceGenerator.IncrementalTests; +namespace TUnit.SourceGenerator.IncrementalTests; public class AssertionMethodGeneratorIncrementalTests { diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/DynamicTestsGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/DynamicTestsGeneratorIncrementalTests.cs index 0cfc8648daa..2db2bed8e48 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/DynamicTestsGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/DynamicTestsGeneratorIncrementalTests.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis.CSharp; using TUnit.Core.SourceGenerator.CodeGenerators; -namespace TUnit.Assertions.SourceGenerator.IncrementalTests; +namespace TUnit.SourceGenerator.IncrementalTests; public class DynamicTestsGeneratorIncrementalTests { diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/Fixture.cs b/tests/TUnit.SourceGenerator.IncrementalTests/Fixture.cs index 6bd7f9eecde..9d5ddd8f594 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/Fixture.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/Fixture.cs @@ -6,7 +6,7 @@ using TUnit.Core; using TUnit.Tests.Shared; -namespace TUnit.Assertions.SourceGenerator.IncrementalTests; +namespace TUnit.SourceGenerator.IncrementalTests; public static class Fixture { diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/HookMetadataGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/HookMetadataGeneratorIncrementalTests.cs index 8f3c56fd117..d486c871ad6 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/HookMetadataGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/HookMetadataGeneratorIncrementalTests.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis.CSharp; using TUnit.Core.SourceGenerator.Generators; -namespace TUnit.Assertions.SourceGenerator.IncrementalTests; +namespace TUnit.SourceGenerator.IncrementalTests; public class HookMetadataGeneratorIncrementalTests { diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/InfrastructureGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/InfrastructureGeneratorIncrementalTests.cs index 48f0fb8d854..0b91f126794 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/InfrastructureGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/InfrastructureGeneratorIncrementalTests.cs @@ -3,7 +3,7 @@ using Microsoft.CodeAnalysis.CSharp; using TUnit.Core.SourceGenerator.CodeGenerators; -namespace TUnit.Assertions.SourceGenerator.IncrementalTests; +namespace TUnit.SourceGenerator.IncrementalTests; public class InfrastructureGeneratorIncrementalTests { diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/MethodAssertionGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/MethodAssertionGeneratorIncrementalTests.cs index 3c6b07d0b56..18e290cde82 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/MethodAssertionGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/MethodAssertionGeneratorIncrementalTests.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis.CSharp; using TUnit.Assertions.SourceGenerator.Generators; -namespace TUnit.Assertions.SourceGenerator.IncrementalTests; +namespace TUnit.SourceGenerator.IncrementalTests; public class MethodAssertionGeneratorIncrementalTests { diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/PropertyInjectionSourceGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/PropertyInjectionSourceGeneratorIncrementalTests.cs index 4bc2b37e90c..37c8c55f373 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/PropertyInjectionSourceGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/PropertyInjectionSourceGeneratorIncrementalTests.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis.CSharp; using TUnit.Core.SourceGenerator.Generators; -namespace TUnit.Assertions.SourceGenerator.IncrementalTests; +namespace TUnit.SourceGenerator.IncrementalTests; public class PropertyInjectionSourceGeneratorIncrementalTests { diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/ShouldExtensionGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/ShouldExtensionGeneratorIncrementalTests.cs index 34fe0aa3797..369f0762bdb 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/ShouldExtensionGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/ShouldExtensionGeneratorIncrementalTests.cs @@ -4,7 +4,7 @@ using Microsoft.CodeAnalysis.CSharp; using ShouldGenerator::TUnit.Assertions.Should.SourceGenerator; -namespace TUnit.Assertions.SourceGenerator.IncrementalTests; +namespace TUnit.SourceGenerator.IncrementalTests; public class ShouldExtensionGeneratorIncrementalTests { diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/StaticPropertyInitializationGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/StaticPropertyInitializationGeneratorIncrementalTests.cs index a040db0d6a4..b327072d24f 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/StaticPropertyInitializationGeneratorIncrementalTests.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/StaticPropertyInitializationGeneratorIncrementalTests.cs @@ -3,7 +3,7 @@ using TUnit.Core.SourceGenerator.CodeGenerators; using TUnit.Core.SourceGenerator.Models; -namespace TUnit.Assertions.SourceGenerator.IncrementalTests; +namespace TUnit.SourceGenerator.IncrementalTests; public class StaticPropertyInitializationGeneratorIncrementalTests { diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/TUnit.SourceGenerator.IncrementalTests.csproj b/tests/TUnit.SourceGenerator.IncrementalTests/TUnit.SourceGenerator.IncrementalTests.csproj index 6d63e87c9b1..0d82772e057 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/TUnit.SourceGenerator.IncrementalTests.csproj +++ b/tests/TUnit.SourceGenerator.IncrementalTests/TUnit.SourceGenerator.IncrementalTests.csproj @@ -9,7 +9,6 @@ false $(NoWarn);MSB3277 - TUnit.Assertions.SourceGenerator.IncrementalTests diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/TestHelper.cs b/tests/TUnit.SourceGenerator.IncrementalTests/TestHelper.cs index b8fea110b12..8fcda9b567b 100644 --- a/tests/TUnit.SourceGenerator.IncrementalTests/TestHelper.cs +++ b/tests/TUnit.SourceGenerator.IncrementalTests/TestHelper.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace TUnit.Assertions.SourceGenerator.IncrementalTests; +namespace TUnit.SourceGenerator.IncrementalTests; internal static class TestHelper { From 72134d38155b5dcdd6c0f53161ac493a60f7e981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 14:22:33 +0000 Subject: [PATCH 22/49] build: add the fast test projects to TUnit.Dev.slnx TUnit.Dev.slnx says it leaves out only the Roslyn version variants, but it also omitted the Should library and its tests, the Mocks InternalsAccess task and tests, SourceGenerator.IncrementalTests, AspNetCore.Tests and Templates.Tests, so "run all tests" from the dev solution never ran them. Add them with the projects they need (each runs in seconds without external services). Aspire.Tests (needs Docker, >1 min) and the template's Playwright content project stay out. --- TUnit.Dev.slnx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/TUnit.Dev.slnx b/TUnit.Dev.slnx index 127bb8d1521..d107eb81076 100644 --- a/TUnit.Dev.slnx +++ b/TUnit.Dev.slnx @@ -7,6 +7,7 @@ + @@ -18,6 +19,7 @@ + @@ -28,6 +30,7 @@ + @@ -44,6 +47,7 @@ + @@ -54,8 +58,13 @@ + + + + + @@ -63,12 +72,16 @@ + + + + From 2ad96948b95e9933d6fe4715f6e109250af0d08a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 15:33:53 +0000 Subject: [PATCH 23/49] test(mocks): reference Mono.Cecil directly in InternalsAccess.Tests PublicizeAssemblyReferencesTaskTests uses Mono.Cecil directly but only received it transitively through the task project reference. An IDE build whose restore did not carry the transitive package (seen in Rider on macOS) failed with CS0103 "The name 'Mono' does not exist". Declare the dependency the code actually has. --- .../TUnit.Mocks.InternalsAccess.Tests.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/TUnit.Mocks.InternalsAccess.Tests/TUnit.Mocks.InternalsAccess.Tests.csproj b/tests/TUnit.Mocks.InternalsAccess.Tests/TUnit.Mocks.InternalsAccess.Tests.csproj index 1f418da88f1..907375df137 100644 --- a/tests/TUnit.Mocks.InternalsAccess.Tests/TUnit.Mocks.InternalsAccess.Tests.csproj +++ b/tests/TUnit.Mocks.InternalsAccess.Tests/TUnit.Mocks.InternalsAccess.Tests.csproj @@ -33,6 +33,9 @@ + + From 157efd34929fafceb613d3a90c1eb3de02f92ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 15:57:30 +0000 Subject: [PATCH 24/49] test: sort host assembly references so generator snapshots match on every OS The trusted-platform-assemblies list has no defined order and differs between Linux and macOS hosts. Generators emit referenced assemblies in reference order, so DuplicateTypeNameAcrossAssembliesTests' snapshot listed assemblies in a different order on macOS. Sort the paths by file name to make compilations identical on every host. --- tests/SharedTestHelpers/TestHostReferences.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/SharedTestHelpers/TestHostReferences.cs b/tests/SharedTestHelpers/TestHostReferences.cs index 80ca7c23222..b365f448410 100644 --- a/tests/SharedTestHelpers/TestHostReferences.cs +++ b/tests/SharedTestHelpers/TestHostReferences.cs @@ -14,6 +14,10 @@ namespace TUnit.Tests.Shared; /// that a plain run does not, so types such as System.Net.HttpStatusCode resolved or not and generator output /// differed between launches. On .NET, TRUSTED_PLATFORM_ASSEMBLIES lists the app's whole deployment /// (framework plus deps.json) up front. .NET Framework has no such list, so it keeps the loaded assemblies. +/// +/// Neither source has a defined order (the TPA order differs between Linux and macOS hosts), and generators emit +/// referenced assemblies in reference order, so the paths are sorted to keep snapshots identical on every host. +/// /// internal static class TestHostReferences { @@ -21,14 +25,15 @@ internal static class TestHostReferences private static string[] Resolve() { - if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string trustedPlatformAssemblies) - { - return trustedPlatformAssemblies.Split(new[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries); - } + var paths = AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string trustedPlatformAssemblies + ? trustedPlatformAssemblies.Split(new[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries) + : AppDomain.CurrentDomain.GetAssemblies() + .Where(assembly => !assembly.IsDynamic && !string.IsNullOrWhiteSpace(assembly.Location)) + .Select(assembly => assembly.Location); - return AppDomain.CurrentDomain.GetAssemblies() - .Where(assembly => !assembly.IsDynamic && !string.IsNullOrWhiteSpace(assembly.Location)) - .Select(assembly => assembly.Location) + return paths + .OrderBy(Path.GetFileName, StringComparer.Ordinal) + .ThenBy(path => path, StringComparer.Ordinal) .ToArray(); } } From a901388d3355dd35b3bae833898b69e15463fe7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 16:20:12 +0000 Subject: [PATCH 25/49] test: scrub wrapped source paths in PublicAPI snapshots The scrubber only handled CI's deterministic /_/ source paths. A local checkout path longer than 80 characters is wrapped by the API generator into "..." + continuation lines, so OpenTelemetry_Library_Has_No_API_Changes kept a fragment of the path on macOS. Replace whole path literals, including continuations, for Unix and Windows paths. --- tests/TUnit.PublicAPI/Tests.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/TUnit.PublicAPI/Tests.cs b/tests/TUnit.PublicAPI/Tests.cs index 4bef708f001..c6a0514277d 100644 --- a/tests/TUnit.PublicAPI/Tests.cs +++ b/tests/TUnit.PublicAPI/Tests.cs @@ -42,12 +42,13 @@ private async Task VerifyPublicApi(Assembly assembly) await VerifyTUnit.Verify(publicApi) .AddScrubber(sb => { - // Scrub deterministic source paths (e.g. "/_/TUnit.OpenTelemetry/File.cs") - // before the URL regex mangles them into bare "/_/". + // Scrub source path literals (e.g. hook attributes' caller file path) before the URL regex + // mangles them. CI builds embed deterministic "/_/..." paths; local builds embed the full + // checkout path, which the generator wraps into "..." + "..." pieces once it exceeds 80 characters. var replaced = System.Text.RegularExpressions.Regex.Replace( sb.ToString(), - @"/_/[^""\s,)]*", - "PATH_SCRUBBED"); + @"""(?:/|[A-Za-z]:(?:\\\\|/))[^""\r\n]*""(?:[ \t]*\+[ \t]*\r?\n[ \t]*""[^""\r\n]*"")*", + @"""PATH_SCRUBBED"""); sb.Clear(); sb.Append(replaced); return sb; From 9d1855bf3de58574777a5dfb7fe73701c2e5237c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 21:23:24 +0000 Subject: [PATCH 26/49] fix: address review feedback on expectation messages, dotnet resolution and path scrubbing - AssertionMethodGenerator: accept [AssertionFrom] expectation messages that already start with "to " (the documented style) so they no longer render as "to to ...", and leave a spelled-out "to not ..." alone instead of negating it twice. Built-in messages, which omit the "to", are unchanged. - DotNetCli: only prefer the running installation's muxer when it has an SDK; a runtime-only installation cannot run `dotnet build`, so fall back to `dotnet` on PATH. - PublicAPI: only scrub rooted ".cs" string literals followed by the caller line number argument, so public values such as "/v1" are kept, and cover the scrubber with focused tests (short CI paths, wrapped Unix and Windows paths, unrelated strings). --- .../Generators/AssertionMethodGenerator.cs | 31 ++++++++---- tests/SharedTestHelpers/DotNetCli.cs | 13 ++++- .../AssertionFromExpectationMessageTests.cs | 39 +++++++++++++++ .../SourcePathScrubberTests.cs | 50 +++++++++++++++++++ tests/TUnit.PublicAPI/Tests.cs | 24 ++++++--- 5 files changed, 137 insertions(+), 20 deletions(-) create mode 100644 tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs create mode 100644 tests/TUnit.PublicAPI/SourcePathScrubberTests.cs diff --git a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs index b449c5beac7..97743c864ee 100644 --- a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs +++ b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs @@ -974,17 +974,7 @@ private static void GenerateAssertConditionClassForMethod(StringBuilder sourceBu if (!string.IsNullOrEmpty(attributeData.ExpectationMessage)) { // Use custom expectation message - var expectation = attributeData.ExpectationMessage; - if (parameters.Any()) - { - // Use interpolated string for parameter substitution - sourceBuilder.AppendLine($" return $\"to {{(_negated ? \"not \" : \"\")}}{expectation}\";"); - } - else - { - // No parameters, just return the literal string with negation support - sourceBuilder.AppendLine($" return $\"to {{(_negated ? \"not \" : \"\")}}{expectation}\";"); - } + sourceBuilder.AppendLine($" return $\"{RenderCustomExpectation(attributeData.ExpectationMessage!)}\";"); } else { @@ -1002,6 +992,25 @@ private static void GenerateAssertConditionClassForMethod(StringBuilder sourceBu sourceBuilder.AppendLine(); } + /// + /// Renders the interpolated-string body of GetExpectation() for a custom ExpectationMessage. + /// + /// + /// The built-in messages omit the leading "to" ("be null or empty") and leave negation to the generator, while + /// the documented style spells it out ("to be null or empty", or "to not contain '{value}'" with + /// NegateLogic). Both are accepted so neither renders as "to to ..." or "to not not ...". + /// + private static string RenderCustomExpectation(string expectationMessage) + { + var expectation = expectationMessage.StartsWith("to ", StringComparison.Ordinal) + ? expectationMessage.Substring("to ".Length) + : expectationMessage; + + return expectation.StartsWith("not ", StringComparison.Ordinal) + ? $"to {expectation}" + : $"to {{(_negated ? \"not \" : \"\")}}{expectation}"; + } + private static string GenerateAssertConditionClassNameForMethod(INamedTypeSymbol targetType, INamedTypeSymbol containingType, string methodName, IMethodSymbol method) { var targetTypeName = GetSimpleTypeName(targetType); diff --git a/tests/SharedTestHelpers/DotNetCli.cs b/tests/SharedTestHelpers/DotNetCli.cs index 14579e6b889..79066234d29 100644 --- a/tests/SharedTestHelpers/DotNetCli.cs +++ b/tests/SharedTestHelpers/DotNetCli.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using System.Runtime.InteropServices; namespace TUnit.Tests.Shared; @@ -11,7 +12,9 @@ namespace TUnit.Tests.Shared; /// A bare dotnet resolves through PATH, which an IDE does not necessarily share with the /// installation it builds and runs tests with (on macOS, apps started from the Dock or Finder get a different /// PATH than a terminal). That dotnet can lack an SDK satisfying global.json and exit with -/// 155 (SdkResolveFailure). Prefer the installation that is running the current test process. +/// 155 (SdkResolveFailure). Prefer the installation that is running the current test process, as long as it +/// has an SDK: a runtime-only installation cannot run +/// dotnet build, so PATH is the better bet there. /// internal static class DotNetCli { @@ -31,6 +34,12 @@ private static string Resolve() var root = System.IO.Path.GetFullPath(System.IO.Path.Combine(runtimeDirectory, "..", "..", "..")); var muxer = System.IO.Path.Combine(root, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"); - return File.Exists(muxer) ? muxer : "dotnet"; + return File.Exists(muxer) && HasSdk(root) ? muxer : "dotnet"; + } + + private static bool HasSdk(string root) + { + var sdkDirectory = System.IO.Path.Combine(root, "sdk"); + return Directory.Exists(sdkDirectory) && Directory.EnumerateDirectories(sdkDirectory).Any(); } } diff --git a/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs b/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs new file mode 100644 index 00000000000..653818e3c1c --- /dev/null +++ b/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs @@ -0,0 +1,39 @@ +using TUnit.Assertions.Attributes; + +namespace TUnit.Assertions.Tests; + +/// +/// [AssertionFrom] expectation messages written in the documented style, which spells out the leading "to" +/// (and, with NegateLogic, the "not") that the generator otherwise adds itself. +/// +[AssertionFrom(nameof(string.IsNullOrEmpty), CustomName = "IsBlankDocumentedStyle", ExpectationMessage = "to be null or empty")] +[AssertionFrom(nameof(string.IsNullOrEmpty), CustomName = "IsNotBlankDocumentedStyle", NegateLogic = true, ExpectationMessage = "to be null or empty")] +[AssertionFrom(nameof(string.IsNullOrWhiteSpace), CustomName = "HasTextDocumentedStyle", NegateLogic = true, ExpectationMessage = "to not be null, empty, or whitespace")] +public static partial class DocumentedStyleStringAssertions; + +public class AssertionFromExpectationMessageTests +{ + [Test] + public async Task Leading_To_Is_Not_Repeated() + { + await Assert.That(async () => await Assert.That("Foo").IsBlankDocumentedStyle()) + .Throws() + .WithMessageContaining("Expected to be null or empty"); + } + + [Test] + public async Task Leading_To_Is_Not_Repeated_When_Negated() + { + await Assert.That(async () => await Assert.That("").IsNotBlankDocumentedStyle()) + .Throws() + .WithMessageContaining("Expected to not be null or empty"); + } + + [Test] + public async Task Spelled_Out_Negation_Is_Not_Repeated() + { + await Assert.That(async () => await Assert.That(" ").HasTextDocumentedStyle()) + .Throws() + .WithMessageContaining("Expected to not be null, empty, or whitespace"); + } +} diff --git a/tests/TUnit.PublicAPI/SourcePathScrubberTests.cs b/tests/TUnit.PublicAPI/SourcePathScrubberTests.cs new file mode 100644 index 00000000000..2f72fdd3c01 --- /dev/null +++ b/tests/TUnit.PublicAPI/SourcePathScrubberTests.cs @@ -0,0 +1,50 @@ +namespace TUnit.PublicAPI; + +public class SourcePathScrubberTests +{ + [Test] + [Arguments("[.Before(., \"/_/TUnit.OpenTelemetry/File.cs\", 39, Order=-2147482648)]")] + [Arguments("[.After(., \"/_/F.cs\", 7)]")] + [Arguments("[.Before(., \"C:\\\\a\\\\File.cs\", 39)]")] + [Arguments("[.Before(., \"C:/a/File.cs\", 39)]")] + public async Task Scrubs_Single_Line_Caller_File_Paths(string line) + { + var scrubbed = Tests.ScrubSourcePaths(line); + + await Assert.That(scrubbed).Contains("\"PATH_SCRUBBED\""); + await Assert.That(scrubbed).DoesNotContain("File.cs"); + } + + [Test] + public async Task Scrubs_Wrapped_Unix_Caller_File_Path() + { + var text = "[.Before(., \"/home/user/src/TUnit/TUnit.OpenTelemetry/AutoStart/\" +\n" + + " \"TUnitOpenTelemetryAutoStart.cs\", 39, Order=-2147482648)]"; + + var scrubbed = Tests.ScrubSourcePaths(text); + + await Assert.That(scrubbed).IsEqualTo("[.Before(., \"PATH_SCRUBBED\", 39, Order=-2147482648)]"); + } + + [Test] + public async Task Scrubs_Wrapped_Windows_Caller_File_Path() + { + var text = "[.After(., \"C:\\\\Users\\\\dev\\\\source\\\\repos\\\\TUnit\\\\TUnit.OpenTelemetry\\\\\" +\r\n" + + " \"AutoStart\\\\TUnitOpenTelemetryAutoStart.cs\", 61)]"; + + var scrubbed = Tests.ScrubSourcePaths(text); + + await Assert.That(scrubbed).IsEqualTo("[.After(., \"PATH_SCRUBBED\", 61)]"); + } + + [Test] + [Arguments("public const string Prefix = \"/v1\";")] + [Arguments("[Route(\"/v1\", 2)]")] + [Arguments("public const string Template = \"/api/File.cs\";")] + [Arguments("[Description(\"C:\\\\docs\\\\readme.md\")]")] + [Arguments("[Description(\"File.cs\", 39)]")] + public async Task Leaves_Unrelated_Strings_Alone(string line) + { + await Assert.That(Tests.ScrubSourcePaths(line)).IsEqualTo(line); + } +} diff --git a/tests/TUnit.PublicAPI/Tests.cs b/tests/TUnit.PublicAPI/Tests.cs index c6a0514277d..c3e40d720a5 100644 --- a/tests/TUnit.PublicAPI/Tests.cs +++ b/tests/TUnit.PublicAPI/Tests.cs @@ -42,13 +42,7 @@ private async Task VerifyPublicApi(Assembly assembly) await VerifyTUnit.Verify(publicApi) .AddScrubber(sb => { - // Scrub source path literals (e.g. hook attributes' caller file path) before the URL regex - // mangles them. CI builds embed deterministic "/_/..." paths; local builds embed the full - // checkout path, which the generator wraps into "..." + "..." pieces once it exceeds 80 characters. - var replaced = System.Text.RegularExpressions.Regex.Replace( - sb.ToString(), - @"""(?:/|[A-Za-z]:(?:\\\\|/))[^""\r\n]*""(?:[ \t]*\+[ \t]*\r?\n[ \t]*""[^""\r\n]*"")*", - @"""PATH_SCRUBBED"""); + var replaced = ScrubSourcePaths(sb.ToString()); sb.Clear(); sb.Append(replaced); return sb; @@ -74,6 +68,14 @@ await VerifyTUnit.Verify(publicApi) .UniqueForTargetFrameworkAndVersion(assembly); } + /// + /// Scrubs source path literals (hook attributes' [CallerFilePath] argument) before the URL regex mangles + /// them. CI builds embed deterministic "/_/..." paths; local builds embed the full checkout path, which the + /// generator wraps into "..." + "..." pieces once it exceeds 80 characters. Only a rooted ".cs" path followed by + /// the [CallerLineNumber] argument matches, so public string values such as "/v1" are left alone. + /// + internal static string ScrubSourcePaths(string text) => SourcePathRegex().Replace(text, @"""PATH_SCRUBBED"""); + private StringBuilder Scrub(StringBuilder text) { var newText = UrlRegex().Replace(text.ToString(), string.Empty) @@ -95,10 +97,18 @@ private string Scrub(string text) } + private const string SourcePathPattern = + @"""(?:/|[A-Za-z]:(?:\\\\|/))[^""\r\n]*(?:""[ \t]*\+[ \t]*\r?\n[ \t]*""[^""\r\n]*)*\.cs""(?=,[ \t]*\d+[,)])"; + #if NET + [GeneratedRegex(SourcePathPattern)] + private static partial Regex SourcePathRegex(); + [GeneratedRegex(@"((http|https):\/\/)?(www\.)?([a-zA-Z0-9-]+\.[a-zA-Z]{2,})(\/[a-zA-Z0-9-._~:\/?#[\]@!$&'()*+,;=]*)?")] public static partial Regex UrlRegex(); #else + private static Regex SourcePathRegex() => new(SourcePathPattern); + public static Regex UrlRegex() => new( @"((http|https):\/\/)?(www\.)?([a-zA-Z0-9-]+\.[a-zA-Z]{2,})(\/[a-zA-Z0-9-._~:\/?#[\]@!$&'()*+,;=]*)?"); From 500f30ed8a21f7e29cae26f21d5b92d638e33263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 21:23:31 +0000 Subject: [PATCH 27/49] fix(assertions): substitute {param} placeholders in [AssertionFrom] messages A custom ExpectationMessage was emitted verbatim into GetExpectation()'s interpolated string, but the generated class stores method parameters as `_{name}` fields, so the documented `"to start with {value}"` failed to compile with CS0103. Rewrite `{name}` to `{_name}` for each parameter, as [GenerateAssertion] already does. Quote the placeholders in the StartsWith/EndsWith examples so the shown failure messages ('he') match what the examples produce. --- .../source-generator-assertions.md | 4 ++-- .../Generators/AssertionMethodGenerator.cs | 11 +++++++++-- .../Attributes/AssertionFromAttribute`1.cs | 2 +- .../AssertionFromExpectationMessageTests.cs | 18 ++++++++++++++++++ 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/docs/docs/assertions/extensibility/source-generator-assertions.md b/docs/docs/assertions/extensibility/source-generator-assertions.md index 6cb1658dc8b..adf6e518cc6 100644 --- a/docs/docs/assertions/extensibility/source-generator-assertions.md +++ b/docs/docs/assertions/extensibility/source-generator-assertions.md @@ -239,8 +239,8 @@ Use `[AssertionFrom]` to create assertions from existing methods in libraries or using TUnit.Assertions.Attributes; [AssertionFrom(nameof(string.IsNullOrEmpty), CustomName = "IsBlank", ExpectationMessage = "to be null or empty")] -[AssertionFrom(nameof(string.StartsWith), CustomName = "BeginsWithText", ExpectationMessage = "to start with {value}")] -[AssertionFrom(nameof(string.EndsWith), CustomName = "EndsWithText", ExpectationMessage = "to end with {value}")] +[AssertionFrom(nameof(string.StartsWith), CustomName = "BeginsWithText", ExpectationMessage = "to start with '{value}'")] +[AssertionFrom(nameof(string.EndsWith), CustomName = "EndsWithText", ExpectationMessage = "to end with '{value}'")] public static partial class StringAssertionExtensions { } diff --git a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs index 97743c864ee..28413e1563a 100644 --- a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs +++ b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs @@ -974,7 +974,7 @@ private static void GenerateAssertConditionClassForMethod(StringBuilder sourceBu if (!string.IsNullOrEmpty(attributeData.ExpectationMessage)) { // Use custom expectation message - sourceBuilder.AppendLine($" return $\"{RenderCustomExpectation(attributeData.ExpectationMessage!)}\";"); + sourceBuilder.AppendLine($" return $\"{RenderCustomExpectation(attributeData.ExpectationMessage!, parameters)}\";"); } else { @@ -999,13 +999,20 @@ private static void GenerateAssertConditionClassForMethod(StringBuilder sourceBu /// The built-in messages omit the leading "to" ("be null or empty") and leave negation to the generator, while /// the documented style spells it out ("to be null or empty", or "to not contain '{value}'" with /// NegateLogic). Both are accepted so neither renders as "to to ..." or "to not not ...". + /// Parameter placeholders such as {value} are rewritten to the generated _value field. /// - private static string RenderCustomExpectation(string expectationMessage) + private static string RenderCustomExpectation(string expectationMessage, IParameterSymbol[] parameters) { var expectation = expectationMessage.StartsWith("to ", StringComparison.Ordinal) ? expectationMessage.Substring("to ".Length) : expectationMessage; + // Replace each parameter placeholder {paramName} with {_paramName} + foreach (var parameter in parameters) + { + expectation = expectation.Replace($"{{{parameter.Name}}}", $"{{_{parameter.Name}}}"); + } + return expectation.StartsWith("not ", StringComparison.Ordinal) ? $"to {expectation}" : $"to {{(_negated ? \"not \" : \"\")}}{expectation}"; diff --git a/src/TUnit.Assertions/Attributes/AssertionFromAttribute`1.cs b/src/TUnit.Assertions/Attributes/AssertionFromAttribute`1.cs index 8a18aa72e8d..316dfa36e30 100644 --- a/src/TUnit.Assertions/Attributes/AssertionFromAttribute`1.cs +++ b/src/TUnit.Assertions/Attributes/AssertionFromAttribute`1.cs @@ -135,7 +135,7 @@ public AssertionFromAttribute(Type containingType, string methodName) /// /// /// - /// [AssertionFrom<string>("StartsWith", ExpectationMessage = "start with {value}")] + /// [AssertionFrom<string>("StartsWith", ExpectationMessage = "start with '{value}'")] /// // Error message: "Expected 'hello' to start with 'xyz' but..." /// /// diff --git a/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs b/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs index 653818e3c1c..1ec03c6962c 100644 --- a/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs +++ b/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs @@ -9,6 +9,8 @@ namespace TUnit.Assertions.Tests; [AssertionFrom(nameof(string.IsNullOrEmpty), CustomName = "IsBlankDocumentedStyle", ExpectationMessage = "to be null or empty")] [AssertionFrom(nameof(string.IsNullOrEmpty), CustomName = "IsNotBlankDocumentedStyle", NegateLogic = true, ExpectationMessage = "to be null or empty")] [AssertionFrom(nameof(string.IsNullOrWhiteSpace), CustomName = "HasTextDocumentedStyle", NegateLogic = true, ExpectationMessage = "to not be null, empty, or whitespace")] +[AssertionFrom(nameof(string.StartsWith), CustomName = "BeginsWithTextDocumentedStyle", ExpectationMessage = "to start with '{value}'")] +[AssertionFrom(nameof(string.Contains), CustomName = "LacksTextDocumentedStyle", NegateLogic = true, ExpectationMessage = "to not contain '{value}'")] public static partial class DocumentedStyleStringAssertions; public class AssertionFromExpectationMessageTests @@ -36,4 +38,20 @@ await Assert.That(async () => await Assert.That(" ").HasTextDocumentedStyle()) .Throws() .WithMessageContaining("Expected to not be null, empty, or whitespace"); } + + [Test] + public async Task Parameter_Placeholder_Is_Substituted() + { + await Assert.That(async () => await Assert.That("hello").BeginsWithTextDocumentedStyle("xyz")) + .Throws() + .WithMessageContaining("Expected to start with 'xyz'"); + } + + [Test] + public async Task Parameter_Placeholder_Is_Substituted_With_Spelled_Out_Negation() + { + await Assert.That(async () => await Assert.That("hello").LacksTextDocumentedStyle("ell")) + .Throws() + .WithMessageContaining("Expected to not contain 'ell'"); + } } From 695c892caef589a7aedc84a1424829f9f4e12970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 21:47:20 +0000 Subject: [PATCH 28/49] fix(assertions): render [AssertionFrom] message text in a single pass Placeholders were substituted with one string.Replace per parameter, so the escaped "{{value}}" also became "{_value}" and printed the generated field name. Tokenize the message once instead: "{name}" for a parameter becomes the "{_name}" field, and everything else stays literal text, including escaped braces, placeholders that name no parameter, stray braces, quotes and backslashes, which previously produced code that did not compile. Decide the "to"/"to not" wording in one helper for custom and default messages; a negated default now reads "to not satisfy X" rather than "not to satisfy X", matching the custom messages. Document how ExpectationMessage is rendered on both attributes and in the docs, and guard every built-in [AssertionFrom] declaration, including the files without a snapshot test, against "to to" and "not not". --- .../source-generator-assertions.md | 8 ++ .../Generators/AssertionMethodGenerator.cs | 77 ++++++++++++---- .../Attributes/AssertionFromAttribute.cs | 11 +++ .../Attributes/AssertionFromAttribute`1.cs | 8 +- .../BuiltInExpectationMessageTests.cs | 91 +++++++++++++++++++ .../AssertionFromExpectationMessageTests.cs | 39 ++++++++ 6 files changed, 217 insertions(+), 17 deletions(-) create mode 100644 tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs diff --git a/docs/docs/assertions/extensibility/source-generator-assertions.md b/docs/docs/assertions/extensibility/source-generator-assertions.md index adf6e518cc6..e618c796770 100644 --- a/docs/docs/assertions/extensibility/source-generator-assertions.md +++ b/docs/docs/assertions/extensibility/source-generator-assertions.md @@ -283,6 +283,14 @@ await Assert.That("hello").LacksText("ell"); // ❌ Fails: "Expected to not con **Note:** Negation only works with `bool`-returning methods. `AssertionResult` methods determine their own pass/fail logic. +### How `ExpectationMessage` Is Rendered + +For `[AssertionFrom]`, the message is the phrase that follows "Expected". Write it in the positive form, with or without the leading "to": + +- The generator adds "to", plus "not" when `NegateLogic = true`: `"be null or empty"` and `"to be null or empty"` both render as "to be null or empty", or "to not be null or empty" when negated. +- A phrase that already starts with "not" (`"to not contain '{value}'"`) is used as written rather than negated again. +- `{paramName}` is replaced with that parameter's value. Everything else is literal text: write `{{` and `}}` for literal braces, and a placeholder that names no parameter is printed as is. + ### Referencing Methods on Different Types ```csharp diff --git a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs index 28413e1563a..1cb8ba773d5 100644 --- a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs +++ b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs @@ -4,7 +4,9 @@ using System.Collections.Immutable; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using TUnit.Assertions.SourceGenerator.Models; @@ -979,12 +981,10 @@ private static void GenerateAssertConditionClassForMethod(StringBuilder sourceBu else { // Use default expectation message - sourceBuilder.Append($" return $\"{{(_negated ? \"not \" : \"\")}}to satisfy {methodName}"); - if (parameters.Any()) - { - sourceBuilder.Append($"({string.Join(", ", parameters.Select(p => $"{{_{p.Name}}}"))})"); - } - sourceBuilder.AppendLine("\";"); + var arguments = parameters.Any() + ? $"({string.Join(", ", parameters.Select(p => $"{{_{p.Name}}}"))})" + : string.Empty; + sourceBuilder.AppendLine($" return $\"{RenderNegatableExpectation($"satisfy {methodName}{arguments}")}\";"); } sourceBuilder.AppendLine(" }"); @@ -996,10 +996,10 @@ private static void GenerateAssertConditionClassForMethod(StringBuilder sourceBu /// Renders the interpolated-string body of GetExpectation() for a custom ExpectationMessage. /// /// - /// The built-in messages omit the leading "to" ("be null or empty") and leave negation to the generator, while - /// the documented style spells it out ("to be null or empty", or "to not contain '{value}'" with - /// NegateLogic). Both are accepted so neither renders as "to to ..." or "to not not ...". - /// Parameter placeholders such as {value} are rewritten to the generated _value field. + /// The canonical form is the positive phrase without the leading "to" ("be null or empty"); the generator adds + /// "to" and, with NegateLogic, "not". The documented style that spells these out ("to be null or empty", + /// or "to not contain '{value}'" with NegateLogic) is also accepted, so neither renders as "to to ..." or + /// "to not not ...". /// private static string RenderCustomExpectation(string expectationMessage, IParameterSymbol[] parameters) { @@ -1007,15 +1007,60 @@ private static string RenderCustomExpectation(string expectationMessage, IParame ? expectationMessage.Substring("to ".Length) : expectationMessage; - // Replace each parameter placeholder {paramName} with {_paramName} - foreach (var parameter in parameters) + var phrase = RenderMessageText(expectation, parameters); + + return expectation.StartsWith("not ", StringComparison.Ordinal) + ? $"to {phrase}" + : RenderNegatableExpectation(phrase); + } + + /// + /// The one place the expectation's "to"/"to not" wording is decided, for custom and default messages alike. + /// + private static string RenderNegatableExpectation(string phrase) => $"to {{(_negated ? \"not \" : \"\")}}{phrase}"; + + /// + /// Renders message text as the body of an interpolated string literal in a single pass: a {name} + /// placeholder for a method parameter becomes the generated {_name} field, and everything else stays + /// literal text, including escaped braces ({{name}} prints {name}), placeholders that name no + /// parameter, stray braces, quotes and backslashes. + /// + private static string RenderMessageText(string message, IParameterSymbol[] parameters) + { + var parameterNames = new HashSet(parameters.Select(p => p.Name), StringComparer.Ordinal); + var result = new StringBuilder(message.Length); + var literalStart = 0; + + foreach (Match match in MessageTokenRegex.Matches(message)) { - expectation = expectation.Replace($"{{{parameter.Name}}}", $"{{_{parameter.Name}}}"); + result.Append(FormatLiteral(message.Substring(literalStart, match.Index - literalStart))); + + var name = match.Groups["name"]; + if (name.Success && parameterNames.Contains(name.Value)) + { + result.Append("{_").Append(name.Value).Append('}'); + } + else + { + // An escaped brace pair, an unknown placeholder or a stray brace: keep its text literal + var literal = match.Value is "{{" ? "{" : match.Value is "}}" ? "}" : match.Value; + result.Append(literal.Replace("{", "{{").Replace("}", "}}")); + } + + literalStart = match.Index + match.Length; } - return expectation.StartsWith("not ", StringComparison.Ordinal) - ? $"to {expectation}" - : $"to {{(_negated ? \"not \" : \"\")}}{expectation}"; + result.Append(FormatLiteral(message.Substring(literalStart))); + return result.ToString(); + } + + private static readonly Regex MessageTokenRegex = new(@"\{\{|\}\}|\{(?[A-Za-z_][A-Za-z0-9_]*)\}|[{}]"); + + // With quote: false, FormatLiteral leaves double quotes unescaped, so quote and strip the outer pair instead + private static string FormatLiteral(string text) + { + var literal = SymbolDisplay.FormatLiteral(text, quote: true); + return literal.Substring(1, literal.Length - 2); } private static string GenerateAssertConditionClassNameForMethod(INamedTypeSymbol targetType, INamedTypeSymbol containingType, string methodName, IMethodSymbol method) diff --git a/src/TUnit.Assertions/Attributes/AssertionFromAttribute.cs b/src/TUnit.Assertions/Attributes/AssertionFromAttribute.cs index 3c749ababb3..c832e56714f 100644 --- a/src/TUnit.Assertions/Attributes/AssertionFromAttribute.cs +++ b/src/TUnit.Assertions/Attributes/AssertionFromAttribute.cs @@ -134,6 +134,17 @@ public AssertionFromAttribute(Type targetType, Type containingType, string metho /// Optional custom expectation message shown in assertion failures. /// If not specified, a default message based on the method name will be used. /// + /// + /// + /// Write the positive phrase without the leading "to", e.g. "be null or empty": the generator adds "to", + /// and "not" when NegateLogic is set. A leading "to " is ignored, and a phrase that already starts with + /// "not " (after any "to ") is used as written instead of being negated again. + /// + /// + /// A placeholder such as {value} is replaced with that method parameter's value. Everything else is literal + /// text: write {{ and }} for literal braces; a placeholder that names no parameter is printed as is. + /// + /// /// /// /// [AssertionFrom(typeof(double), "IsNaN", ExpectationMessage = "be NaN")] diff --git a/src/TUnit.Assertions/Attributes/AssertionFromAttribute`1.cs b/src/TUnit.Assertions/Attributes/AssertionFromAttribute`1.cs index 316dfa36e30..3ddf21c2a87 100644 --- a/src/TUnit.Assertions/Attributes/AssertionFromAttribute`1.cs +++ b/src/TUnit.Assertions/Attributes/AssertionFromAttribute`1.cs @@ -130,7 +130,13 @@ public AssertionFromAttribute(Type containingType, string methodName) /// It appears as: "Expected {value} to {expectation} but {actual_result}". /// /// - /// You can use parameter placeholders like {param1}, {param2} which will be replaced with actual values. + /// Write the positive phrase without the leading "to", e.g. "be null or empty": the generator adds "to", + /// and "not" when NegateLogic is set. A leading "to " is ignored, and a phrase that already starts with + /// "not " (after any "to ") is used as written instead of being negated again. + /// + /// + /// A placeholder such as {value} is replaced with that method parameter's value. Everything else is literal + /// text: write {{ and }} for literal braces; a placeholder that names no parameter is printed as is. /// /// /// diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs b/tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs new file mode 100644 index 00000000000..554860262d1 --- /dev/null +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs @@ -0,0 +1,91 @@ +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using TUnit.Assertions.SourceGenerator.Generators; + +namespace TUnit.Assertions.SourceGenerator.Tests; + +/// +/// Runs over every built-in [AssertionFrom] declaration, including the files +/// without a snapshot test, and checks the rendered expectations read as one sentence. +/// +internal class BuiltInExpectationMessageTests +{ + private static readonly Regex ExpectationRegex = new( + @"protected override string GetExpectation\(\)\s*\{\s*return (?[^\r\n]*);"); + + private static readonly Regex RepeatedWordRegex = new( + @"\b(to|not) \1\b|\{\(_negated \? ""not "" : """"\)\}(to|not) "); + + public static IEnumerable AssertionFromFiles() => + Directory.EnumerateFiles(Path.Combine(Git.SourceDirectory.FullName, "TUnit.Assertions"), "*.cs", SearchOption.AllDirectories) + .Where(file => File.ReadLines(file).Any(line => line.TrimStart().StartsWith("[AssertionFrom", StringComparison.Ordinal))) + .Select(file => file.Substring(Git.SourceDirectory.FullName.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)) + .OrderBy(file => file, StringComparer.Ordinal); + + [Test] + [MethodDataSource(nameof(AssertionFromFiles))] + public async Task Expectations_Do_Not_Repeat_To_Or_Not(string file) + { + var expectations = GenerateExpectations(Path.Combine(Git.SourceDirectory.FullName, file)); + + await Assert.That(expectations.Where(expectation => RepeatedWordRegex.IsMatch(expectation))).IsEmpty(); + } + + [Test] + public async Task Built_In_Declarations_Render_Expectations() + { + // Guards the test above against passing vacuously, e.g. if the source files stop resolving. + var expectations = AssertionFromFiles() + .SelectMany(file => GenerateExpectations(Path.Combine(Git.SourceDirectory.FullName, file))) + .ToArray(); + + await Assert.That(expectations.Length).IsGreaterThan(100); + } + + private static string[] GenerateExpectations(string file) + { +#if NET + string[] preprocessorSymbols = ["NET", "NET5_0_OR_GREATER", "NET6_0_OR_GREATER", "NET7_0_OR_GREATER", "NET8_0_OR_GREATER"]; +#else + string[] preprocessorSymbols = []; +#endif + var parseOptions = CSharpParseOptions.Default.WithPreprocessorSymbols(preprocessorSymbols); + + var compilation = CSharpCompilation.Create( + nameof(BuiltInExpectationMessageTests), + [ + CSharpSyntaxTree.ParseText(File.ReadAllText(file), parseOptions), + CSharpSyntaxTree.ParseText( + """ + global using global::System; + global using global::System.Collections.Generic; + global using global::System.IO; + global using global::System.Linq; + global using global::System.Net.Http; + global using global::System.Threading; + global using global::System.Threading.Tasks; + """, + parseOptions) + ], + ReferencesHelper.References, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var result = CSharpGeneratorDriver.Create(new AssertionMethodGenerator()) + .WithUpdatedParseOptions(parseOptions) + .RunGenerators(compilation) + .GetRunResult() + .Results + .Single(); + + if (result.Exception is not null) + { + throw new InvalidOperationException($"The generator failed for {file}.", result.Exception); + } + + return result.GeneratedSources + .SelectMany(source => ExpectationRegex.Matches(source.SourceText.ToString()).Cast()) + .Select(match => match.Groups["expectation"].Value) + .ToArray(); + } +} diff --git a/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs b/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs index 1ec03c6962c..93f27fa0b6b 100644 --- a/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs +++ b/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs @@ -13,6 +13,21 @@ namespace TUnit.Assertions.Tests; [AssertionFrom(nameof(string.Contains), CustomName = "LacksTextDocumentedStyle", NegateLogic = true, ExpectationMessage = "to not contain '{value}'")] public static partial class DocumentedStyleStringAssertions; +// The generator shares one condition class per target method, so these use their own methods to get their own messages. +[AssertionFrom(typeof(ExpectationMessageMethods), nameof(ExpectationMessageMethods.HasText), ExpectationMessage = "contain {{text}} ({text})")] +[AssertionFrom(typeof(ExpectationMessageMethods), nameof(ExpectationMessageMethods.HasSuffix), ExpectationMessage = "end with {suffix}")] +[AssertionFrom(typeof(ExpectationMessageMethods), nameof(ExpectationMessageMethods.IsEmptyText), ExpectationMessage = "be \"blank\" \\ empty")] +public static partial class EscapingStringAssertions; + +public static class ExpectationMessageMethods +{ + public static bool HasText(string value, string text) => value.Contains(text); + + public static bool HasSuffix(string value, string ending) => value.EndsWith(ending, StringComparison.Ordinal); + + public static bool IsEmptyText(string value) => value.Length == 0; +} + public class AssertionFromExpectationMessageTests { [Test] @@ -54,4 +69,28 @@ await Assert.That(async () => await Assert.That("hello").LacksTextDocumentedStyl .Throws() .WithMessageContaining("Expected to not contain 'ell'"); } + + [Test] + public async Task Escaped_Braces_Stay_Literal() + { + await Assert.That(async () => await Assert.That("hello").HasText("xyz")) + .Throws() + .WithMessageContaining("Expected to contain {text} (xyz)"); + } + + [Test] + public async Task Placeholder_That_Names_No_Parameter_Stays_Literal() + { + await Assert.That(async () => await Assert.That("hello").HasSuffix("xyz")) + .Throws() + .WithMessageContaining("Expected to end with {suffix}"); + } + + [Test] + public async Task Quotes_And_Backslashes_Stay_Literal() + { + await Assert.That(async () => await Assert.That("Foo").IsEmptyText()) + .Throws() + .WithMessageContaining("Expected to be \"blank\" \\ empty"); + } } From 349b3f42b9779af742c0c8eb94458bd55bc5f298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 21:47:20 +0000 Subject: [PATCH 29/49] test: note that DotNetCli's SDK check ignores global.json --- tests/SharedTestHelpers/DotNetCli.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/SharedTestHelpers/DotNetCli.cs b/tests/SharedTestHelpers/DotNetCli.cs index 79066234d29..6a2a7d3e86c 100644 --- a/tests/SharedTestHelpers/DotNetCli.cs +++ b/tests/SharedTestHelpers/DotNetCli.cs @@ -14,7 +14,8 @@ namespace TUnit.Tests.Shared; /// PATH than a terminal). That dotnet can lack an SDK satisfying global.json and exit with /// 155 (SdkResolveFailure). Prefer the installation that is running the current test process, as long as it /// has an SDK: a runtime-only installation cannot run -/// dotnet build, so PATH is the better bet there. +/// dotnet build, so PATH is the better bet there. The SDK check is a heuristic: it only looks for an +/// installed SDK, not one that satisfies global.json. /// internal static class DotNetCli { From 630da587f39cec14e8f9ba985de1226f6cb06e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 21:56:54 +0000 Subject: [PATCH 30/49] fix(assertions): keep each [AssertionFrom] message when attributes share a method A condition class is named after its target method, and a second class with the same name was dropped at emit time, so two [AssertionFrom] attributes on one method with different expectation messages (or other settings that change the class) shared the first one's class and message. Identical classes are still shared, as for IsNullOrEmpty and its NegateLogic counterpart; a class whose source differs now gets a numbered name, and its attribute's extension methods use it. Built-in output is unchanged. --- .../Generators/AssertionMethodGenerator.cs | 67 +++++++++++++++++-- .../Conditions/SpanAssertionExtensions.cs | 24 ------- .../AssertionFromExpectationMessageTests.cs | 30 ++++++++- 3 files changed, 90 insertions(+), 31 deletions(-) delete mode 100644 src/TUnit.Assertions/Conditions/SpanAssertionExtensions.cs diff --git a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs index 1cb8ba773d5..7e5fb41f674 100644 --- a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs +++ b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs @@ -323,7 +323,7 @@ private static void GenerateAssertionsForClass( } // Track all generated classes globally to avoid duplicates across different extension classes - var allGeneratedClasses = new HashSet(); + var allGeneratedClasses = new GeneratedConditionClasses(); // Group by class and generate one file per class. Non-generic attribute data comes first, // then generic, so a class using both keeps its existing member order. @@ -337,6 +337,45 @@ private static void GenerateAssertionsForClass( } } + private static string RenameClass(string source, string className, string newName) => + className == newName + ? source + : Regex.Replace(source, $@"\b{Regex.Escape(className)}\b", newName); + + /// + /// The condition classes emitted so far, across every extension class. A condition class is named after its + /// target method, so attributes on the same method share it, which is fine while their sources match (e.g. + /// IsNullOrEmpty and its NegateLogic counterpart, which passes the negation to the constructor). + /// A class with the same name but a different source, such as a different ExpectationMessage, gets a + /// numbered name instead of being dropped, so its attribute keeps its own message. + /// + private sealed class GeneratedConditionClasses + { + private readonly HashSet _names = new(StringComparer.Ordinal); + private readonly Dictionary<(string ClassName, string Source), string> _emitted = new(); + + /// + /// Returns the name to use for , and whether it still has to be emitted. + /// + public (string Name, bool IsNew) Claim(ConditionClassModel conditionClass) + { + var key = (conditionClass.ClassName, conditionClass.Source); + if (_emitted.TryGetValue(key, out var existing)) + { + return (existing, false); + } + + var name = conditionClass.ClassName; + for (var suffix = 2; !_names.Add(name); suffix++) + { + name = conditionClass.ClassName + suffix.ToString(CultureInfo.InvariantCulture); + } + + _emitted.Add(key, name); + return (name, true); + } + } + private static AssertionClassModel BuildClassModel(INamedTypeSymbol classSymbol, List attributeDataList) { var entries = new List(attributeDataList.Count); @@ -357,7 +396,7 @@ private static void GenerateAssertionsForSpecificClass( string className, string? originalNamespace, AssertionEntryModel[] entries, - HashSet allGeneratedClasses) + GeneratedConditionClasses allGeneratedClasses) { var sourceBuilder = new StringBuilder(); // Always generate extension methods in TUnit.Assertions.Extensions namespace @@ -388,6 +427,7 @@ private static void GenerateAssertionsForSpecificClass( } // Generate all assert condition classes first + var extensionMethodSources = new List(entries.Length); foreach (var entry in entries) { if (entry.MissingMethodMessage is not null) @@ -403,24 +443,39 @@ private static void GenerateAssertionsForSpecificClass( Location.None)); } + var extensionMethodsSource = entry.ExtensionMethodsSource; + var namesInEntry = new HashSet(StringComparer.Ordinal); foreach (var conditionClass in entry.ConditionClasses) { - if (!allGeneratedClasses.Add(conditionClass.ClassName)) + // Overloads that map to the same class name can't be told apart in the extension methods, so the + // first one wins, as before + if (!namesInEntry.Add(conditionClass.ClassName)) { continue; } - sourceBuilder.Append(conditionClass.Source); + var (name, isNew) = allGeneratedClasses.Claim(conditionClass); + if (name != conditionClass.ClassName) + { + extensionMethodsSource = RenameClass(extensionMethodsSource, conditionClass.ClassName, name); + } + + if (isNew) + { + sourceBuilder.Append(RenameClass(conditionClass.Source, conditionClass.ClassName, name)); + } } + + extensionMethodSources.Add(extensionMethodsSource); } // Generate extension methods class sourceBuilder.AppendLine($"public static partial class {className}"); sourceBuilder.AppendLine("{"); - foreach (var entry in entries) + foreach (var extensionMethodsSource in extensionMethodSources) { - sourceBuilder.Append(entry.ExtensionMethodsSource); + sourceBuilder.Append(extensionMethodsSource); } sourceBuilder.AppendLine("}"); diff --git a/src/TUnit.Assertions/Conditions/SpanAssertionExtensions.cs b/src/TUnit.Assertions/Conditions/SpanAssertionExtensions.cs deleted file mode 100644 index f87d05bd0f6..00000000000 --- a/src/TUnit.Assertions/Conditions/SpanAssertionExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using TUnit.Assertions.Attributes; - -namespace TUnit.Assertions.Conditions; - -#if NET5_0_OR_GREATER -using System; - -/// -/// Source-generated assertions for Memory<T> and ReadOnlyMemory<T> types using [AssertionFrom] attributes. -/// Note: Span types cannot be used due to ref struct constraints in the assertion framework. -/// Available in .NET 5.0+ -/// - -// Memory assertions -[AssertionFrom(typeof(Memory<>), nameof(Memory.IsEmpty), ExpectationMessage = "be empty")] -[AssertionFrom(typeof(Memory<>), nameof(Memory.IsEmpty), CustomName = "IsNotEmpty", NegateLogic = true, ExpectationMessage = "be empty")] - -// ReadOnlyMemory assertions -[AssertionFrom(typeof(ReadOnlyMemory<>), nameof(ReadOnlyMemory.IsEmpty), ExpectationMessage = "be empty")] -[AssertionFrom(typeof(ReadOnlyMemory<>), nameof(ReadOnlyMemory.IsEmpty), CustomName = "IsNotEmpty", NegateLogic = true, ExpectationMessage = "be empty")] -public static partial class SpanAssertionExtensions -{ -} -#endif diff --git a/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs b/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs index 93f27fa0b6b..d9d2fe5dbb8 100644 --- a/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs +++ b/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs @@ -11,9 +11,13 @@ namespace TUnit.Assertions.Tests; [AssertionFrom(nameof(string.IsNullOrWhiteSpace), CustomName = "HasTextDocumentedStyle", NegateLogic = true, ExpectationMessage = "to not be null, empty, or whitespace")] [AssertionFrom(nameof(string.StartsWith), CustomName = "BeginsWithTextDocumentedStyle", ExpectationMessage = "to start with '{value}'")] [AssertionFrom(nameof(string.Contains), CustomName = "LacksTextDocumentedStyle", NegateLogic = true, ExpectationMessage = "to not contain '{value}'")] +[AssertionFrom(nameof(string.Contains), CustomName = "HasTextSharingMethod", ExpectationMessage = "contain '{value}'")] public static partial class DocumentedStyleStringAssertions; -// The generator shares one condition class per target method, so these use their own methods to get their own messages. +// Same target method as an attribute above, but declared on another class and with another message. +[AssertionFrom(nameof(string.IsNullOrEmpty), CustomName = "IsVacant", ExpectationMessage = "be vacant")] +public static partial class SharedMethodStringAssertions; + [AssertionFrom(typeof(ExpectationMessageMethods), nameof(ExpectationMessageMethods.HasText), ExpectationMessage = "contain {{text}} ({text})")] [AssertionFrom(typeof(ExpectationMessageMethods), nameof(ExpectationMessageMethods.HasSuffix), ExpectationMessage = "end with {suffix}")] [AssertionFrom(typeof(ExpectationMessageMethods), nameof(ExpectationMessageMethods.IsEmptyText), ExpectationMessage = "be \"blank\" \\ empty")] @@ -93,4 +97,28 @@ await Assert.That(async () => await Assert.That("Foo").IsEmptyText()) .Throws() .WithMessageContaining("Expected to be \"blank\" \\ empty"); } + + [Test] + public async Task Attributes_Sharing_A_Method_Keep_Their_Own_Messages() + { + await Assert.That(async () => await Assert.That("hello").HasTextSharingMethod("xyz")) + .Throws() + .WithMessageContaining("Expected to contain 'xyz'"); + + await Assert.That(async () => await Assert.That("hello").LacksTextDocumentedStyle("ell")) + .Throws() + .WithMessageContaining("Expected to not contain 'ell'"); + } + + [Test] + public async Task Attributes_Sharing_A_Method_Across_Classes_Keep_Their_Own_Messages() + { + await Assert.That(async () => await Assert.That("Foo").IsVacant()) + .Throws() + .WithMessageContaining("Expected to be vacant"); + + await Assert.That(async () => await Assert.That("Foo").IsBlankDocumentedStyle()) + .Throws() + .WithMessageContaining("Expected to be null or empty"); + } } From 427f034a427a013e8274ce27c43363066c414ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 21:56:54 +0000 Subject: [PATCH 31/49] refactor(assertions): drop the [AssertionFrom] Memory declarations that generated nothing SpanAssertionExtensions declared IsEmpty/IsNotEmpty for Memory<> and ReadOnlyMemory<>, but the generator never produced anything for them: it cannot find System.Object from CoreLib to construct the unbound type, and the unbound-generic code path does not produce compilable code once it does. The hand-written MemoryAssertionBase already provides IsEmpty() and IsNotEmpty() for both types, so remove the dead declarations and the two empty public SpanAssertionExtensions classes they left behind. --- ...tions_Library_Has_No_API_Changes.DotNet10_0.verified.txt | 6 ------ ...rtions_Library_Has_No_API_Changes.DotNet8_0.verified.txt | 6 ------ ...rtions_Library_Has_No_API_Changes.DotNet9_0.verified.txt | 6 ------ 3 files changed, 18 deletions(-) diff --git a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt index a3bc5816772..a76b8642bd6 100644 --- a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -1994,11 +1994,6 @@ namespace .Conditions [.("IsSubnormal", ExpectationMessage="be subnormal")] public static class SingleAssertionExtensions { } public sealed class SingleItemSource : . { } - [.(typeof(), "IsEmpty", CustomName="IsNotEmpty", ExpectationMessage="be empty", NegateLogic=true)] - [.(typeof(), "IsEmpty", ExpectationMessage="be empty")] - [.(typeof(), "IsEmpty", CustomName="IsNotEmpty", ExpectationMessage="be empty", NegateLogic=true)] - [.(typeof(), "IsEmpty", ExpectationMessage="be empty")] - public static class SpanAssertionExtensions { } [.("Contains")] public class StringContainsAssertion : . { @@ -6101,7 +6096,6 @@ namespace .Extensions public static .<., TItem> SetEquals(this .<.> source, . other, [.("other")] string? expression = null) { } public static .<., TItem> SetEquals(this .<.> source, . other, [.("other")] string? expression = null) { } } - public static class SpanAssertionExtensions { } public static class StreamAssertionExtensions { public static . CanRead(this .<.Stream> source) { } diff --git a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt index f088c0c775e..e9c7cbb777d 100644 --- a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -1976,11 +1976,6 @@ namespace .Conditions [.("IsSubnormal", ExpectationMessage="be subnormal")] public static class SingleAssertionExtensions { } public sealed class SingleItemSource : . { } - [.(typeof(), "IsEmpty", CustomName="IsNotEmpty", ExpectationMessage="be empty", NegateLogic=true)] - [.(typeof(), "IsEmpty", ExpectationMessage="be empty")] - [.(typeof(), "IsEmpty", CustomName="IsNotEmpty", ExpectationMessage="be empty", NegateLogic=true)] - [.(typeof(), "IsEmpty", ExpectationMessage="be empty")] - public static class SpanAssertionExtensions { } [.("Contains")] public class StringContainsAssertion : . { @@ -6009,7 +6004,6 @@ namespace .Extensions public static .<., TItem> SetEquals(this .<.> source, . other, [.("other")] string? expression = null) { } public static .<., TItem> SetEquals(this .<.> source, . other, [.("other")] string? expression = null) { } } - public static class SpanAssertionExtensions { } public static class StreamAssertionExtensions { public static . CanRead(this .<.Stream> source) { } diff --git a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt index 3af6e98b1a7..440c79b648b 100644 --- a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -1994,11 +1994,6 @@ namespace .Conditions [.("IsSubnormal", ExpectationMessage="be subnormal")] public static class SingleAssertionExtensions { } public sealed class SingleItemSource : . { } - [.(typeof(), "IsEmpty", CustomName="IsNotEmpty", ExpectationMessage="be empty", NegateLogic=true)] - [.(typeof(), "IsEmpty", ExpectationMessage="be empty")] - [.(typeof(), "IsEmpty", CustomName="IsNotEmpty", ExpectationMessage="be empty", NegateLogic=true)] - [.(typeof(), "IsEmpty", ExpectationMessage="be empty")] - public static class SpanAssertionExtensions { } [.("Contains")] public class StringContainsAssertion : . { @@ -6101,7 +6096,6 @@ namespace .Extensions public static .<., TItem> SetEquals(this .<.> source, . other, [.("other")] string? expression = null) { } public static .<., TItem> SetEquals(this .<.> source, . other, [.("other")] string? expression = null) { } } - public static class SpanAssertionExtensions { } public static class StreamAssertionExtensions { public static . CanRead(this .<.Stream> source) { } From c1beaf49faf923c508d1a9d37dda66e85dfddc60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 22:40:35 +0000 Subject: [PATCH 32/49] fix(assertions): substitute non-ASCII and {_name} placeholders in [AssertionFrom] messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The single-pass tokenizer only recognised ASCII identifiers, so a placeholder for a parameter such as `önskad` printed literally, and it treated `{_value}`, which messages could reference while they were emitted verbatim, as unknown text. Treat any brace-free text between single braces as a placeholder candidate, substitute it when it names a parameter (with or without the leading underscore), and keep everything else literal, escaping quotes and backslashes inside it too. Also pin the numbered class names for three attributes on one method. --- .../Generators/AssertionMethodGenerator.cs | 30 +++++++---- .../AssertionFromExpectationMessageTests.cs | 54 +++++++++++++++++++ 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs index 7e5fb41f674..1211fb92363 100644 --- a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs +++ b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs @@ -1076,9 +1076,10 @@ private static string RenderCustomExpectation(string expectationMessage, IParame /// /// Renders message text as the body of an interpolated string literal in a single pass: a {name} - /// placeholder for a method parameter becomes the generated {_name} field, and everything else stays - /// literal text, including escaped braces ({{name}} prints {name}), placeholders that name no - /// parameter, stray braces, quotes and backslashes. + /// placeholder for a method parameter (any valid identifier, not only ASCII) becomes the generated + /// {_name} field, as does {_name} itself, which messages could reference before placeholders were + /// parsed. Everything else stays literal text, including escaped braces ({{name}} prints + /// {name}), placeholders that name no parameter, stray braces, quotes and backslashes. /// private static string RenderMessageText(string message, IParameterSymbol[] parameters) { @@ -1091,15 +1092,24 @@ private static string RenderMessageText(string message, IParameterSymbol[] param result.Append(FormatLiteral(message.Substring(literalStart, match.Index - literalStart))); var name = match.Groups["name"]; - if (name.Success && parameterNames.Contains(name.Value)) + var parameterName = !name.Success ? null + : parameterNames.Contains(name.Value) ? name.Value + : name.Value.StartsWith("_", StringComparison.Ordinal) && parameterNames.Contains(name.Value.Substring(1)) ? name.Value.Substring(1) + : null; + + if (parameterName is not null) + { + result.Append("{_").Append(parameterName).Append('}'); + } + else if (name.Success) { - result.Append("{_").Append(name.Value).Append('}'); + // A placeholder that names no parameter: keep its text, braces included, literal + result.Append("{{").Append(FormatLiteral(name.Value)).Append("}}"); } else { - // An escaped brace pair, an unknown placeholder or a stray brace: keep its text literal - var literal = match.Value is "{{" ? "{" : match.Value is "}}" ? "}" : match.Value; - result.Append(literal.Replace("{", "{{").Replace("}", "}}")); + // An escaped brace pair or a stray brace: keep the brace literal + result.Append(match.Value[0] is '{' ? "{{" : "}}"); } literalStart = match.Index + match.Length; @@ -1109,7 +1119,9 @@ private static string RenderMessageText(string message, IParameterSymbol[] param return result.ToString(); } - private static readonly Regex MessageTokenRegex = new(@"\{\{|\}\}|\{(?[A-Za-z_][A-Za-z0-9_]*)\}|[{}]"); + // Any brace-free text between single braces is a placeholder candidate; it is only substituted when it names a + // parameter, so matching broadly keeps non-ASCII identifiers working without a hand-written identifier grammar + private static readonly Regex MessageTokenRegex = new(@"\{\{|\}\}|\{(?[^{}]*)\}|[{}]"); // With quote: false, FormatLiteral leaves double quotes unescaped, so quote and strip the outer pair instead private static string FormatLiteral(string text) diff --git a/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs b/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs index d9d2fe5dbb8..7214a2489f2 100644 --- a/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs +++ b/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs @@ -21,8 +21,16 @@ public static partial class SharedMethodStringAssertions; [AssertionFrom(typeof(ExpectationMessageMethods), nameof(ExpectationMessageMethods.HasText), ExpectationMessage = "contain {{text}} ({text})")] [AssertionFrom(typeof(ExpectationMessageMethods), nameof(ExpectationMessageMethods.HasSuffix), ExpectationMessage = "end with {suffix}")] [AssertionFrom(typeof(ExpectationMessageMethods), nameof(ExpectationMessageMethods.IsEmptyText), ExpectationMessage = "be \"blank\" \\ empty")] +[AssertionFrom(typeof(ExpectationMessageMethods), nameof(ExpectationMessageMethods.HasWanted), ExpectationMessage = "contain {önskad}")] +[AssertionFrom(typeof(ExpectationMessageMethods), nameof(ExpectationMessageMethods.HasPrefix), ExpectationMessage = "start with {_prefix}")] public static partial class EscapingStringAssertions; +// Three attributes on one method, each with its own message: the condition classes get numbered names. +[AssertionFrom(nameof(string.EndsWith), CustomName = "EndsWithFirst", ExpectationMessage = "end with {value} (first)")] +[AssertionFrom(nameof(string.EndsWith), CustomName = "EndsWithSecond", ExpectationMessage = "end with {value} (second)")] +[AssertionFrom(nameof(string.EndsWith), CustomName = "EndsWithThird", ExpectationMessage = "end with {value} (third)")] +public static partial class ThreeMessagesStringAssertions; + public static class ExpectationMessageMethods { public static bool HasText(string value, string text) => value.Contains(text); @@ -30,6 +38,10 @@ public static class ExpectationMessageMethods public static bool HasSuffix(string value, string ending) => value.EndsWith(ending, StringComparison.Ordinal); public static bool IsEmptyText(string value) => value.Length == 0; + + public static bool HasWanted(string value, string önskad) => value.Contains(önskad); + + public static bool HasPrefix(string value, string prefix) => value.StartsWith(prefix, StringComparison.Ordinal); } public class AssertionFromExpectationMessageTests @@ -121,4 +133,46 @@ await Assert.That(async () => await Assert.That("Foo").IsBlankDocumentedStyle()) .Throws() .WithMessageContaining("Expected to be null or empty"); } + + [Test] + public async Task Non_Ascii_Placeholder_Is_Substituted() + { + await Assert.That(async () => await Assert.That("hello").HasWanted("xyz")) + .Throws() + .WithMessageContaining("Expected to contain xyz"); + } + + [Test] + public async Task Generated_Field_Placeholder_Is_Still_Substituted() + { + await Assert.That(async () => await Assert.That("hello").HasPrefix("xyz")) + .Throws() + .WithMessageContaining("Expected to start with xyz"); + } + + [Test] + public async Task Three_Attributes_Sharing_A_Method_Keep_Their_Own_Messages() + { + await Assert.That(async () => await Assert.That("hello").EndsWithFirst("xyz")) + .Throws() + .WithMessageContaining("Expected to end with xyz (first)"); + + await Assert.That(async () => await Assert.That("hello").EndsWithSecond("xyz")) + .Throws() + .WithMessageContaining("Expected to end with xyz (second)"); + + await Assert.That(async () => await Assert.That("hello").EndsWithThird("xyz")) + .Throws() + .WithMessageContaining("Expected to end with xyz (third)"); + } + + [Test] + public async Task Classes_Sharing_A_Method_Get_Numbered_Names() + { + var assembly = typeof(AssertionFromExpectationMessageTests).Assembly; + + await Assert.That(assembly.GetType("TUnit.Assertions.Extensions.StringEndsWithWithStringAssertion")).IsNotNull(); + await Assert.That(assembly.GetType("TUnit.Assertions.Extensions.StringEndsWithWithStringAssertion2")).IsNotNull(); + await Assert.That(assembly.GetType("TUnit.Assertions.Extensions.StringEndsWithWithStringAssertion3")).IsNotNull(); + } } From ca3dff5462f15fb32fb130ac2eff60240ac5c415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 22:40:35 +0000 Subject: [PATCH 33/49] fix(assertions): keep SpanAssertionExtensions as obsolete empty classes Removing the dead Memory declarations also removed the two public SpanAssertionExtensions classes (in TUnit.Assertions.Conditions and TUnit.Assertions.Extensions). They never had members, but removing a public type is still a breaking change, so keep both as empty [Obsolete] classes that point to MemoryAssertionBase, where IsEmpty() and IsNotEmpty() for Memory and ReadOnlyMemory live. --- .../Conditions/SpanAssertionExtensions.cs | 28 +++++++++++++++++++ ...Has_No_API_Changes.DotNet10_0.verified.txt | 6 ++++ ..._Has_No_API_Changes.DotNet8_0.verified.txt | 6 ++++ ..._Has_No_API_Changes.DotNet9_0.verified.txt | 6 ++++ 4 files changed, 46 insertions(+) create mode 100644 src/TUnit.Assertions/Conditions/SpanAssertionExtensions.cs diff --git a/src/TUnit.Assertions/Conditions/SpanAssertionExtensions.cs b/src/TUnit.Assertions/Conditions/SpanAssertionExtensions.cs new file mode 100644 index 00000000000..d8fabb81bfe --- /dev/null +++ b/src/TUnit.Assertions/Conditions/SpanAssertionExtensions.cs @@ -0,0 +1,28 @@ +#if NET5_0_OR_GREATER +using System; + +namespace TUnit.Assertions.Conditions +{ + /// + /// No longer holds any assertions. It only ever carried [AssertionFrom] declarations for Memory<T> and + /// ReadOnlyMemory<T> that generated nothing; IsEmpty() and IsNotEmpty() for those types are on the source + /// returned by Assert.That. Kept so that removing it is not a breaking change. + /// + [Obsolete("Has no members. Memory and ReadOnlyMemory assertions such as IsEmpty() are on MemoryAssertionBase.")] + public static class SpanAssertionExtensions + { + } +} + +namespace TUnit.Assertions.Extensions +{ + /// + /// No longer holds any assertions; see . + /// Kept so that removing it is not a breaking change. + /// + [Obsolete("Has no members. Memory and ReadOnlyMemory assertions such as IsEmpty() are on MemoryAssertionBase.")] + public static class SpanAssertionExtensions + { + } +} +#endif diff --git a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt index a76b8642bd6..2585928dfd4 100644 --- a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -1994,6 +1994,9 @@ namespace .Conditions [.("IsSubnormal", ExpectationMessage="be subnormal")] public static class SingleAssertionExtensions { } public sealed class SingleItemSource : . { } + [("Has no members. Memory and ReadOnlyMemory assertions such as IsEmpty() are " + + "on MemoryAssertionBase.")] + public static class SpanAssertionExtensions { } [.("Contains")] public class StringContainsAssertion : . { @@ -6096,6 +6099,9 @@ namespace .Extensions public static .<., TItem> SetEquals(this .<.> source, . other, [.("other")] string? expression = null) { } public static .<., TItem> SetEquals(this .<.> source, . other, [.("other")] string? expression = null) { } } + [("Has no members. Memory and ReadOnlyMemory assertions such as IsEmpty() are " + + "on MemoryAssertionBase.")] + public static class SpanAssertionExtensions { } public static class StreamAssertionExtensions { public static . CanRead(this .<.Stream> source) { } diff --git a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt index e9c7cbb777d..b14401b9cb5 100644 --- a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -1976,6 +1976,9 @@ namespace .Conditions [.("IsSubnormal", ExpectationMessage="be subnormal")] public static class SingleAssertionExtensions { } public sealed class SingleItemSource : . { } + [("Has no members. Memory and ReadOnlyMemory assertions such as IsEmpty() are " + + "on MemoryAssertionBase.")] + public static class SpanAssertionExtensions { } [.("Contains")] public class StringContainsAssertion : . { @@ -6004,6 +6007,9 @@ namespace .Extensions public static .<., TItem> SetEquals(this .<.> source, . other, [.("other")] string? expression = null) { } public static .<., TItem> SetEquals(this .<.> source, . other, [.("other")] string? expression = null) { } } + [("Has no members. Memory and ReadOnlyMemory assertions such as IsEmpty() are " + + "on MemoryAssertionBase.")] + public static class SpanAssertionExtensions { } public static class StreamAssertionExtensions { public static . CanRead(this .<.Stream> source) { } diff --git a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt index 440c79b648b..9bd6b424f9e 100644 --- a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -1994,6 +1994,9 @@ namespace .Conditions [.("IsSubnormal", ExpectationMessage="be subnormal")] public static class SingleAssertionExtensions { } public sealed class SingleItemSource : . { } + [("Has no members. Memory and ReadOnlyMemory assertions such as IsEmpty() are " + + "on MemoryAssertionBase.")] + public static class SpanAssertionExtensions { } [.("Contains")] public class StringContainsAssertion : . { @@ -6096,6 +6099,9 @@ namespace .Extensions public static .<., TItem> SetEquals(this .<.> source, . other, [.("other")] string? expression = null) { } public static .<., TItem> SetEquals(this .<.> source, . other, [.("other")] string? expression = null) { } } + [("Has no members. Memory and ReadOnlyMemory assertions such as IsEmpty() are " + + "on MemoryAssertionBase.")] + public static class SpanAssertionExtensions { } public static class StreamAssertionExtensions { public static . CanRead(this .<.Stream> source) { } From 8f3ba745ac10a2601f8019eb19a713b16abd0b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 22:52:20 +0000 Subject: [PATCH 34/49] docs: document the members this PR adds or changes Turn the explanatory comments above touched methods into XML doc comments and document the touched production and shared test helper members that had none (DotNetCli, TestHostReferences, the PublicAPI path scrubber, the [AssertionFrom] message rendering helpers and the built-in expectation guard test). Test methods, pipeline module overrides and test-local assertion helpers stay undocumented, as elsewhere in the repository. --- .../PublicMethodMissingTestAttributeAnalyzer.cs | 7 +++++-- src/TUnit.Aspire.Core/AspireFixture.cs | 6 ++++-- .../Generators/AssertionMethodGenerator.cs | 17 ++++++++++++++--- tests/SharedTestHelpers/DotNetCli.cs | 6 ++++++ tests/SharedTestHelpers/TestHostReferences.cs | 4 ++++ .../BuiltInExpectationMessageTests.cs | 8 ++++++++ .../AssertionFromExpectationMessageTests.cs | 4 ++++ tests/TUnit.PublicAPI/Tests.cs | 4 ++++ 8 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/TUnit.Analyzers/PublicMethodMissingTestAttributeAnalyzer.cs b/src/TUnit.Analyzers/PublicMethodMissingTestAttributeAnalyzer.cs index 014db2b5ffb..74a4fd2405b 100644 --- a/src/TUnit.Analyzers/PublicMethodMissingTestAttributeAnalyzer.cs +++ b/src/TUnit.Analyzers/PublicMethodMissingTestAttributeAnalyzer.cs @@ -75,8 +75,11 @@ private bool IsTestDataSource(IMethodSymbol methodSymbol, IMethodSymbol[] testMe return false; } - // A public method that implicitly implements an interface member (e.g. ITestStartEventReceiver.OnTestStart) - // cannot be made private, so the diagnostic's advice would not apply. + /// + /// Whether implicitly implements an interface member of its containing type (e.g. + /// ITestStartEventReceiver.OnTestStart). Such a method cannot be made private, so the diagnostic's advice + /// would not apply. + /// private static bool ImplementsInterfaceMember(IMethodSymbol method) { var containingType = method.ContainingType; diff --git a/src/TUnit.Aspire.Core/AspireFixture.cs b/src/TUnit.Aspire.Core/AspireFixture.cs index 675ddd13686..2df3d2713b6 100644 --- a/src/TUnit.Aspire.Core/AspireFixture.cs +++ b/src/TUnit.Aspire.Core/AspireFixture.cs @@ -1710,8 +1710,10 @@ internal static string FormatExitCode(int? exitCode) return DecodeExitCode(code) is { } meaning ? $"{number} ({meaning})" : number; } - // Invariant: exit codes are identifiers to search for, and cultures such as sv-SE would otherwise - // print negative Windows codes with U+2212 MINUS SIGN. + /// + /// Formats an exit code with the invariant culture: exit codes are identifiers to search for, and cultures such + /// as sv-SE would otherwise print negative Windows codes with U+2212 MINUS SIGN. + /// private static string FormatExitCodeNumber(int code) => code.ToString(CultureInfo.InvariantCulture); /// One-line human description of a resource's failure state for the concise exception message. diff --git a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs index 1211fb92363..a31bbc5335d 100644 --- a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs +++ b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs @@ -337,6 +337,10 @@ private static void GenerateAssertionsForClass( } } + /// + /// Replaces whole-word occurrences of in pre-rendered source. Generated code only + /// uses a condition class name as an identifier (declaration, constructor, new, return types, cref). + /// private static string RenameClass(string source, string className, string newName) => className == newName ? source @@ -1119,11 +1123,18 @@ private static string RenderMessageText(string message, IParameterSymbol[] param return result.ToString(); } - // Any brace-free text between single braces is a placeholder candidate; it is only substituted when it names a - // parameter, so matching broadly keeps non-ASCII identifiers working without a hand-written identifier grammar + /// + /// Tokens of an ExpectationMessage: escaped brace pairs, {...} placeholders and stray braces. Any + /// brace-free text between single braces is a placeholder candidate; it is only substituted when it names a + /// parameter, so matching broadly keeps non-ASCII identifiers working without a hand-written identifier grammar. + /// private static readonly Regex MessageTokenRegex = new(@"\{\{|\}\}|\{(?[^{}]*)\}|[{}]"); - // With quote: false, FormatLiteral leaves double quotes unescaped, so quote and strip the outer pair instead + /// + /// Escapes for use inside a C# string literal. With quote: false, + /// leaves double quotes unescaped, so this quotes the text + /// and strips the outer pair instead. + /// private static string FormatLiteral(string text) { var literal = SymbolDisplay.FormatLiteral(text, quote: true); diff --git a/tests/SharedTestHelpers/DotNetCli.cs b/tests/SharedTestHelpers/DotNetCli.cs index 6a2a7d3e86c..214f67026cb 100644 --- a/tests/SharedTestHelpers/DotNetCli.cs +++ b/tests/SharedTestHelpers/DotNetCli.cs @@ -19,8 +19,13 @@ namespace TUnit.Tests.Shared; /// internal static class DotNetCli { + /// The dotnet executable to start child processes with: a full path, or dotnet to use PATH. public static string Path { get; } = Resolve(); + /// + /// Prefers DOTNET_HOST_PATH, then the muxer of the installation running this process when it has an SDK, + /// and otherwise falls back to dotnet on PATH. + /// private static string Resolve() { // Set by the .NET CLI for processes it starts (e.g. under `dotnet test`). @@ -38,6 +43,7 @@ private static string Resolve() return File.Exists(muxer) && HasSdk(root) ? muxer : "dotnet"; } + /// Whether the installation at has at least one SDK under sdk/. private static bool HasSdk(string root) { var sdkDirectory = System.IO.Path.Combine(root, "sdk"); diff --git a/tests/SharedTestHelpers/TestHostReferences.cs b/tests/SharedTestHelpers/TestHostReferences.cs index b365f448410..f766b372c54 100644 --- a/tests/SharedTestHelpers/TestHostReferences.cs +++ b/tests/SharedTestHelpers/TestHostReferences.cs @@ -21,8 +21,12 @@ namespace TUnit.Tests.Shared; /// internal static class TestHostReferences { + /// The assembly paths to reference, sorted by file name and then by path. public static IReadOnlyList AssemblyPaths { get; } = Resolve(); + /// + /// Reads TRUSTED_PLATFORM_ASSEMBLIES on .NET, or the loaded assemblies on .NET Framework, and sorts them. + /// private static string[] Resolve() { var paths = AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string trustedPlatformAssemblies diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs b/tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs index 554860262d1..658fefc9009 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs @@ -17,6 +17,10 @@ internal class BuiltInExpectationMessageTests private static readonly Regex RepeatedWordRegex = new( @"\b(to|not) \1\b|\{\(_negated \? ""not "" : """"\)\}(to|not) "); + /// + /// The source files under src/TUnit.Assertions that declare [AssertionFrom], relative to src so + /// test names stay the same on every machine. + /// public static IEnumerable AssertionFromFiles() => Directory.EnumerateFiles(Path.Combine(Git.SourceDirectory.FullName, "TUnit.Assertions"), "*.cs", SearchOption.AllDirectories) .Where(file => File.ReadLines(file).Any(line => line.TrimStart().StartsWith("[AssertionFrom", StringComparison.Ordinal))) @@ -43,6 +47,10 @@ public async Task Built_In_Declarations_Render_Expectations() await Assert.That(expectations.Length).IsGreaterThan(100); } + /// + /// Runs over and returns the expression each + /// generated GetExpectation() returns. + /// private static string[] GenerateExpectations(string file) { #if NET diff --git a/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs b/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs index 7214a2489f2..47d5492eded 100644 --- a/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs +++ b/tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs @@ -31,6 +31,10 @@ public static partial class EscapingStringAssertions; [AssertionFrom(nameof(string.EndsWith), CustomName = "EndsWithThird", ExpectationMessage = "end with {value} (third)")] public static partial class ThreeMessagesStringAssertions; +/// +/// Bool-returning methods for the [AssertionFrom] declarations above, so each has its own target method and parameter +/// names that the messages refer to. +/// public static class ExpectationMessageMethods { public static bool HasText(string value, string text) => value.Contains(text); diff --git a/tests/TUnit.PublicAPI/Tests.cs b/tests/TUnit.PublicAPI/Tests.cs index c3e40d720a5..0d87dfd99fc 100644 --- a/tests/TUnit.PublicAPI/Tests.cs +++ b/tests/TUnit.PublicAPI/Tests.cs @@ -97,6 +97,10 @@ private string Scrub(string text) } + /// + /// A rooted ".cs" string literal, possibly wrapped into "..." + "..." pieces, followed by the caller line number + /// argument. See . + /// private const string SourcePathPattern = @"""(?:/|[A-Za-z]:(?:\\\\|/))[^""\r\n]*(?:""[ \t]*\+[ \t]*\r?\n[ \t]*""[^""\r\n]*)*\.cs""(?=,[ \t]*\d+[,)])"; From 1425adb1c163b6b18f748ecd129e92e62bd62b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 22:59:37 +0000 Subject: [PATCH 35/49] refactor(assertions): resolve generated class names through markers, warn on overload collisions Pre-rendered condition classes and extension methods now refer to a condition class by its name between U+0001 markers, and the emit step, the only place that knows the final (possibly numbered) name, swaps them with a plain replace. This replaces the whole-word regex rename, which could in principle touch other text. Generated output is unchanged. When two overloads of one [AssertionFrom] target map to the same class name but would generate different classes, only the first is emitted, as before, but the generator now reports warning TU0002 instead of doing so silently. No built-in declaration triggers it. Pin the GetExpectation() code rendered for each documented message form (canonical, leading "to", spelled-out "not", escaped braces, unknown, non-ASCII and {_name} placeholders, quotes and the default message) with generator-level tests that need no per-framework snapshots. --- .../Generators/AssertionMethodGenerator.cs | 55 ++++++++---- .../AssertionMethodGeneratorRunner.cs | 68 ++++++++++++++ .../BuiltInExpectationMessageTests.cs | 52 +---------- .../ExpectationMessageRenderingTests.cs | 89 +++++++++++++++++++ 4 files changed, 198 insertions(+), 66 deletions(-) create mode 100644 tests/TUnit.Assertions.SourceGenerator.Tests/AssertionMethodGeneratorRunner.cs create mode 100644 tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs diff --git a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs index a31bbc5335d..d2d9167bbad 100644 --- a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs +++ b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs @@ -337,14 +337,33 @@ private static void GenerateAssertionsForClass( } } + private static readonly DiagnosticDescriptor OverloadsShareConditionClass = new( + "TU0002", + "Overloads share a generated assertion class", + "More than one overload maps to the generated assertion class '{0}'; only the first one is generated, and the extension methods for the others use it", + "TUnit.Assertions", + DiagnosticSeverity.Warning, + true); + + private const char ClassNameMarker = '\u0001'; + + /// + /// How pre-rendered source refers to a condition class: its name between markers that cannot occur in C# source, + /// so the emit step, which alone knows the final name, can substitute it with a plain replace. + /// + private static string ClassNameReference(string className) => $"{ClassNameMarker}{className}{ClassNameMarker}"; + + /// Substitutes for every reference to the class. + private static string ResolveClassName(string source, string className, string name) => + source.Replace(ClassNameReference(className), name); + /// - /// Replaces whole-word occurrences of in pre-rendered source. Generated code only - /// uses a condition class name as an identifier (declaration, constructor, new, return types, cref). + /// Resolves any references left over (e.g. to a class an entry did not render) to the class's own name. /// - private static string RenameClass(string source, string className, string newName) => - className == newName + private static string ResolveRemainingClassNames(string source) => + source.IndexOf(ClassNameMarker) < 0 ? source - : Regex.Replace(source, $@"\b{Regex.Escape(className)}\b", newName); + : Regex.Replace(source, $"{ClassNameMarker}([^{ClassNameMarker}]*){ClassNameMarker}", "$1"); /// /// The condition classes emitted so far, across every extension class. A condition class is named after its @@ -448,29 +467,33 @@ private static void GenerateAssertionsForSpecificClass( } var extensionMethodsSource = entry.ExtensionMethodsSource; - var namesInEntry = new HashSet(StringComparer.Ordinal); + var sourcesInEntry = new Dictionary(StringComparer.Ordinal); foreach (var conditionClass in entry.ConditionClasses) { // Overloads that map to the same class name can't be told apart in the extension methods, so the - // first one wins, as before - if (!namesInEntry.Add(conditionClass.ClassName)) + // first one wins, as before; say so when the dropped one would have been different + if (sourcesInEntry.TryGetValue(conditionClass.ClassName, out var firstSource)) { + if (firstSource != conditionClass.Source) + { + context.ReportDiagnostic(Diagnostic.Create(OverloadsShareConditionClass, Location.None, conditionClass.ClassName)); + } + continue; } + sourcesInEntry.Add(conditionClass.ClassName, conditionClass.Source); + var (name, isNew) = allGeneratedClasses.Claim(conditionClass); - if (name != conditionClass.ClassName) - { - extensionMethodsSource = RenameClass(extensionMethodsSource, conditionClass.ClassName, name); - } + extensionMethodsSource = ResolveClassName(extensionMethodsSource, conditionClass.ClassName, name); if (isNew) { - sourceBuilder.Append(RenameClass(conditionClass.Source, conditionClass.ClassName, name)); + sourceBuilder.Append(ResolveClassName(conditionClass.Source, conditionClass.ClassName, name)); } } - extensionMethodSources.Add(extensionMethodsSource); + extensionMethodSources.Add(ResolveRemainingClassNames(extensionMethodsSource)); } // Generate extension methods class @@ -660,7 +683,7 @@ private static void GenerateAssertConditionClassForMethod(StringBuilder sourceBu var containingType = attributeData.ContainingType; var methodName = attributeData.MethodName; - var className = GenerateAssertConditionClassNameForMethod(attributeData.TargetType, attributeData.ContainingType, methodName, staticMethod); + var className = ClassNameReference(GenerateAssertConditionClassNameForMethod(attributeData.TargetType, attributeData.ContainingType, methodName, staticMethod)); // Determine which parameters to skip IParameterSymbol[] parameters; @@ -1257,7 +1280,7 @@ private static void GenerateMethodsForSpecificOverload(StringBuilder sourceBuild var methodName = attributeData.MethodName; var baseMethodName = attributeData.CustomName ?? methodName; - var className = GenerateAssertConditionClassNameForMethod(attributeData.TargetType, attributeData.ContainingType, methodName, staticMethod); + var className = ClassNameReference(GenerateAssertConditionClassNameForMethod(attributeData.TargetType, attributeData.ContainingType, methodName, staticMethod)); var actualMethodName = baseMethodName; if (staticMethod.Parameters.Length > 1) diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/AssertionMethodGeneratorRunner.cs b/tests/TUnit.Assertions.SourceGenerator.Tests/AssertionMethodGeneratorRunner.cs new file mode 100644 index 00000000000..a7b9cefae29 --- /dev/null +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/AssertionMethodGeneratorRunner.cs @@ -0,0 +1,68 @@ +using System.Collections.Immutable; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using TUnit.Assertions.SourceGenerator.Generators; + +namespace TUnit.Assertions.SourceGenerator.Tests; + +/// +/// Runs over a single source text, without snapshot verification, for tests +/// that assert on specific parts of its output. +/// +internal static class AssertionMethodGeneratorRunner +{ + private static readonly Regex ExpectationRegex = new( + @"protected override string GetExpectation\(\)\s*\{\s*return (?[^\r\n]*);"); + + /// Runs the generator over and returns its generated sources and diagnostics. + public static (string[] Sources, ImmutableArray Diagnostics) Run(string source) + { +#if NET + string[] preprocessorSymbols = ["NET", "NET5_0_OR_GREATER", "NET6_0_OR_GREATER", "NET7_0_OR_GREATER", "NET8_0_OR_GREATER"]; +#else + string[] preprocessorSymbols = []; +#endif + var parseOptions = CSharpParseOptions.Default.WithPreprocessorSymbols(preprocessorSymbols); + + var compilation = CSharpCompilation.Create( + nameof(AssertionMethodGeneratorRunner), + [ + CSharpSyntaxTree.ParseText(source, parseOptions), + CSharpSyntaxTree.ParseText( + """ + global using global::System; + global using global::System.Collections.Generic; + global using global::System.IO; + global using global::System.Linq; + global using global::System.Net.Http; + global using global::System.Threading; + global using global::System.Threading.Tasks; + """, + parseOptions) + ], + ReferencesHelper.References, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var result = CSharpGeneratorDriver.Create(new AssertionMethodGenerator()) + .WithUpdatedParseOptions(parseOptions) + .RunGenerators(compilation) + .GetRunResult() + .Results + .Single(); + + if (result.Exception is not null) + { + throw new InvalidOperationException("The generator failed.", result.Exception); + } + + return (result.GeneratedSources.Select(x => x.SourceText.ToString()).ToArray(), result.Diagnostics); + } + + /// The expression each generated GetExpectation() in returns. + public static string[] Expectations(IEnumerable sources) => + sources + .SelectMany(source => ExpectationRegex.Matches(source).Cast()) + .Select(match => match.Groups["expectation"].Value) + .ToArray(); +} diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs b/tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs index 658fefc9009..339277746b6 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs @@ -1,6 +1,4 @@ using System.Text.RegularExpressions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using TUnit.Assertions.SourceGenerator.Generators; namespace TUnit.Assertions.SourceGenerator.Tests; @@ -11,9 +9,6 @@ namespace TUnit.Assertions.SourceGenerator.Tests; /// internal class BuiltInExpectationMessageTests { - private static readonly Regex ExpectationRegex = new( - @"protected override string GetExpectation\(\)\s*\{\s*return (?[^\r\n]*);"); - private static readonly Regex RepeatedWordRegex = new( @"\b(to|not) \1\b|\{\(_negated \? ""not "" : """"\)\}(to|not) "); @@ -51,49 +46,6 @@ public async Task Built_In_Declarations_Render_Expectations() /// Runs over and returns the expression each /// generated GetExpectation() returns. /// - private static string[] GenerateExpectations(string file) - { -#if NET - string[] preprocessorSymbols = ["NET", "NET5_0_OR_GREATER", "NET6_0_OR_GREATER", "NET7_0_OR_GREATER", "NET8_0_OR_GREATER"]; -#else - string[] preprocessorSymbols = []; -#endif - var parseOptions = CSharpParseOptions.Default.WithPreprocessorSymbols(preprocessorSymbols); - - var compilation = CSharpCompilation.Create( - nameof(BuiltInExpectationMessageTests), - [ - CSharpSyntaxTree.ParseText(File.ReadAllText(file), parseOptions), - CSharpSyntaxTree.ParseText( - """ - global using global::System; - global using global::System.Collections.Generic; - global using global::System.IO; - global using global::System.Linq; - global using global::System.Net.Http; - global using global::System.Threading; - global using global::System.Threading.Tasks; - """, - parseOptions) - ], - ReferencesHelper.References, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - - var result = CSharpGeneratorDriver.Create(new AssertionMethodGenerator()) - .WithUpdatedParseOptions(parseOptions) - .RunGenerators(compilation) - .GetRunResult() - .Results - .Single(); - - if (result.Exception is not null) - { - throw new InvalidOperationException($"The generator failed for {file}.", result.Exception); - } - - return result.GeneratedSources - .SelectMany(source => ExpectationRegex.Matches(source.SourceText.ToString()).Cast()) - .Select(match => match.Groups["expectation"].Value) - .ToArray(); - } + private static string[] GenerateExpectations(string file) => + AssertionMethodGeneratorRunner.Expectations(AssertionMethodGeneratorRunner.Run(File.ReadAllText(file)).Sources); } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs b/tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs new file mode 100644 index 00000000000..0c0e888636c --- /dev/null +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs @@ -0,0 +1,89 @@ +using TUnit.Assertions.SourceGenerator.Generators; + +namespace TUnit.Assertions.SourceGenerator.Tests; + +/// +/// Pins the GetExpectation() code renders for each documented form of +/// [AssertionFrom] ExpectationMessage, and the diagnostic for overloads that share a condition class. +/// +internal class ExpectationMessageRenderingTests +{ + private const string MessagesSource = + """ + using TUnit.Assertions.Attributes; + + namespace Rendering; + + public static class Methods + { + public static bool Canonical(string value) => true; + public static bool SpelledOutTo(string value) => true; + public static bool SpelledOutNot(string value, string text) => true; + public static bool EscapedBraces(string value, string text) => true; + public static bool UnknownPlaceholder(string value, string ending) => true; + public static bool NonAscii(string value, string önskad) => true; + public static bool GeneratedField(string value, string prefix) => true; + public static bool Quoted(string value) => true; + public static bool Default(string value, string text) => true; + } + + [AssertionFrom(typeof(Methods), nameof(Methods.Canonical), ExpectationMessage = "be blank")] + [AssertionFrom(typeof(Methods), nameof(Methods.SpelledOutTo), ExpectationMessage = "to be spelled out")] + [AssertionFrom(typeof(Methods), nameof(Methods.SpelledOutNot), NegateLogic = true, ExpectationMessage = "to not contain {text}")] + [AssertionFrom(typeof(Methods), nameof(Methods.EscapedBraces), ExpectationMessage = "contain {{text}} and {text}")] + [AssertionFrom(typeof(Methods), nameof(Methods.UnknownPlaceholder), ExpectationMessage = "end with {suffix}")] + [AssertionFrom(typeof(Methods), nameof(Methods.NonAscii), ExpectationMessage = "contain {önskad}")] + [AssertionFrom(typeof(Methods), nameof(Methods.GeneratedField), ExpectationMessage = "start with {_prefix}")] + [AssertionFrom(typeof(Methods), nameof(Methods.Quoted), ExpectationMessage = "be \"quoted\" \\ here")] + [AssertionFrom(typeof(Methods), nameof(Methods.Default))] + public static partial class MessageAssertions; + """; + + private static readonly Lazy RenderedExpectations = new(() => + AssertionMethodGeneratorRunner.Expectations(AssertionMethodGeneratorRunner.Run(MessagesSource).Sources)); + + [Test] + [Arguments("Canonical form", @"$""to {(_negated ? ""not "" : """")}be blank""")] + [Arguments("Leading 'to' is not repeated", @"$""to {(_negated ? ""not "" : """")}be spelled out""")] + [Arguments("Spelled-out 'not' is not negated again", @"$""to not contain {_text}""")] + [Arguments("Escaped braces stay literal", @"$""to {(_negated ? ""not "" : """")}contain {{text}} and {_text}""")] + [Arguments("Unknown placeholder stays literal", @"$""to {(_negated ? ""not "" : """")}end with {{suffix}}""")] + [Arguments("Non-ASCII placeholder", @"$""to {(_negated ? ""not "" : """")}contain {_önskad}""")] + [Arguments("Generated field placeholder", @"$""to {(_negated ? ""not "" : """")}start with {_prefix}""")] + [Arguments("Quotes and backslashes are escaped", @"$""to {(_negated ? ""not "" : """")}be \""quoted\"" \\ here""")] + [Arguments("Default message", @"$""to {(_negated ? ""not "" : """")}satisfy Default({_text})""")] + public async Task Renders_Expectation(string form, string expected) + { + await Assert.That(RenderedExpectations.Value).Contains(expected); + } + + [Test] + public async Task Overloads_Sharing_A_Condition_Class_Are_Reported() + { + var (_, diagnostics) = AssertionMethodGeneratorRunner.Run( + """ + using TUnit.Assertions.Attributes; + + namespace Rendering; + + public static class Overloads + { + public static bool Matches(string value, int count) => true; + public static bool Matches(string value, bool flag) => true; + } + + [AssertionFrom(typeof(Overloads), nameof(Overloads.Matches))] + public static partial class OverloadAssertions; + """); + + await Assert.That(diagnostics.Select(x => x.Id)).Contains("TU0002"); + } + + [Test] + public async Task Distinct_Overloads_Are_Not_Reported() + { + var (_, diagnostics) = AssertionMethodGeneratorRunner.Run(MessagesSource); + + await Assert.That(diagnostics.Select(x => x.Id)).DoesNotContain("TU0002"); + } +} From 66c5a3d9769c71057d8da6fd95245a1c72e3f6e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 23:12:52 +0000 Subject: [PATCH 36/49] test: check that the running .NET installation resolves an SDK for this repository DotNetCli preferred the running installation's muxer whenever its sdk/ folder had any subdirectory, which says nothing about global.json, so a child build could still fail with exit code 155. Run `dotnet --version` with that muxer from the test assembly's directory instead: it applies global.json the same way the child builds do and fails exactly when they would, in which case `dotnet` on PATH is used. `dotnet --list-sdks` would not help here, since it ignores global.json too. --- tests/SharedTestHelpers/DotNetCli.cs | 59 ++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/tests/SharedTestHelpers/DotNetCli.cs b/tests/SharedTestHelpers/DotNetCli.cs index 214f67026cb..65d19aee88c 100644 --- a/tests/SharedTestHelpers/DotNetCli.cs +++ b/tests/SharedTestHelpers/DotNetCli.cs @@ -1,6 +1,6 @@ using System; +using System.Diagnostics; using System.IO; -using System.Linq; using System.Runtime.InteropServices; namespace TUnit.Tests.Shared; @@ -13,9 +13,8 @@ namespace TUnit.Tests.Shared; /// installation it builds and runs tests with (on macOS, apps started from the Dock or Finder get a different /// PATH than a terminal). That dotnet can lack an SDK satisfying global.json and exit with /// 155 (SdkResolveFailure). Prefer the installation that is running the current test process, as long as it -/// has an SDK: a runtime-only installation cannot run -/// dotnet build, so PATH is the better bet there. The SDK check is a heuristic: it only looks for an -/// installed SDK, not one that satisfies global.json. +/// resolves an SDK for this repository: a runtime-only installation, or one whose SDKs don't satisfy +/// global.json, cannot run dotnet build here, so PATH is the better bet there. /// internal static class DotNetCli { @@ -23,12 +22,12 @@ internal static class DotNetCli public static string Path { get; } = Resolve(); /// - /// Prefers DOTNET_HOST_PATH, then the muxer of the installation running this process when it has an SDK, - /// and otherwise falls back to dotnet on PATH. + /// Prefers DOTNET_HOST_PATH, then the muxer of the installation running this process when it resolves an + /// SDK for this repository, and otherwise falls back to dotnet on PATH. /// private static string Resolve() { - // Set by the .NET CLI for processes it starts (e.g. under `dotnet test`). + // Set by the .NET CLI for processes it starts (e.g. under `dotnet test`), so it already resolved an SDK. var hostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); if (!string.IsNullOrEmpty(hostPath) && File.Exists(hostPath)) { @@ -40,13 +39,49 @@ private static string Resolve() var root = System.IO.Path.GetFullPath(System.IO.Path.Combine(runtimeDirectory, "..", "..", "..")); var muxer = System.IO.Path.Combine(root, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"); - return File.Exists(muxer) && HasSdk(root) ? muxer : "dotnet"; + return File.Exists(muxer) && ResolvesSdk(muxer) ? muxer : "dotnet"; } - /// Whether the installation at has at least one SDK under sdk/. - private static bool HasSdk(string root) + /// + /// Whether resolves an SDK from the test assembly's directory, which lies inside the + /// repository: dotnet --version applies global.json the same way the child builds do and fails, with + /// 155, exactly when they would. + /// + private static bool ResolvesSdk(string muxer) { - var sdkDirectory = System.IO.Path.Combine(root, "sdk"); - return Directory.Exists(sdkDirectory) && Directory.EnumerateDirectories(sdkDirectory).Any(); + try + { + using var process = Process.Start(new ProcessStartInfo(muxer, "--version") + { + WorkingDirectory = AppContext.BaseDirectory, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }); + + if (process is null) + { + return false; + } + + // Drain both streams so neither can fill up and block the process + var output = process.StandardOutput.ReadToEndAsync(); + var error = process.StandardError.ReadToEndAsync(); + + if (!process.WaitForExit(30_000)) + { + process.Kill(); + return false; + } + + _ = output.Result; + _ = error.Result; + return process.ExitCode == 0; + } + catch (Exception exception) when (exception is InvalidOperationException or System.ComponentModel.Win32Exception or IOException) + { + return false; + } } } From 0e6f2e09c739bb97a688bc998a53dc05644af178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 23:12:52 +0000 Subject: [PATCH 37/49] docs: write [AssertionFrom] expectation message examples in the canonical form Show the positive phrase without the leading "to", with NegateLogic adding the "not", and describe the spelled-out "to"/"not" forms as accepted for compatibility rather than as an equal alternative. --- .../source-generator-assertions.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/docs/assertions/extensibility/source-generator-assertions.md b/docs/docs/assertions/extensibility/source-generator-assertions.md index e618c796770..9ab29c22fc0 100644 --- a/docs/docs/assertions/extensibility/source-generator-assertions.md +++ b/docs/docs/assertions/extensibility/source-generator-assertions.md @@ -238,9 +238,9 @@ Use `[AssertionFrom]` to create assertions from existing methods in libraries or ```csharp using TUnit.Assertions.Attributes; -[AssertionFrom(nameof(string.IsNullOrEmpty), CustomName = "IsBlank", ExpectationMessage = "to be null or empty")] -[AssertionFrom(nameof(string.StartsWith), CustomName = "BeginsWithText", ExpectationMessage = "to start with '{value}'")] -[AssertionFrom(nameof(string.EndsWith), CustomName = "EndsWithText", ExpectationMessage = "to end with '{value}'")] +[AssertionFrom(nameof(string.IsNullOrEmpty), CustomName = "IsBlank", ExpectationMessage = "be null or empty")] +[AssertionFrom(nameof(string.StartsWith), CustomName = "BeginsWithText", ExpectationMessage = "start with '{value}'")] +[AssertionFrom(nameof(string.EndsWith), CustomName = "EndsWithText", ExpectationMessage = "end with '{value}'")] public static partial class StringAssertionExtensions { } @@ -256,7 +256,7 @@ await Assert.That("hello").BeginsWithText("he"); ### With Custom Names ```csharp -[AssertionFrom(nameof(string.Contains), CustomName = "Has", ExpectationMessage = "to have '{value}'")] +[AssertionFrom(nameof(string.Contains), CustomName = "Has", ExpectationMessage = "have '{value}'")] public static partial class StringAssertionExtensions { } @@ -271,7 +271,7 @@ await Assert.That("hello").Has("world"); // ❌ Fails: "Expected to have For `bool`-returning methods, you can generate negated versions: ```csharp -[AssertionFrom(nameof(string.Contains), CustomName = "LacksText", NegateLogic = true, ExpectationMessage = "to not contain '{value}'")] +[AssertionFrom(nameof(string.Contains), CustomName = "LacksText", NegateLogic = true, ExpectationMessage = "contain '{value}'")] public static partial class StringAssertionExtensions { } @@ -285,17 +285,17 @@ await Assert.That("hello").LacksText("ell"); // ❌ Fails: "Expected to not con ### How `ExpectationMessage` Is Rendered -For `[AssertionFrom]`, the message is the phrase that follows "Expected". Write it in the positive form, with or without the leading "to": +For `[AssertionFrom]`, the message is the phrase that follows "Expected to". Write it in the positive form, without the leading "to", as in the examples above: -- The generator adds "to", plus "not" when `NegateLogic = true`: `"be null or empty"` and `"to be null or empty"` both render as "to be null or empty", or "to not be null or empty" when negated. -- A phrase that already starts with "not" (`"to not contain '{value}'"`) is used as written rather than negated again. +- The generator adds "to", plus "not" when `NegateLogic = true`: `"contain '{value}'"` renders as "to contain 'x'", or "to not contain 'x'" when negated. +- For compatibility, a leading "to" is ignored (`"to be null or empty"` renders the same as `"be null or empty"`), and a phrase that already starts with "not" (`"to not contain '{value}'"`) is used as written rather than negated again. - `{paramName}` is replaced with that parameter's value. Everything else is literal text: write `{{` and `}}` for literal braces, and a placeholder that names no parameter is printed as is. ### Referencing Methods on Different Types ```csharp // Reference static methods from another type -[AssertionFrom(typeof(StringHelper), nameof(StringHelper.IsValidEmail), ExpectationMessage = "to be a valid email")] +[AssertionFrom(typeof(StringHelper), nameof(StringHelper.IsValidEmail), ExpectationMessage = "be a valid email")] public static partial class StringAssertionExtensions { } From 76f6200fdd1d9cc664393fa835f0bb45364c1e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 23:27:34 +0000 Subject: [PATCH 38/49] fix(assertions): report [AssertionFrom] diagnostics at the attribute, fail loudly on unresolved class names TU0001 (method not found) and TU0002 (overloads share a class) were reported with Location.None, so users got no file or line to go to. Keep the attribute's location, as an equatable path and span, on entries that have something to report, and report both diagnostics there. Entries without diagnostics carry no location, so edits above an attribute stay cached. TU0002 is now detected per entry when it is rendered. Drop the fallback that resolved leftover class-name markers to the bare name. No built-in declaration or test input ever reached it, and if a reference were ever missed, a stray marker that fails the build is better than silently binding to some class. Guard every built-in [AssertionFrom] file and the rendering test input against markers reaching generated source. --- .../Generators/AssertionMethodGenerator.cs | 77 ++++++++++++------- .../AssertionMethodGeneratorRunner.cs | 5 +- .../BuiltInExpectationMessageTests.cs | 9 +++ .../ExpectationMessageRenderingTests.cs | 18 ++++- 4 files changed, 80 insertions(+), 29 deletions(-) diff --git a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs index d2d9167bbad..b70e02ec932 100644 --- a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs +++ b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs @@ -8,6 +8,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; using TUnit.Assertions.SourceGenerator.Models; namespace TUnit.Assertions.SourceGenerator.Generators; @@ -137,7 +138,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) requiresGenericTypeParameter, treatAsInstance, expectationMessage, - isUnboundGeneric + isUnboundGeneric, + attributeData.ApplicationSyntaxReference?.GetSyntax().GetLocation() ); attributeDataList.Add(createAssertionAttributeData); @@ -245,7 +247,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) requiresGenericTypeParameter, treatAsInstance, expectationMessage, - isUnboundGeneric + isUnboundGeneric, + attributeData.ApplicationSyntaxReference?.GetSyntax().GetLocation() ); attributeDataList.Add(createAssertionAttributeData); @@ -349,7 +352,8 @@ private static void GenerateAssertionsForClass( /// /// How pre-rendered source refers to a condition class: its name between markers that cannot occur in C# source, - /// so the emit step, which alone knows the final name, can substitute it with a plain replace. + /// so the emit step, which alone knows the final name, can substitute it with a plain replace. A reference that is + /// never resolved leaves a marker behind, which fails the build loudly rather than binding to a wrong class. /// private static string ClassNameReference(string className) => $"{ClassNameMarker}{className}{ClassNameMarker}"; @@ -357,14 +361,6 @@ private static void GenerateAssertionsForClass( private static string ResolveClassName(string source, string className, string name) => source.Replace(ClassNameReference(className), name); - /// - /// Resolves any references left over (e.g. to a class an entry did not render) to the class's own name. - /// - private static string ResolveRemainingClassNames(string source) => - source.IndexOf(ClassNameMarker) < 0 - ? source - : Regex.Replace(source, $"{ClassNameMarker}([^{ClassNameMarker}]*){ClassNameMarker}", "$1"); - /// /// The condition classes emitted so far, across every extension class. A condition class is named after its /// target method, so attributes on the same method share it, which is fine while their sources match (e.g. @@ -463,27 +459,25 @@ private static void GenerateAssertionsForSpecificClass( "TUnit.Assertions", DiagnosticSeverity.Error, true), - Location.None)); + AttributeLocationModel.ToLocation(entry.AttributeLocation))); + } + + foreach (var collidingClassName in entry.CollidingClassNames) + { + context.ReportDiagnostic(Diagnostic.Create(OverloadsShareConditionClass, AttributeLocationModel.ToLocation(entry.AttributeLocation), collidingClassName)); } var extensionMethodsSource = entry.ExtensionMethodsSource; - var sourcesInEntry = new Dictionary(StringComparer.Ordinal); + var namesInEntry = new HashSet(StringComparer.Ordinal); foreach (var conditionClass in entry.ConditionClasses) { // Overloads that map to the same class name can't be told apart in the extension methods, so the - // first one wins, as before; say so when the dropped one would have been different - if (sourcesInEntry.TryGetValue(conditionClass.ClassName, out var firstSource)) + // first one wins, as before (reported above when the others would have been different) + if (!namesInEntry.Add(conditionClass.ClassName)) { - if (firstSource != conditionClass.Source) - { - context.ReportDiagnostic(Diagnostic.Create(OverloadsShareConditionClass, Location.None, conditionClass.ClassName)); - } - continue; } - sourcesInEntry.Add(conditionClass.ClassName, conditionClass.Source); - var (name, isNew) = allGeneratedClasses.Claim(conditionClass); extensionMethodsSource = ResolveClassName(extensionMethodsSource, conditionClass.ClassName, name); @@ -493,7 +487,7 @@ private static void GenerateAssertionsForSpecificClass( } } - extensionMethodSources.Add(ResolveRemainingClassNames(extensionMethodsSource)); + extensionMethodSources.Add(extensionMethodsSource); } // Generate extension methods class @@ -520,7 +514,20 @@ private static AssertionEntryModel RenderEntry(CreateAssertionAttributeData attr var conditionClasses = RenderConditionClasses(attributeData, out var missingMethodMessage); var extensionMethodsSource = RenderExtensionMethods(attributeData); - return new AssertionEntryModel(conditionClasses, missingMethodMessage, extensionMethodsSource); + // Overloads that map to the same class name but would generate different classes; only the first is emitted + var collidingClassNames = conditionClasses + .GroupBy(x => x.ClassName, StringComparer.Ordinal) + .Where(group => group.Select(x => x.Source).Distinct(StringComparer.Ordinal).Skip(1).Any()) + .Select(group => group.Key) + .ToImmutableEquatableArray(); + + // Only keep the location when there is something to report: it changes with every edit above the attribute, + // and would otherwise stop unrelated edits from being cached + var attributeLocation = missingMethodMessage is not null || collidingClassNames.Count > 0 + ? AttributeLocationModel.From(attributeData.AttributeLocation) + : null; + + return new AssertionEntryModel(conditionClasses, missingMethodMessage, extensionMethodsSource, collidingClassNames, attributeLocation); } private static ImmutableEquatableArray RenderConditionClasses(CreateAssertionAttributeData attributeData, out string? missingMethodMessage) @@ -1464,9 +1471,26 @@ ImmutableEquatableArray Entries private sealed record AssertionEntryModel( ImmutableEquatableArray ConditionClasses, string? MissingMethodMessage, - string ExtensionMethodsSource + string ExtensionMethodsSource, + ImmutableEquatableArray CollidingClassNames, + AttributeLocationModel? AttributeLocation ); + /// + /// Equatable stand-in for the [AssertionFrom] attribute's , which references its + /// and would keep old compilations alive in the incremental cache. + /// + private sealed record AttributeLocationModel(string FilePath, TextSpan Span, LinePositionSpan LineSpan) + { + public static AttributeLocationModel? From(Location? location) => + location is { IsInSource: true } && location.GetLineSpan() is { Path: { Length: > 0 } path } lineSpan + ? new AttributeLocationModel(path, location.SourceSpan, lineSpan.Span) + : null; + + public static Location ToLocation(AttributeLocationModel? model) => + model is null ? Location.None : Location.Create(model.FilePath, model.Span, model.LineSpan); + } + private sealed record ConditionClassModel( string ClassName, string Source @@ -1484,6 +1508,7 @@ private record CreateAssertionAttributeData( bool RequiresGenericTypeParameter, bool TreatAsInstance, string? ExpectationMessage, - bool IsUnboundGeneric + bool IsUnboundGeneric, + Location? AttributeLocation ); } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/AssertionMethodGeneratorRunner.cs b/tests/TUnit.Assertions.SourceGenerator.Tests/AssertionMethodGeneratorRunner.cs index a7b9cefae29..655ef4930ce 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/AssertionMethodGeneratorRunner.cs +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/AssertionMethodGeneratorRunner.cs @@ -15,6 +15,9 @@ internal static class AssertionMethodGeneratorRunner private static readonly Regex ExpectationRegex = new( @"protected override string GetExpectation\(\)\s*\{\s*return (?[^\r\n]*);"); + /// The path the input source is parsed with, so diagnostics can point into it. + public const string InputPath = "Input.cs"; + /// Runs the generator over and returns its generated sources and diagnostics. public static (string[] Sources, ImmutableArray Diagnostics) Run(string source) { @@ -28,7 +31,7 @@ public static (string[] Sources, ImmutableArray Diagnostics) Run(str var compilation = CSharpCompilation.Create( nameof(AssertionMethodGeneratorRunner), [ - CSharpSyntaxTree.ParseText(source, parseOptions), + CSharpSyntaxTree.ParseText(source, parseOptions, path: InputPath), CSharpSyntaxTree.ParseText( """ global using global::System; diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs b/tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs index 339277746b6..826961d9bf8 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs @@ -31,6 +31,15 @@ public async Task Expectations_Do_Not_Repeat_To_Or_Not(string file) await Assert.That(expectations.Where(expectation => RepeatedWordRegex.IsMatch(expectation))).IsEmpty(); } + [Test] + [MethodDataSource(nameof(AssertionFromFiles))] + public async Task Generated_Source_Has_No_Unresolved_Class_Names(string file) + { + var (sources, _) = AssertionMethodGeneratorRunner.Run(File.ReadAllText(Path.Combine(Git.SourceDirectory.FullName, file))); + + await Assert.That(sources.Where(source => source.Contains('\u0001'))).IsEmpty(); + } + [Test] public async Task Built_In_Declarations_Render_Expectations() { diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs b/tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs index 0c0e888636c..28dc593c069 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs @@ -58,7 +58,16 @@ public async Task Renders_Expectation(string form, string expected) } [Test] - public async Task Overloads_Sharing_A_Condition_Class_Are_Reported() + public async Task Generated_Source_Has_No_Unresolved_Class_Names() + { + var (sources, _) = AssertionMethodGeneratorRunner.Run(MessagesSource); + + await Assert.That(sources).IsNotEmpty(); + await Assert.That(sources.Where(source => source.Contains('\u0001'))).IsEmpty(); + } + + [Test] + public async Task Overloads_Sharing_A_Condition_Class_Are_Reported_At_The_Attribute() { var (_, diagnostics) = AssertionMethodGeneratorRunner.Run( """ @@ -76,7 +85,12 @@ public static class Overloads public static partial class OverloadAssertions; """); - await Assert.That(diagnostics.Select(x => x.Id)).Contains("TU0002"); + var diagnostic = diagnostics.Single(x => x.Id == "TU0002"); + var lineSpan = diagnostic.Location.GetLineSpan(); + + await Assert.That(lineSpan.Path).IsEqualTo(AssertionMethodGeneratorRunner.InputPath); + // Line 10 (0-based): the [AssertionFrom] line + await Assert.That(lineSpan.StartLinePosition.Line).IsEqualTo(10); } [Test] From 715f690de73a10093f69ca55d2bf09e903eba3bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Sun, 18 Oct 2026 23:27:34 +0000 Subject: [PATCH 39/49] fix(aspire): format the out-of-memory exit code with the invariant culture DescribeState formatted the OutOfMemory exit code with the current culture, unlike every other exit code, so on sv-SE a negative Windows out-of-memory status printed with U+2212 MINUS SIGN. Use FormatExitCodeNumber there too, and cover it with an sv-SE test. --- src/TUnit.Aspire.Core/AspireFixture.cs | 4 +++- .../AspireDiagnosticsTests.cs | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/TUnit.Aspire.Core/AspireFixture.cs b/src/TUnit.Aspire.Core/AspireFixture.cs index 2df3d2713b6..05a706af851 100644 --- a/src/TUnit.Aspire.Core/AspireFixture.cs +++ b/src/TUnit.Aspire.Core/AspireFixture.cs @@ -1719,7 +1719,9 @@ internal static string FormatExitCode(int? exitCode) /// One-line human description of a resource's failure state for the concise exception message. internal static string DescribeState(in ResourceDiagnosis d) => d.Class switch { - ResourceFailureClass.OutOfMemory => $"{d.State}, exit code {d.ExitCode} (OOM / SIGKILL)", + ResourceFailureClass.OutOfMemory => d.ExitCode is int oomCode + ? $"{d.State}, exit code {FormatExitCodeNumber(oomCode)} (OOM / SIGKILL)" + : $"{d.State} (OOM / SIGKILL)", ResourceFailureClass.NonZeroExit => $"{d.State}, exit code {FormatExitCode(d.ExitCode)}", ResourceFailureClass.CrashedNoCode => $"{d.State} (no exit code)", ResourceFailureClass.HealthCheckFailing => d.Detail is { Length: > 0 } health diff --git a/tests/TUnit.Aspire.Tests/AspireDiagnosticsTests.cs b/tests/TUnit.Aspire.Tests/AspireDiagnosticsTests.cs index 612064f2dd7..70724861b56 100644 --- a/tests/TUnit.Aspire.Tests/AspireDiagnosticsTests.cs +++ b/tests/TUnit.Aspire.Tests/AspireDiagnosticsTests.cs @@ -115,6 +115,29 @@ public async Task DescribeState_OutOfMemory_MentionsOom() await Assert.That(DescribeState(diagnosis)).Contains("OOM / SIGKILL"); } + [Test] + public async Task DescribeState_OutOfMemory_FormatsNegativeExitCodeInvariantly() + { + // Windows reports out-of-memory as a negative NTSTATUS; sv-SE would print it with U+2212 MINUS SIGN. + var swedish = new System.Globalization.CultureInfo("sv-SE"); + await Assert.That(swedish.NumberFormat.NegativeSign).IsEqualTo("\u2212"); + + var diagnosis = new ResourceDiagnosis("cache", "Exited", -1073741801, ResourceFailureClass.OutOfMemory, null, null); + var original = System.Globalization.CultureInfo.CurrentCulture; + string description; + try + { + System.Globalization.CultureInfo.CurrentCulture = swedish; + description = DescribeState(diagnosis); + } + finally + { + System.Globalization.CultureInfo.CurrentCulture = original; + } + + await Assert.That(description).Contains("exit code -1073741801"); + } + [Test] public async Task DescribeState_HealthCheckFailing_IncludesDetail() { From bea54c9c1aceacf3d45abc6d12dd6a4a5f98c89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Mon, 19 Oct 2026 04:57:22 +0000 Subject: [PATCH 40/49] perf(assertions): compute the [AssertionFrom] attribute location only when reporting a diagnostic Also pin that a leading "not" without "to" on a NegateLogic message is not negated again. --- .../Generators/AssertionMethodGenerator.cs | 24 ++++++++++++++----- .../ExpectationMessageRenderingTests.cs | 3 +++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs index b70e02ec932..88cb340f9d3 100644 --- a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs +++ b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs @@ -139,7 +139,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) treatAsInstance, expectationMessage, isUnboundGeneric, - attributeData.ApplicationSyntaxReference?.GetSyntax().GetLocation() + attributeData.ApplicationSyntaxReference ); attributeDataList.Add(createAssertionAttributeData); @@ -248,7 +248,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) treatAsInstance, expectationMessage, isUnboundGeneric, - attributeData.ApplicationSyntaxReference?.GetSyntax().GetLocation() + attributeData.ApplicationSyntaxReference ); attributeDataList.Add(createAssertionAttributeData); @@ -524,7 +524,7 @@ private static AssertionEntryModel RenderEntry(CreateAssertionAttributeData attr // Only keep the location when there is something to report: it changes with every edit above the attribute, // and would otherwise stop unrelated edits from being cached var attributeLocation = missingMethodMessage is not null || collidingClassNames.Count > 0 - ? AttributeLocationModel.From(attributeData.AttributeLocation) + ? AttributeLocationModel.From(attributeData.AttributeSyntax) : null; return new AssertionEntryModel(conditionClasses, missingMethodMessage, extensionMethodsSource, collidingClassNames, attributeLocation); @@ -1482,10 +1482,22 @@ private sealed record AssertionEntryModel( /// private sealed record AttributeLocationModel(string FilePath, TextSpan Span, LinePositionSpan LineSpan) { - public static AttributeLocationModel? From(Location? location) => - location is { IsInSource: true } && location.GetLineSpan() is { Path: { Length: > 0 } path } lineSpan + /// + /// Takes the rather than a so the location is only + /// computed for the attributes that report a diagnostic, not for every attribute on every pass. + /// + public static AttributeLocationModel? From(SyntaxReference? syntax) + { + if (syntax is null) + { + return null; + } + + var location = Location.Create(syntax.SyntaxTree, syntax.Span); + return location.GetLineSpan() is { Path: { Length: > 0 } path } lineSpan ? new AttributeLocationModel(path, location.SourceSpan, lineSpan.Span) : null; + } public static Location ToLocation(AttributeLocationModel? model) => model is null ? Location.None : Location.Create(model.FilePath, model.Span, model.LineSpan); @@ -1509,6 +1521,6 @@ private record CreateAssertionAttributeData( bool TreatAsInstance, string? ExpectationMessage, bool IsUnboundGeneric, - Location? AttributeLocation + SyntaxReference? AttributeSyntax ); } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs b/tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs index 28dc593c069..ccac33505d4 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs @@ -19,6 +19,7 @@ public static class Methods public static bool Canonical(string value) => true; public static bool SpelledOutTo(string value) => true; public static bool SpelledOutNot(string value, string text) => true; + public static bool LeadingNot(string value, string text) => true; public static bool EscapedBraces(string value, string text) => true; public static bool UnknownPlaceholder(string value, string ending) => true; public static bool NonAscii(string value, string önskad) => true; @@ -30,6 +31,7 @@ public static class Methods [AssertionFrom(typeof(Methods), nameof(Methods.Canonical), ExpectationMessage = "be blank")] [AssertionFrom(typeof(Methods), nameof(Methods.SpelledOutTo), ExpectationMessage = "to be spelled out")] [AssertionFrom(typeof(Methods), nameof(Methods.SpelledOutNot), NegateLogic = true, ExpectationMessage = "to not contain {text}")] + [AssertionFrom(typeof(Methods), nameof(Methods.LeadingNot), NegateLogic = true, ExpectationMessage = "not start with {text}")] [AssertionFrom(typeof(Methods), nameof(Methods.EscapedBraces), ExpectationMessage = "contain {{text}} and {text}")] [AssertionFrom(typeof(Methods), nameof(Methods.UnknownPlaceholder), ExpectationMessage = "end with {suffix}")] [AssertionFrom(typeof(Methods), nameof(Methods.NonAscii), ExpectationMessage = "contain {önskad}")] @@ -46,6 +48,7 @@ public static partial class MessageAssertions; [Arguments("Canonical form", @"$""to {(_negated ? ""not "" : """")}be blank""")] [Arguments("Leading 'to' is not repeated", @"$""to {(_negated ? ""not "" : """")}be spelled out""")] [Arguments("Spelled-out 'not' is not negated again", @"$""to not contain {_text}""")] + [Arguments("Leading 'not' without 'to' is not negated again", @"$""to not start with {_text}""")] [Arguments("Escaped braces stay literal", @"$""to {(_negated ? ""not "" : """")}contain {{text}} and {_text}""")] [Arguments("Unknown placeholder stays literal", @"$""to {(_negated ? ""not "" : """")}end with {{suffix}}""")] [Arguments("Non-ASCII placeholder", @"$""to {(_negated ? ""not "" : """")}contain {_önskad}""")] From 66ba36ba0edfb2b390ab0a171061edbd39a42e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Mon, 19 Oct 2026 05:12:24 +0000 Subject: [PATCH 41/49] fix(assertions): give each [AssertionFrom] overload that shares a class name its own class Overloads of one method that map to the same condition class name now get numbered classes, as collisions across extension classes already did, instead of the first overload's class being used for all of them. This replaces the TU0002 warning, which no longer has anything to report. --- .../Generators/AssertionMethodGenerator.cs | 72 +++++++------------ .../ExpectationMessageRenderingTests.cs | 24 +++---- 2 files changed, 34 insertions(+), 62 deletions(-) diff --git a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs index 88cb340f9d3..5a12016be3d 100644 --- a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs +++ b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs @@ -340,33 +340,30 @@ private static void GenerateAssertionsForClass( } } - private static readonly DiagnosticDescriptor OverloadsShareConditionClass = new( - "TU0002", - "Overloads share a generated assertion class", - "More than one overload maps to the generated assertion class '{0}'; only the first one is generated, and the extension methods for the others use it", - "TUnit.Assertions", - DiagnosticSeverity.Warning, - true); - private const char ClassNameMarker = '\u0001'; + private const char OverloadSeparator = '\u0002'; /// - /// How pre-rendered source refers to a condition class: its name between markers that cannot occur in C# source, - /// so the emit step, which alone knows the final name, can substitute it with a plain replace. A reference that is - /// never resolved leaves a marker behind, which fails the build loudly rather than binding to a wrong class. + /// How pre-rendered source refers to the condition class for : its name and the overload + /// it was generated for, between markers that cannot occur in C# source, so the emit step, which alone knows the + /// final name, can substitute it with a plain replace. Including the overload keeps overloads that map to the + /// same class name apart, so each binds to its own class. A reference that is never resolved leaves a marker + /// behind, which fails the build loudly rather than binding to a wrong class. /// - private static string ClassNameReference(string className) => $"{ClassNameMarker}{className}{ClassNameMarker}"; + private static string ClassNameReference(string className, IMethodSymbol method) => + $"{ClassNameMarker}{className}{OverloadSeparator}{method.OriginalDefinition.ToDisplayString()}{ClassNameMarker}"; - /// Substitutes for every reference to the class. - private static string ResolveClassName(string source, string className, string name) => - source.Replace(ClassNameReference(className), name); + /// Substitutes for every occurrence of . + private static string ResolveClassName(string source, string reference, string name) => + source.Replace(reference, name); /// /// The condition classes emitted so far, across every extension class. A condition class is named after its /// target method, so attributes on the same method share it, which is fine while their sources match (e.g. /// IsNullOrEmpty and its NegateLogic counterpart, which passes the negation to the constructor). - /// A class with the same name but a different source, such as a different ExpectationMessage, gets a - /// numbered name instead of being dropped, so its attribute keeps its own message. + /// A class with the same name but a different source, such as a different ExpectationMessage or another + /// overload of the same method that maps to the same name, gets a numbered name instead of being dropped, so its + /// attribute keeps its own message and each overload its own class. /// private sealed class GeneratedConditionClasses { @@ -462,28 +459,15 @@ private static void GenerateAssertionsForSpecificClass( AttributeLocationModel.ToLocation(entry.AttributeLocation))); } - foreach (var collidingClassName in entry.CollidingClassNames) - { - context.ReportDiagnostic(Diagnostic.Create(OverloadsShareConditionClass, AttributeLocationModel.ToLocation(entry.AttributeLocation), collidingClassName)); - } - var extensionMethodsSource = entry.ExtensionMethodsSource; - var namesInEntry = new HashSet(StringComparer.Ordinal); foreach (var conditionClass in entry.ConditionClasses) { - // Overloads that map to the same class name can't be told apart in the extension methods, so the - // first one wins, as before (reported above when the others would have been different) - if (!namesInEntry.Add(conditionClass.ClassName)) - { - continue; - } - var (name, isNew) = allGeneratedClasses.Claim(conditionClass); - extensionMethodsSource = ResolveClassName(extensionMethodsSource, conditionClass.ClassName, name); + extensionMethodsSource = ResolveClassName(extensionMethodsSource, conditionClass.Reference, name); if (isNew) { - sourceBuilder.Append(ResolveClassName(conditionClass.Source, conditionClass.ClassName, name)); + sourceBuilder.Append(ResolveClassName(conditionClass.Source, conditionClass.Reference, name)); } } @@ -514,20 +498,13 @@ private static AssertionEntryModel RenderEntry(CreateAssertionAttributeData attr var conditionClasses = RenderConditionClasses(attributeData, out var missingMethodMessage); var extensionMethodsSource = RenderExtensionMethods(attributeData); - // Overloads that map to the same class name but would generate different classes; only the first is emitted - var collidingClassNames = conditionClasses - .GroupBy(x => x.ClassName, StringComparer.Ordinal) - .Where(group => group.Select(x => x.Source).Distinct(StringComparer.Ordinal).Skip(1).Any()) - .Select(group => group.Key) - .ToImmutableEquatableArray(); - // Only keep the location when there is something to report: it changes with every edit above the attribute, // and would otherwise stop unrelated edits from being cached - var attributeLocation = missingMethodMessage is not null || collidingClassNames.Count > 0 + var attributeLocation = missingMethodMessage is not null ? AttributeLocationModel.From(attributeData.AttributeSyntax) : null; - return new AssertionEntryModel(conditionClasses, missingMethodMessage, extensionMethodsSource, collidingClassNames, attributeLocation); + return new AssertionEntryModel(conditionClasses, missingMethodMessage, extensionMethodsSource, attributeLocation); } private static ImmutableEquatableArray RenderConditionClasses(CreateAssertionAttributeData attributeData, out string? missingMethodMessage) @@ -618,11 +595,11 @@ private static ImmutableEquatableArray RenderConditionClass { var className = GenerateAssertConditionClassNameForMethod(attributeData.TargetType, attributeData.ContainingType, attributeData.MethodName, method); - // Duplicate class names are dropped at emit time, where the set of classes + // Duplicate class names are resolved at emit time, where the set of classes // already written across every extension class is known. var classBuilder = new StringBuilder(); GenerateAssertConditionClassForMethod(classBuilder, attributeData, method); - conditionClasses.Add(new ConditionClassModel(className, classBuilder.ToString())); + conditionClasses.Add(new ConditionClassModel(className, ClassNameReference(className, method), classBuilder.ToString())); } return conditionClasses.ToImmutableEquatableArray(); @@ -690,7 +667,7 @@ private static void GenerateAssertConditionClassForMethod(StringBuilder sourceBu var containingType = attributeData.ContainingType; var methodName = attributeData.MethodName; - var className = ClassNameReference(GenerateAssertConditionClassNameForMethod(attributeData.TargetType, attributeData.ContainingType, methodName, staticMethod)); + var className = ClassNameReference(GenerateAssertConditionClassNameForMethod(attributeData.TargetType, attributeData.ContainingType, methodName, staticMethod), staticMethod); // Determine which parameters to skip IParameterSymbol[] parameters; @@ -1287,7 +1264,7 @@ private static void GenerateMethodsForSpecificOverload(StringBuilder sourceBuild var methodName = attributeData.MethodName; var baseMethodName = attributeData.CustomName ?? methodName; - var className = ClassNameReference(GenerateAssertConditionClassNameForMethod(attributeData.TargetType, attributeData.ContainingType, methodName, staticMethod)); + var className = ClassNameReference(GenerateAssertConditionClassNameForMethod(attributeData.TargetType, attributeData.ContainingType, methodName, staticMethod), staticMethod); var actualMethodName = baseMethodName; if (staticMethod.Parameters.Length > 1) @@ -1472,7 +1449,6 @@ private sealed record AssertionEntryModel( ImmutableEquatableArray ConditionClasses, string? MissingMethodMessage, string ExtensionMethodsSource, - ImmutableEquatableArray CollidingClassNames, AttributeLocationModel? AttributeLocation ); @@ -1503,8 +1479,12 @@ public static Location ToLocation(AttributeLocationModel? model) => model is null ? Location.None : Location.Create(model.FilePath, model.Span, model.LineSpan); } + /// The name the class is generated under, unless it is taken by a different class. + /// How and the extension methods refer to the class. + /// The class's source, referring to itself through . private sealed record ConditionClassModel( string ClassName, + string Reference, string Source ); diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs b/tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs index ccac33505d4..1c9cd84a645 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs @@ -4,7 +4,7 @@ namespace TUnit.Assertions.SourceGenerator.Tests; /// /// Pins the GetExpectation() code renders for each documented form of -/// [AssertionFrom] ExpectationMessage, and the diagnostic for overloads that share a condition class. +/// [AssertionFrom] ExpectationMessage, and the classes for overloads that share a condition class name. /// internal class ExpectationMessageRenderingTests { @@ -70,9 +70,9 @@ public async Task Generated_Source_Has_No_Unresolved_Class_Names() } [Test] - public async Task Overloads_Sharing_A_Condition_Class_Are_Reported_At_The_Attribute() + public async Task Overloads_Sharing_A_Class_Name_Each_Bind_To_Their_Own_Class() { - var (_, diagnostics) = AssertionMethodGeneratorRunner.Run( + var (sources, diagnostics) = AssertionMethodGeneratorRunner.Run( """ using TUnit.Assertions.Attributes; @@ -88,19 +88,11 @@ public static class Overloads public static partial class OverloadAssertions; """); - var diagnostic = diagnostics.Single(x => x.Id == "TU0002"); - var lineSpan = diagnostic.Location.GetLineSpan(); + var source = string.Join(Environment.NewLine, sources); - await Assert.That(lineSpan.Path).IsEqualTo(AssertionMethodGeneratorRunner.InputPath); - // Line 10 (0-based): the [AssertionFrom] line - await Assert.That(lineSpan.StartLinePosition.Line).IsEqualTo(10); - } - - [Test] - public async Task Distinct_Overloads_Are_Not_Reported() - { - var (_, diagnostics) = AssertionMethodGeneratorRunner.Run(MessagesSource); - - await Assert.That(diagnostics.Select(x => x.Id)).DoesNotContain("TU0002"); + await Assert.That(diagnostics).IsEmpty(); + await Assert.That(source).Contains("new StringOverloadsMatchesWithStringAnd1MoreAssertion(source.Context, count, false)"); + await Assert.That(source).Contains("new StringOverloadsMatchesWithStringAnd1MoreAssertion2(source.Context, flag, false)"); + await Assert.That(source).Contains("Rendering.Overloads.Matches(actualValue, _flag)"); } } From 628c858980401ac11a481f5e3cb69ad4c7020a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Mon, 19 Oct 2026 06:07:42 +0000 Subject: [PATCH 42/49] revert: move the production fixes to their own pull request Keeps this pull request to test-environment and CI changes. The analyzer, Aspire and [AssertionFrom] generator fixes, with the tests and snapshots that depend on them, follow in a separate pull request from the fix/assertionfrom-expectation-messages branch. --- .../source-generator-assertions.md | 20 +- ...ublicMethodMissingTestAttributeAnalyzer.cs | 24 -- src/TUnit.Aspire.Core/AspireFixture.cs | 20 +- .../Generators/AssertionMethodGenerator.cs | 249 +++--------------- .../Attributes/AssertionFromAttribute.cs | 11 - .../Attributes/AssertionFromAttribute`1.cs | 10 +- .../Conditions/SpanAssertionExtensions.cs | 38 ++- ...MethodMissingTestAttributeAnalyzerTests.cs | 75 ------ .../AspireDiagnosticsTests.cs | 23 -- ...AssemblyAssertions.DotNet10_0.verified.txt | 4 +- .../AssertionMethodGeneratorRunner.cs | 71 ----- .../BuiltInExpectationMessageTests.cs | 60 ----- ...ionTokenAssertions.DotNet10_0.verified.txt | 4 +- ...atesCharAssertions.DotNet10_0.verified.txt | 28 +- ...tureInfoAssertions.DotNet10_0.verified.txt | 4 +- ...DateTimeAssertions.DotNet10_0.verified.txt | 2 +- ...toryInfoAssertions.DotNet10_0.verified.txt | 2 +- ...EncodingAssertions.DotNet10_0.verified.txt | 2 +- .../ExpectationMessageRenderingTests.cs | 98 ------- ...FileInfoAssertions.DotNet10_0.verified.txt | 4 +- ...sProcessAssertions.DotNet10_0.verified.txt | 6 +- ...esStreamAssertions.DotNet10_0.verified.txt | 8 +- ...atesTaskAssertions.DotNet10_0.verified.txt | 6 +- ...esThreadAssertions.DotNet10_0.verified.txt | 6 +- ...ZoneInfoAssertions.DotNet10_0.verified.txt | 2 +- ...atesTypeAssertions.DotNet10_0.verified.txt | 46 ++-- ...ratesUriAssertions.DotNet10_0.verified.txt | 12 +- ...eferenceAssertions.DotNet10_0.verified.txt | 4 +- .../AssertionFromExpectationMessageTests.cs | 182 ------------- .../AssertionGroupTests.cs | 27 +- .../StringNullabilityAssertionTests.cs | 16 -- ...Has_No_API_Changes.DotNet10_0.verified.txt | 8 +- ..._Has_No_API_Changes.DotNet8_0.verified.txt | 8 +- ..._Has_No_API_Changes.DotNet9_0.verified.txt | 8 +- 34 files changed, 162 insertions(+), 926 deletions(-) delete mode 100644 tests/TUnit.Assertions.SourceGenerator.Tests/AssertionMethodGeneratorRunner.cs delete mode 100644 tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs delete mode 100644 tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs delete mode 100644 tests/TUnit.Assertions.Tests/AssertionFromExpectationMessageTests.cs diff --git a/docs/docs/assertions/extensibility/source-generator-assertions.md b/docs/docs/assertions/extensibility/source-generator-assertions.md index 9ab29c22fc0..6cb1658dc8b 100644 --- a/docs/docs/assertions/extensibility/source-generator-assertions.md +++ b/docs/docs/assertions/extensibility/source-generator-assertions.md @@ -238,9 +238,9 @@ Use `[AssertionFrom]` to create assertions from existing methods in libraries or ```csharp using TUnit.Assertions.Attributes; -[AssertionFrom(nameof(string.IsNullOrEmpty), CustomName = "IsBlank", ExpectationMessage = "be null or empty")] -[AssertionFrom(nameof(string.StartsWith), CustomName = "BeginsWithText", ExpectationMessage = "start with '{value}'")] -[AssertionFrom(nameof(string.EndsWith), CustomName = "EndsWithText", ExpectationMessage = "end with '{value}'")] +[AssertionFrom(nameof(string.IsNullOrEmpty), CustomName = "IsBlank", ExpectationMessage = "to be null or empty")] +[AssertionFrom(nameof(string.StartsWith), CustomName = "BeginsWithText", ExpectationMessage = "to start with {value}")] +[AssertionFrom(nameof(string.EndsWith), CustomName = "EndsWithText", ExpectationMessage = "to end with {value}")] public static partial class StringAssertionExtensions { } @@ -256,7 +256,7 @@ await Assert.That("hello").BeginsWithText("he"); ### With Custom Names ```csharp -[AssertionFrom(nameof(string.Contains), CustomName = "Has", ExpectationMessage = "have '{value}'")] +[AssertionFrom(nameof(string.Contains), CustomName = "Has", ExpectationMessage = "to have '{value}'")] public static partial class StringAssertionExtensions { } @@ -271,7 +271,7 @@ await Assert.That("hello").Has("world"); // ❌ Fails: "Expected to have For `bool`-returning methods, you can generate negated versions: ```csharp -[AssertionFrom(nameof(string.Contains), CustomName = "LacksText", NegateLogic = true, ExpectationMessage = "contain '{value}'")] +[AssertionFrom(nameof(string.Contains), CustomName = "LacksText", NegateLogic = true, ExpectationMessage = "to not contain '{value}'")] public static partial class StringAssertionExtensions { } @@ -283,19 +283,11 @@ await Assert.That("hello").LacksText("ell"); // ❌ Fails: "Expected to not con **Note:** Negation only works with `bool`-returning methods. `AssertionResult` methods determine their own pass/fail logic. -### How `ExpectationMessage` Is Rendered - -For `[AssertionFrom]`, the message is the phrase that follows "Expected to". Write it in the positive form, without the leading "to", as in the examples above: - -- The generator adds "to", plus "not" when `NegateLogic = true`: `"contain '{value}'"` renders as "to contain 'x'", or "to not contain 'x'" when negated. -- For compatibility, a leading "to" is ignored (`"to be null or empty"` renders the same as `"be null or empty"`), and a phrase that already starts with "not" (`"to not contain '{value}'"`) is used as written rather than negated again. -- `{paramName}` is replaced with that parameter's value. Everything else is literal text: write `{{` and `}}` for literal braces, and a placeholder that names no parameter is printed as is. - ### Referencing Methods on Different Types ```csharp // Reference static methods from another type -[AssertionFrom(typeof(StringHelper), nameof(StringHelper.IsValidEmail), ExpectationMessage = "be a valid email")] +[AssertionFrom(typeof(StringHelper), nameof(StringHelper.IsValidEmail), ExpectationMessage = "to be a valid email")] public static partial class StringAssertionExtensions { } diff --git a/src/TUnit.Analyzers/PublicMethodMissingTestAttributeAnalyzer.cs b/src/TUnit.Analyzers/PublicMethodMissingTestAttributeAnalyzer.cs index 74a4fd2405b..fed67935f3d 100644 --- a/src/TUnit.Analyzers/PublicMethodMissingTestAttributeAnalyzer.cs +++ b/src/TUnit.Analyzers/PublicMethodMissingTestAttributeAnalyzer.cs @@ -54,7 +54,6 @@ private void AnalyzeSymbol(SymbolAnalysisContext context) .Where(x => !IsDisposableDispose(x)) .Where(x => !IsAsyncDisposableDispose(x)) .Where(x => !IsInitializeAsync(x)) - .Where(x => !ImplementsInterfaceMember(x)) .Where(x => !IsTestDataSource(x, testMethods))) { context.ReportDiagnostic(Diagnostic.Create(Rules.PublicMethodMissingTestAttribute, method.Locations.FirstOrDefault())); @@ -75,29 +74,6 @@ private bool IsTestDataSource(IMethodSymbol methodSymbol, IMethodSymbol[] testMe return false; } - /// - /// Whether implicitly implements an interface member of its containing type (e.g. - /// ITestStartEventReceiver.OnTestStart). Such a method cannot be made private, so the diagnostic's advice - /// would not apply. - /// - private static bool ImplementsInterfaceMember(IMethodSymbol method) - { - var containingType = method.ContainingType; - - foreach (var @interface in containingType.AllInterfaces) - { - foreach (var member in @interface.GetMembers(method.Name)) - { - if (SymbolEqualityComparer.Default.Equals(containingType.FindImplementationForInterfaceMember(member), method)) - { - return true; - } - } - } - - return false; - } - private bool IsDisposableDispose(IMethodSymbol method) { return method is { ReturnsVoid: true, Name: "Dispose" } && diff --git a/src/TUnit.Aspire.Core/AspireFixture.cs b/src/TUnit.Aspire.Core/AspireFixture.cs index 05a706af851..c5369c35564 100644 --- a/src/TUnit.Aspire.Core/AspireFixture.cs +++ b/src/TUnit.Aspire.Core/AspireFixture.cs @@ -1,7 +1,6 @@ using System.Collections.Concurrent; using System.Collections.Immutable; using System.Diagnostics; -using System.Globalization; using System.Text; using Aspire.Hosting; using Aspire.Hosting.ApplicationModel; @@ -1706,22 +1705,13 @@ internal static string FormatExitCode(int? exitCode) return "(unknown)"; } - var number = FormatExitCodeNumber(code); - return DecodeExitCode(code) is { } meaning ? $"{number} ({meaning})" : number; + return DecodeExitCode(code) is { } meaning ? $"{code} ({meaning})" : code.ToString(); } - /// - /// Formats an exit code with the invariant culture: exit codes are identifiers to search for, and cultures such - /// as sv-SE would otherwise print negative Windows codes with U+2212 MINUS SIGN. - /// - private static string FormatExitCodeNumber(int code) => code.ToString(CultureInfo.InvariantCulture); - /// One-line human description of a resource's failure state for the concise exception message. internal static string DescribeState(in ResourceDiagnosis d) => d.Class switch { - ResourceFailureClass.OutOfMemory => d.ExitCode is int oomCode - ? $"{d.State}, exit code {FormatExitCodeNumber(oomCode)} (OOM / SIGKILL)" - : $"{d.State} (OOM / SIGKILL)", + ResourceFailureClass.OutOfMemory => $"{d.State}, exit code {d.ExitCode} (OOM / SIGKILL)", ResourceFailureClass.NonZeroExit => $"{d.State}, exit code {FormatExitCode(d.ExitCode)}", ResourceFailureClass.CrashedNoCode => $"{d.State} (no exit code)", ResourceFailureClass.HealthCheckFailing => d.Detail is { Length: > 0 } health @@ -1817,7 +1807,7 @@ private static string SummarizeHealthReport(HealthReportSnapshot report) } return dep.Snapshot.ExitCode is int code - ? $"'{depName}' ({depState}, exit code {FormatExitCodeNumber(code)})" + ? $"'{depName}' ({depState}, exit code {code})" : $"'{depName}' ({depState})"; } @@ -1997,7 +1987,7 @@ private void AppendResourceDiagnosticBlock( sb.AppendLine($" State: {diagnosis.State}"); if (diagnosis.ExitCode is int code) { - sb.Append($" Exit code: {FormatExitCodeNumber(code)}"); + sb.Append($" Exit code: {code}"); if (DecodeExitCode(code) is { } meaning) { sb.Append($" ({meaning})"); @@ -2088,7 +2078,7 @@ private static void AppendDependencyChain(StringBuilder sb, ResourceNotification if (notificationService.TryGetCurrentState(depName, out var dep) && dep is not null) { var depState = dep.Snapshot.State?.Text ?? "unknown"; - description = dep.Snapshot.ExitCode is int code ? $"{depState}, exit code {FormatExitCodeNumber(code)}" : depState; + description = dep.Snapshot.ExitCode is int code ? $"{depState}, exit code {code}" : depState; } else { diff --git a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs index 5a12016be3d..3c0ce160e03 100644 --- a/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs +++ b/src/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs @@ -4,11 +4,8 @@ using System.Collections.Immutable; using System.Linq; using System.Text; -using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; using TUnit.Assertions.SourceGenerator.Models; namespace TUnit.Assertions.SourceGenerator.Generators; @@ -138,8 +135,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) requiresGenericTypeParameter, treatAsInstance, expectationMessage, - isUnboundGeneric, - attributeData.ApplicationSyntaxReference + isUnboundGeneric ); attributeDataList.Add(createAssertionAttributeData); @@ -247,8 +243,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) requiresGenericTypeParameter, treatAsInstance, expectationMessage, - isUnboundGeneric, - attributeData.ApplicationSyntaxReference + isUnboundGeneric ); attributeDataList.Add(createAssertionAttributeData); @@ -326,7 +321,7 @@ private static void GenerateAssertionsForClass( } // Track all generated classes globally to avoid duplicates across different extension classes - var allGeneratedClasses = new GeneratedConditionClasses(); + var allGeneratedClasses = new HashSet(); // Group by class and generate one file per class. Non-generic attribute data comes first, // then generic, so a class using both keeps its existing member order. @@ -340,58 +335,6 @@ private static void GenerateAssertionsForClass( } } - private const char ClassNameMarker = '\u0001'; - private const char OverloadSeparator = '\u0002'; - - /// - /// How pre-rendered source refers to the condition class for : its name and the overload - /// it was generated for, between markers that cannot occur in C# source, so the emit step, which alone knows the - /// final name, can substitute it with a plain replace. Including the overload keeps overloads that map to the - /// same class name apart, so each binds to its own class. A reference that is never resolved leaves a marker - /// behind, which fails the build loudly rather than binding to a wrong class. - /// - private static string ClassNameReference(string className, IMethodSymbol method) => - $"{ClassNameMarker}{className}{OverloadSeparator}{method.OriginalDefinition.ToDisplayString()}{ClassNameMarker}"; - - /// Substitutes for every occurrence of . - private static string ResolveClassName(string source, string reference, string name) => - source.Replace(reference, name); - - /// - /// The condition classes emitted so far, across every extension class. A condition class is named after its - /// target method, so attributes on the same method share it, which is fine while their sources match (e.g. - /// IsNullOrEmpty and its NegateLogic counterpart, which passes the negation to the constructor). - /// A class with the same name but a different source, such as a different ExpectationMessage or another - /// overload of the same method that maps to the same name, gets a numbered name instead of being dropped, so its - /// attribute keeps its own message and each overload its own class. - /// - private sealed class GeneratedConditionClasses - { - private readonly HashSet _names = new(StringComparer.Ordinal); - private readonly Dictionary<(string ClassName, string Source), string> _emitted = new(); - - /// - /// Returns the name to use for , and whether it still has to be emitted. - /// - public (string Name, bool IsNew) Claim(ConditionClassModel conditionClass) - { - var key = (conditionClass.ClassName, conditionClass.Source); - if (_emitted.TryGetValue(key, out var existing)) - { - return (existing, false); - } - - var name = conditionClass.ClassName; - for (var suffix = 2; !_names.Add(name); suffix++) - { - name = conditionClass.ClassName + suffix.ToString(CultureInfo.InvariantCulture); - } - - _emitted.Add(key, name); - return (name, true); - } - } - private static AssertionClassModel BuildClassModel(INamedTypeSymbol classSymbol, List attributeDataList) { var entries = new List(attributeDataList.Count); @@ -412,7 +355,7 @@ private static void GenerateAssertionsForSpecificClass( string className, string? originalNamespace, AssertionEntryModel[] entries, - GeneratedConditionClasses allGeneratedClasses) + HashSet allGeneratedClasses) { var sourceBuilder = new StringBuilder(); // Always generate extension methods in TUnit.Assertions.Extensions namespace @@ -443,7 +386,6 @@ private static void GenerateAssertionsForSpecificClass( } // Generate all assert condition classes first - var extensionMethodSources = new List(entries.Length); foreach (var entry in entries) { if (entry.MissingMethodMessage is not null) @@ -456,31 +398,27 @@ private static void GenerateAssertionsForSpecificClass( "TUnit.Assertions", DiagnosticSeverity.Error, true), - AttributeLocationModel.ToLocation(entry.AttributeLocation))); + Location.None)); } - var extensionMethodsSource = entry.ExtensionMethodsSource; foreach (var conditionClass in entry.ConditionClasses) { - var (name, isNew) = allGeneratedClasses.Claim(conditionClass); - extensionMethodsSource = ResolveClassName(extensionMethodsSource, conditionClass.Reference, name); - - if (isNew) + if (!allGeneratedClasses.Add(conditionClass.ClassName)) { - sourceBuilder.Append(ResolveClassName(conditionClass.Source, conditionClass.Reference, name)); + continue; } - } - extensionMethodSources.Add(extensionMethodsSource); + sourceBuilder.Append(conditionClass.Source); + } } // Generate extension methods class sourceBuilder.AppendLine($"public static partial class {className}"); sourceBuilder.AppendLine("{"); - foreach (var extensionMethodsSource in extensionMethodSources) + foreach (var entry in entries) { - sourceBuilder.Append(extensionMethodsSource); + sourceBuilder.Append(entry.ExtensionMethodsSource); } sourceBuilder.AppendLine("}"); @@ -498,13 +436,7 @@ private static AssertionEntryModel RenderEntry(CreateAssertionAttributeData attr var conditionClasses = RenderConditionClasses(attributeData, out var missingMethodMessage); var extensionMethodsSource = RenderExtensionMethods(attributeData); - // Only keep the location when there is something to report: it changes with every edit above the attribute, - // and would otherwise stop unrelated edits from being cached - var attributeLocation = missingMethodMessage is not null - ? AttributeLocationModel.From(attributeData.AttributeSyntax) - : null; - - return new AssertionEntryModel(conditionClasses, missingMethodMessage, extensionMethodsSource, attributeLocation); + return new AssertionEntryModel(conditionClasses, missingMethodMessage, extensionMethodsSource); } private static ImmutableEquatableArray RenderConditionClasses(CreateAssertionAttributeData attributeData, out string? missingMethodMessage) @@ -595,11 +527,11 @@ private static ImmutableEquatableArray RenderConditionClass { var className = GenerateAssertConditionClassNameForMethod(attributeData.TargetType, attributeData.ContainingType, attributeData.MethodName, method); - // Duplicate class names are resolved at emit time, where the set of classes + // Duplicate class names are dropped at emit time, where the set of classes // already written across every extension class is known. var classBuilder = new StringBuilder(); GenerateAssertConditionClassForMethod(classBuilder, attributeData, method); - conditionClasses.Add(new ConditionClassModel(className, ClassNameReference(className, method), classBuilder.ToString())); + conditionClasses.Add(new ConditionClassModel(className, classBuilder.ToString())); } return conditionClasses.ToImmutableEquatableArray(); @@ -667,7 +599,7 @@ private static void GenerateAssertConditionClassForMethod(StringBuilder sourceBu var containingType = attributeData.ContainingType; var methodName = attributeData.MethodName; - var className = ClassNameReference(GenerateAssertConditionClassNameForMethod(attributeData.TargetType, attributeData.ContainingType, methodName, staticMethod), staticMethod); + var className = GenerateAssertConditionClassNameForMethod(attributeData.TargetType, attributeData.ContainingType, methodName, staticMethod); // Determine which parameters to skip IParameterSymbol[] parameters; @@ -1042,15 +974,27 @@ private static void GenerateAssertConditionClassForMethod(StringBuilder sourceBu if (!string.IsNullOrEmpty(attributeData.ExpectationMessage)) { // Use custom expectation message - sourceBuilder.AppendLine($" return $\"{RenderCustomExpectation(attributeData.ExpectationMessage!, parameters)}\";"); + var expectation = attributeData.ExpectationMessage; + if (parameters.Any()) + { + // Use interpolated string for parameter substitution + sourceBuilder.AppendLine($" return $\"{{(_negated ? \"not \" : \"\")}} {expectation}\";"); + } + else + { + // No parameters, just return the literal string with negation support + sourceBuilder.AppendLine($" return $\"{{(_negated ? \"not \" : \"\")}} {expectation}\";"); + } } else { // Use default expectation message - var arguments = parameters.Any() - ? $"({string.Join(", ", parameters.Select(p => $"{{_{p.Name}}}"))})" - : string.Empty; - sourceBuilder.AppendLine($" return $\"{RenderNegatableExpectation($"satisfy {methodName}{arguments}")}\";"); + sourceBuilder.Append($" return $\"{{(_negated ? \"not \" : \"\")}}to satisfy {methodName}"); + if (parameters.Any()) + { + sourceBuilder.Append($"({string.Join(", ", parameters.Select(p => $"{{_{p.Name}}}"))})"); + } + sourceBuilder.AppendLine("\";"); } sourceBuilder.AppendLine(" }"); @@ -1058,96 +1002,6 @@ private static void GenerateAssertConditionClassForMethod(StringBuilder sourceBu sourceBuilder.AppendLine(); } - /// - /// Renders the interpolated-string body of GetExpectation() for a custom ExpectationMessage. - /// - /// - /// The canonical form is the positive phrase without the leading "to" ("be null or empty"); the generator adds - /// "to" and, with NegateLogic, "not". The documented style that spells these out ("to be null or empty", - /// or "to not contain '{value}'" with NegateLogic) is also accepted, so neither renders as "to to ..." or - /// "to not not ...". - /// - private static string RenderCustomExpectation(string expectationMessage, IParameterSymbol[] parameters) - { - var expectation = expectationMessage.StartsWith("to ", StringComparison.Ordinal) - ? expectationMessage.Substring("to ".Length) - : expectationMessage; - - var phrase = RenderMessageText(expectation, parameters); - - return expectation.StartsWith("not ", StringComparison.Ordinal) - ? $"to {phrase}" - : RenderNegatableExpectation(phrase); - } - - /// - /// The one place the expectation's "to"/"to not" wording is decided, for custom and default messages alike. - /// - private static string RenderNegatableExpectation(string phrase) => $"to {{(_negated ? \"not \" : \"\")}}{phrase}"; - - /// - /// Renders message text as the body of an interpolated string literal in a single pass: a {name} - /// placeholder for a method parameter (any valid identifier, not only ASCII) becomes the generated - /// {_name} field, as does {_name} itself, which messages could reference before placeholders were - /// parsed. Everything else stays literal text, including escaped braces ({{name}} prints - /// {name}), placeholders that name no parameter, stray braces, quotes and backslashes. - /// - private static string RenderMessageText(string message, IParameterSymbol[] parameters) - { - var parameterNames = new HashSet(parameters.Select(p => p.Name), StringComparer.Ordinal); - var result = new StringBuilder(message.Length); - var literalStart = 0; - - foreach (Match match in MessageTokenRegex.Matches(message)) - { - result.Append(FormatLiteral(message.Substring(literalStart, match.Index - literalStart))); - - var name = match.Groups["name"]; - var parameterName = !name.Success ? null - : parameterNames.Contains(name.Value) ? name.Value - : name.Value.StartsWith("_", StringComparison.Ordinal) && parameterNames.Contains(name.Value.Substring(1)) ? name.Value.Substring(1) - : null; - - if (parameterName is not null) - { - result.Append("{_").Append(parameterName).Append('}'); - } - else if (name.Success) - { - // A placeholder that names no parameter: keep its text, braces included, literal - result.Append("{{").Append(FormatLiteral(name.Value)).Append("}}"); - } - else - { - // An escaped brace pair or a stray brace: keep the brace literal - result.Append(match.Value[0] is '{' ? "{{" : "}}"); - } - - literalStart = match.Index + match.Length; - } - - result.Append(FormatLiteral(message.Substring(literalStart))); - return result.ToString(); - } - - /// - /// Tokens of an ExpectationMessage: escaped brace pairs, {...} placeholders and stray braces. Any - /// brace-free text between single braces is a placeholder candidate; it is only substituted when it names a - /// parameter, so matching broadly keeps non-ASCII identifiers working without a hand-written identifier grammar. - /// - private static readonly Regex MessageTokenRegex = new(@"\{\{|\}\}|\{(?[^{}]*)\}|[{}]"); - - /// - /// Escapes for use inside a C# string literal. With quote: false, - /// leaves double quotes unescaped, so this quotes the text - /// and strips the outer pair instead. - /// - private static string FormatLiteral(string text) - { - var literal = SymbolDisplay.FormatLiteral(text, quote: true); - return literal.Substring(1, literal.Length - 2); - } - private static string GenerateAssertConditionClassNameForMethod(INamedTypeSymbol targetType, INamedTypeSymbol containingType, string methodName, IMethodSymbol method) { var targetTypeName = GetSimpleTypeName(targetType); @@ -1264,7 +1118,7 @@ private static void GenerateMethodsForSpecificOverload(StringBuilder sourceBuild var methodName = attributeData.MethodName; var baseMethodName = attributeData.CustomName ?? methodName; - var className = ClassNameReference(GenerateAssertConditionClassNameForMethod(attributeData.TargetType, attributeData.ContainingType, methodName, staticMethod), staticMethod); + var className = GenerateAssertConditionClassNameForMethod(attributeData.TargetType, attributeData.ContainingType, methodName, staticMethod); var actualMethodName = baseMethodName; if (staticMethod.Parameters.Length > 1) @@ -1448,43 +1302,11 @@ ImmutableEquatableArray Entries private sealed record AssertionEntryModel( ImmutableEquatableArray ConditionClasses, string? MissingMethodMessage, - string ExtensionMethodsSource, - AttributeLocationModel? AttributeLocation + string ExtensionMethodsSource ); - /// - /// Equatable stand-in for the [AssertionFrom] attribute's , which references its - /// and would keep old compilations alive in the incremental cache. - /// - private sealed record AttributeLocationModel(string FilePath, TextSpan Span, LinePositionSpan LineSpan) - { - /// - /// Takes the rather than a so the location is only - /// computed for the attributes that report a diagnostic, not for every attribute on every pass. - /// - public static AttributeLocationModel? From(SyntaxReference? syntax) - { - if (syntax is null) - { - return null; - } - - var location = Location.Create(syntax.SyntaxTree, syntax.Span); - return location.GetLineSpan() is { Path: { Length: > 0 } path } lineSpan - ? new AttributeLocationModel(path, location.SourceSpan, lineSpan.Span) - : null; - } - - public static Location ToLocation(AttributeLocationModel? model) => - model is null ? Location.None : Location.Create(model.FilePath, model.Span, model.LineSpan); - } - - /// The name the class is generated under, unless it is taken by a different class. - /// How and the extension methods refer to the class. - /// The class's source, referring to itself through . private sealed record ConditionClassModel( string ClassName, - string Reference, string Source ); @@ -1500,7 +1322,6 @@ private record CreateAssertionAttributeData( bool RequiresGenericTypeParameter, bool TreatAsInstance, string? ExpectationMessage, - bool IsUnboundGeneric, - SyntaxReference? AttributeSyntax + bool IsUnboundGeneric ); } diff --git a/src/TUnit.Assertions/Attributes/AssertionFromAttribute.cs b/src/TUnit.Assertions/Attributes/AssertionFromAttribute.cs index c832e56714f..3c749ababb3 100644 --- a/src/TUnit.Assertions/Attributes/AssertionFromAttribute.cs +++ b/src/TUnit.Assertions/Attributes/AssertionFromAttribute.cs @@ -134,17 +134,6 @@ public AssertionFromAttribute(Type targetType, Type containingType, string metho /// Optional custom expectation message shown in assertion failures. /// If not specified, a default message based on the method name will be used. /// - /// - /// - /// Write the positive phrase without the leading "to", e.g. "be null or empty": the generator adds "to", - /// and "not" when NegateLogic is set. A leading "to " is ignored, and a phrase that already starts with - /// "not " (after any "to ") is used as written instead of being negated again. - /// - /// - /// A placeholder such as {value} is replaced with that method parameter's value. Everything else is literal - /// text: write {{ and }} for literal braces; a placeholder that names no parameter is printed as is. - /// - /// /// /// /// [AssertionFrom(typeof(double), "IsNaN", ExpectationMessage = "be NaN")] diff --git a/src/TUnit.Assertions/Attributes/AssertionFromAttribute`1.cs b/src/TUnit.Assertions/Attributes/AssertionFromAttribute`1.cs index 3ddf21c2a87..8a18aa72e8d 100644 --- a/src/TUnit.Assertions/Attributes/AssertionFromAttribute`1.cs +++ b/src/TUnit.Assertions/Attributes/AssertionFromAttribute`1.cs @@ -130,18 +130,12 @@ public AssertionFromAttribute(Type containingType, string methodName) /// It appears as: "Expected {value} to {expectation} but {actual_result}". /// /// - /// Write the positive phrase without the leading "to", e.g. "be null or empty": the generator adds "to", - /// and "not" when NegateLogic is set. A leading "to " is ignored, and a phrase that already starts with - /// "not " (after any "to ") is used as written instead of being negated again. - /// - /// - /// A placeholder such as {value} is replaced with that method parameter's value. Everything else is literal - /// text: write {{ and }} for literal braces; a placeholder that names no parameter is printed as is. + /// You can use parameter placeholders like {param1}, {param2} which will be replaced with actual values. /// /// /// /// - /// [AssertionFrom<string>("StartsWith", ExpectationMessage = "start with '{value}'")] + /// [AssertionFrom<string>("StartsWith", ExpectationMessage = "start with {value}")] /// // Error message: "Expected 'hello' to start with 'xyz' but..." /// /// diff --git a/src/TUnit.Assertions/Conditions/SpanAssertionExtensions.cs b/src/TUnit.Assertions/Conditions/SpanAssertionExtensions.cs index d8fabb81bfe..f87d05bd0f6 100644 --- a/src/TUnit.Assertions/Conditions/SpanAssertionExtensions.cs +++ b/src/TUnit.Assertions/Conditions/SpanAssertionExtensions.cs @@ -1,28 +1,24 @@ +using TUnit.Assertions.Attributes; + +namespace TUnit.Assertions.Conditions; + #if NET5_0_OR_GREATER using System; -namespace TUnit.Assertions.Conditions -{ - /// - /// No longer holds any assertions. It only ever carried [AssertionFrom] declarations for Memory<T> and - /// ReadOnlyMemory<T> that generated nothing; IsEmpty() and IsNotEmpty() for those types are on the source - /// returned by Assert.That. Kept so that removing it is not a breaking change. - /// - [Obsolete("Has no members. Memory and ReadOnlyMemory assertions such as IsEmpty() are on MemoryAssertionBase.")] - public static class SpanAssertionExtensions - { - } -} +/// +/// Source-generated assertions for Memory<T> and ReadOnlyMemory<T> types using [AssertionFrom] attributes. +/// Note: Span types cannot be used due to ref struct constraints in the assertion framework. +/// Available in .NET 5.0+ +/// + +// Memory assertions +[AssertionFrom(typeof(Memory<>), nameof(Memory.IsEmpty), ExpectationMessage = "be empty")] +[AssertionFrom(typeof(Memory<>), nameof(Memory.IsEmpty), CustomName = "IsNotEmpty", NegateLogic = true, ExpectationMessage = "be empty")] -namespace TUnit.Assertions.Extensions +// ReadOnlyMemory assertions +[AssertionFrom(typeof(ReadOnlyMemory<>), nameof(ReadOnlyMemory.IsEmpty), ExpectationMessage = "be empty")] +[AssertionFrom(typeof(ReadOnlyMemory<>), nameof(ReadOnlyMemory.IsEmpty), CustomName = "IsNotEmpty", NegateLogic = true, ExpectationMessage = "be empty")] +public static partial class SpanAssertionExtensions { - /// - /// No longer holds any assertions; see . - /// Kept so that removing it is not a breaking change. - /// - [Obsolete("Has no members. Memory and ReadOnlyMemory assertions such as IsEmpty() are on MemoryAssertionBase.")] - public static class SpanAssertionExtensions - { - } } #endif diff --git a/tests/TUnit.Analyzers.Tests/PublicMethodMissingTestAttributeAnalyzerTests.cs b/tests/TUnit.Analyzers.Tests/PublicMethodMissingTestAttributeAnalyzerTests.cs index 2ee35f52daf..f09738aa610 100644 --- a/tests/TUnit.Analyzers.Tests/PublicMethodMissingTestAttributeAnalyzerTests.cs +++ b/tests/TUnit.Analyzers.Tests/PublicMethodMissingTestAttributeAnalyzerTests.cs @@ -153,81 +153,6 @@ public ValueTask DisposeAsync() ); } - [Test] - public async Task Interface_Implementation_No_Error() - { - await Verifier - .VerifyAnalyzerAsync( - """ - using System.Threading.Tasks; - using TUnit.Core; - using TUnit.Core.Interfaces; - - public interface IGreeter - { - T Greet(); - } - - public class MyClass : ITestStartEventReceiver, IGreeter - { - public int Order => 0; - - [Test] - public void MyTest() - { - } - - public ValueTask OnTestStart(TestContext context) - { - return default; - } - - public string Greet() - { - return "Hello"; - } - } - """ - ); - } - - [Test] - public async Task Same_Name_As_Interface_Member_But_Not_Implementing_It_Error() - { - var expected = Verifier.Diagnostic(Rules.PublicMethodMissingTestAttribute).WithLocation(0); - - await Verifier - .VerifyAnalyzerAsync( - """ - using TUnit.Core; - - public interface IGreeter - { - string Greet(); - } - - public class MyClass : IGreeter - { - [Test] - public void MyTest() - { - } - - string IGreeter.Greet() - { - return "Hello"; - } - - public string {|#0:Greet|}(string name) - { - return name; - } - } - """, - expected - ); - } - [Test] public async Task Property_Getter_Setter_No_Error() { diff --git a/tests/TUnit.Aspire.Tests/AspireDiagnosticsTests.cs b/tests/TUnit.Aspire.Tests/AspireDiagnosticsTests.cs index 70724861b56..612064f2dd7 100644 --- a/tests/TUnit.Aspire.Tests/AspireDiagnosticsTests.cs +++ b/tests/TUnit.Aspire.Tests/AspireDiagnosticsTests.cs @@ -115,29 +115,6 @@ public async Task DescribeState_OutOfMemory_MentionsOom() await Assert.That(DescribeState(diagnosis)).Contains("OOM / SIGKILL"); } - [Test] - public async Task DescribeState_OutOfMemory_FormatsNegativeExitCodeInvariantly() - { - // Windows reports out-of-memory as a negative NTSTATUS; sv-SE would print it with U+2212 MINUS SIGN. - var swedish = new System.Globalization.CultureInfo("sv-SE"); - await Assert.That(swedish.NumberFormat.NegativeSign).IsEqualTo("\u2212"); - - var diagnosis = new ResourceDiagnosis("cache", "Exited", -1073741801, ResourceFailureClass.OutOfMemory, null, null); - var original = System.Globalization.CultureInfo.CurrentCulture; - string description; - try - { - System.Globalization.CultureInfo.CurrentCulture = swedish; - description = DescribeState(diagnosis); - } - finally - { - System.Globalization.CultureInfo.CurrentCulture = original; - } - - await Assert.That(description).Contains("exit code -1073741801"); - } - [Test] public async Task DescribeState_HealthCheckFailing_IncludesDetail() { diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/AssemblyAssertionGeneratorTests.GeneratesAssemblyAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/AssemblyAssertionGeneratorTests.GeneratesAssemblyAssertions.DotNet10_0.verified.txt index 9fd25097b08..4c5d4d312fb 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/AssemblyAssertionGeneratorTests.GeneratesAssemblyAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/AssemblyAssertionGeneratorTests.GeneratesAssemblyAssertions.DotNet10_0.verified.txt @@ -43,7 +43,7 @@ public class AssemblyIsDynamicAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be dynamic"; + return $"{(_negated ? "not " : "")} be dynamic"; } } @@ -79,7 +79,7 @@ public class AssemblyIsFullyTrustedAssertion : Assertion -/// Runs over a single source text, without snapshot verification, for tests -/// that assert on specific parts of its output. -/// -internal static class AssertionMethodGeneratorRunner -{ - private static readonly Regex ExpectationRegex = new( - @"protected override string GetExpectation\(\)\s*\{\s*return (?[^\r\n]*);"); - - /// The path the input source is parsed with, so diagnostics can point into it. - public const string InputPath = "Input.cs"; - - /// Runs the generator over and returns its generated sources and diagnostics. - public static (string[] Sources, ImmutableArray Diagnostics) Run(string source) - { -#if NET - string[] preprocessorSymbols = ["NET", "NET5_0_OR_GREATER", "NET6_0_OR_GREATER", "NET7_0_OR_GREATER", "NET8_0_OR_GREATER"]; -#else - string[] preprocessorSymbols = []; -#endif - var parseOptions = CSharpParseOptions.Default.WithPreprocessorSymbols(preprocessorSymbols); - - var compilation = CSharpCompilation.Create( - nameof(AssertionMethodGeneratorRunner), - [ - CSharpSyntaxTree.ParseText(source, parseOptions, path: InputPath), - CSharpSyntaxTree.ParseText( - """ - global using global::System; - global using global::System.Collections.Generic; - global using global::System.IO; - global using global::System.Linq; - global using global::System.Net.Http; - global using global::System.Threading; - global using global::System.Threading.Tasks; - """, - parseOptions) - ], - ReferencesHelper.References, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - - var result = CSharpGeneratorDriver.Create(new AssertionMethodGenerator()) - .WithUpdatedParseOptions(parseOptions) - .RunGenerators(compilation) - .GetRunResult() - .Results - .Single(); - - if (result.Exception is not null) - { - throw new InvalidOperationException("The generator failed.", result.Exception); - } - - return (result.GeneratedSources.Select(x => x.SourceText.ToString()).ToArray(), result.Diagnostics); - } - - /// The expression each generated GetExpectation() in returns. - public static string[] Expectations(IEnumerable sources) => - sources - .SelectMany(source => ExpectationRegex.Matches(source).Cast()) - .Select(match => match.Groups["expectation"].Value) - .ToArray(); -} diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs b/tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs deleted file mode 100644 index 826961d9bf8..00000000000 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/BuiltInExpectationMessageTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Text.RegularExpressions; -using TUnit.Assertions.SourceGenerator.Generators; - -namespace TUnit.Assertions.SourceGenerator.Tests; - -/// -/// Runs over every built-in [AssertionFrom] declaration, including the files -/// without a snapshot test, and checks the rendered expectations read as one sentence. -/// -internal class BuiltInExpectationMessageTests -{ - private static readonly Regex RepeatedWordRegex = new( - @"\b(to|not) \1\b|\{\(_negated \? ""not "" : """"\)\}(to|not) "); - - /// - /// The source files under src/TUnit.Assertions that declare [AssertionFrom], relative to src so - /// test names stay the same on every machine. - /// - public static IEnumerable AssertionFromFiles() => - Directory.EnumerateFiles(Path.Combine(Git.SourceDirectory.FullName, "TUnit.Assertions"), "*.cs", SearchOption.AllDirectories) - .Where(file => File.ReadLines(file).Any(line => line.TrimStart().StartsWith("[AssertionFrom", StringComparison.Ordinal))) - .Select(file => file.Substring(Git.SourceDirectory.FullName.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)) - .OrderBy(file => file, StringComparer.Ordinal); - - [Test] - [MethodDataSource(nameof(AssertionFromFiles))] - public async Task Expectations_Do_Not_Repeat_To_Or_Not(string file) - { - var expectations = GenerateExpectations(Path.Combine(Git.SourceDirectory.FullName, file)); - - await Assert.That(expectations.Where(expectation => RepeatedWordRegex.IsMatch(expectation))).IsEmpty(); - } - - [Test] - [MethodDataSource(nameof(AssertionFromFiles))] - public async Task Generated_Source_Has_No_Unresolved_Class_Names(string file) - { - var (sources, _) = AssertionMethodGeneratorRunner.Run(File.ReadAllText(Path.Combine(Git.SourceDirectory.FullName, file))); - - await Assert.That(sources.Where(source => source.Contains('\u0001'))).IsEmpty(); - } - - [Test] - public async Task Built_In_Declarations_Render_Expectations() - { - // Guards the test above against passing vacuously, e.g. if the source files stop resolving. - var expectations = AssertionFromFiles() - .SelectMany(file => GenerateExpectations(Path.Combine(Git.SourceDirectory.FullName, file))) - .ToArray(); - - await Assert.That(expectations.Length).IsGreaterThan(100); - } - - /// - /// Runs over and returns the expression each - /// generated GetExpectation() returns. - /// - private static string[] GenerateExpectations(string file) => - AssertionMethodGeneratorRunner.Expectations(AssertionMethodGeneratorRunner.Run(File.ReadAllText(file)).Sources); -} diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/CancellationTokenAssertionGeneratorTests.GeneratesCancellationTokenAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/CancellationTokenAssertionGeneratorTests.GeneratesCancellationTokenAssertions.DotNet10_0.verified.txt index b4fc6cbe04b..6c67d53dacc 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/CancellationTokenAssertionGeneratorTests.GeneratesCancellationTokenAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/CancellationTokenAssertionGeneratorTests.GeneratesCancellationTokenAssertions.DotNet10_0.verified.txt @@ -38,7 +38,7 @@ public class CancellationTokenCanBeCanceledAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a letter"; + return $"{(_negated ? "not " : "")} be a letter"; } } @@ -69,7 +69,7 @@ public class CharIsDigitWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a digit"; + return $"{(_negated ? "not " : "")} be a digit"; } } @@ -100,7 +100,7 @@ public class CharIsWhiteSpaceWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be whitespace"; + return $"{(_negated ? "not " : "")} be whitespace"; } } @@ -131,7 +131,7 @@ public class CharIsUpperWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be uppercase"; + return $"{(_negated ? "not " : "")} be uppercase"; } } @@ -162,7 +162,7 @@ public class CharIsLowerWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be lowercase"; + return $"{(_negated ? "not " : "")} be lowercase"; } } @@ -193,7 +193,7 @@ public class CharIsControlWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a control character"; + return $"{(_negated ? "not " : "")} be a control character"; } } @@ -224,7 +224,7 @@ public class CharIsPunctuationWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be punctuation"; + return $"{(_negated ? "not " : "")} be punctuation"; } } @@ -255,7 +255,7 @@ public class CharIsSymbolWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a symbol"; + return $"{(_negated ? "not " : "")} be a symbol"; } } @@ -286,7 +286,7 @@ public class CharIsNumberWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a number"; + return $"{(_negated ? "not " : "")} be a number"; } } @@ -317,7 +317,7 @@ public class CharIsSeparatorWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a separator"; + return $"{(_negated ? "not " : "")} be a separator"; } } @@ -348,7 +348,7 @@ public class CharIsSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a surrogate"; + return $"{(_negated ? "not " : "")} be a surrogate"; } } @@ -379,7 +379,7 @@ public class CharIsHighSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a high surrogate"; + return $"{(_negated ? "not " : "")} be a high surrogate"; } } @@ -410,7 +410,7 @@ public class CharIsLowSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a low surrogate"; + return $"{(_negated ? "not " : "")} be a low surrogate"; } } @@ -441,7 +441,7 @@ public class CharIsLetterOrDigitWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a letter or digit"; + return $"{(_negated ? "not " : "")} be a letter or digit"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/CultureInfoAssertionGeneratorTests.GeneratesCultureInfoAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/CultureInfoAssertionGeneratorTests.GeneratesCultureInfoAssertions.DotNet10_0.verified.txt index 6d98165ab97..da206ed108b 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/CultureInfoAssertionGeneratorTests.GeneratesCultureInfoAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/CultureInfoAssertionGeneratorTests.GeneratesCultureInfoAssertions.DotNet10_0.verified.txt @@ -43,7 +43,7 @@ public class CultureInfoIsNeutralCultureAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be during daylight saving time"; + return $"{(_negated ? "not " : "")} be during daylight saving time"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet10_0.verified.txt index d81d25faf67..499386d8aa7 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet10_0.verified.txt @@ -43,7 +43,7 @@ public class DirectoryInfoExistsAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}exist"; + return $"{(_negated ? "not " : "")} exist"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet10_0.verified.txt index 576e8eefe7f..88212af472f 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet10_0.verified.txt @@ -43,7 +43,7 @@ public class EncodingIsSingleByteAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be single-byte encoding"; + return $"{(_negated ? "not " : "")} be single-byte encoding"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs b/tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs deleted file mode 100644 index 1c9cd84a645..00000000000 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/ExpectationMessageRenderingTests.cs +++ /dev/null @@ -1,98 +0,0 @@ -using TUnit.Assertions.SourceGenerator.Generators; - -namespace TUnit.Assertions.SourceGenerator.Tests; - -/// -/// Pins the GetExpectation() code renders for each documented form of -/// [AssertionFrom] ExpectationMessage, and the classes for overloads that share a condition class name. -/// -internal class ExpectationMessageRenderingTests -{ - private const string MessagesSource = - """ - using TUnit.Assertions.Attributes; - - namespace Rendering; - - public static class Methods - { - public static bool Canonical(string value) => true; - public static bool SpelledOutTo(string value) => true; - public static bool SpelledOutNot(string value, string text) => true; - public static bool LeadingNot(string value, string text) => true; - public static bool EscapedBraces(string value, string text) => true; - public static bool UnknownPlaceholder(string value, string ending) => true; - public static bool NonAscii(string value, string önskad) => true; - public static bool GeneratedField(string value, string prefix) => true; - public static bool Quoted(string value) => true; - public static bool Default(string value, string text) => true; - } - - [AssertionFrom(typeof(Methods), nameof(Methods.Canonical), ExpectationMessage = "be blank")] - [AssertionFrom(typeof(Methods), nameof(Methods.SpelledOutTo), ExpectationMessage = "to be spelled out")] - [AssertionFrom(typeof(Methods), nameof(Methods.SpelledOutNot), NegateLogic = true, ExpectationMessage = "to not contain {text}")] - [AssertionFrom(typeof(Methods), nameof(Methods.LeadingNot), NegateLogic = true, ExpectationMessage = "not start with {text}")] - [AssertionFrom(typeof(Methods), nameof(Methods.EscapedBraces), ExpectationMessage = "contain {{text}} and {text}")] - [AssertionFrom(typeof(Methods), nameof(Methods.UnknownPlaceholder), ExpectationMessage = "end with {suffix}")] - [AssertionFrom(typeof(Methods), nameof(Methods.NonAscii), ExpectationMessage = "contain {önskad}")] - [AssertionFrom(typeof(Methods), nameof(Methods.GeneratedField), ExpectationMessage = "start with {_prefix}")] - [AssertionFrom(typeof(Methods), nameof(Methods.Quoted), ExpectationMessage = "be \"quoted\" \\ here")] - [AssertionFrom(typeof(Methods), nameof(Methods.Default))] - public static partial class MessageAssertions; - """; - - private static readonly Lazy RenderedExpectations = new(() => - AssertionMethodGeneratorRunner.Expectations(AssertionMethodGeneratorRunner.Run(MessagesSource).Sources)); - - [Test] - [Arguments("Canonical form", @"$""to {(_negated ? ""not "" : """")}be blank""")] - [Arguments("Leading 'to' is not repeated", @"$""to {(_negated ? ""not "" : """")}be spelled out""")] - [Arguments("Spelled-out 'not' is not negated again", @"$""to not contain {_text}""")] - [Arguments("Leading 'not' without 'to' is not negated again", @"$""to not start with {_text}""")] - [Arguments("Escaped braces stay literal", @"$""to {(_negated ? ""not "" : """")}contain {{text}} and {_text}""")] - [Arguments("Unknown placeholder stays literal", @"$""to {(_negated ? ""not "" : """")}end with {{suffix}}""")] - [Arguments("Non-ASCII placeholder", @"$""to {(_negated ? ""not "" : """")}contain {_önskad}""")] - [Arguments("Generated field placeholder", @"$""to {(_negated ? ""not "" : """")}start with {_prefix}""")] - [Arguments("Quotes and backslashes are escaped", @"$""to {(_negated ? ""not "" : """")}be \""quoted\"" \\ here""")] - [Arguments("Default message", @"$""to {(_negated ? ""not "" : """")}satisfy Default({_text})""")] - public async Task Renders_Expectation(string form, string expected) - { - await Assert.That(RenderedExpectations.Value).Contains(expected); - } - - [Test] - public async Task Generated_Source_Has_No_Unresolved_Class_Names() - { - var (sources, _) = AssertionMethodGeneratorRunner.Run(MessagesSource); - - await Assert.That(sources).IsNotEmpty(); - await Assert.That(sources.Where(source => source.Contains('\u0001'))).IsEmpty(); - } - - [Test] - public async Task Overloads_Sharing_A_Class_Name_Each_Bind_To_Their_Own_Class() - { - var (sources, diagnostics) = AssertionMethodGeneratorRunner.Run( - """ - using TUnit.Assertions.Attributes; - - namespace Rendering; - - public static class Overloads - { - public static bool Matches(string value, int count) => true; - public static bool Matches(string value, bool flag) => true; - } - - [AssertionFrom(typeof(Overloads), nameof(Overloads.Matches))] - public static partial class OverloadAssertions; - """); - - var source = string.Join(Environment.NewLine, sources); - - await Assert.That(diagnostics).IsEmpty(); - await Assert.That(source).Contains("new StringOverloadsMatchesWithStringAnd1MoreAssertion(source.Context, count, false)"); - await Assert.That(source).Contains("new StringOverloadsMatchesWithStringAnd1MoreAssertion2(source.Context, flag, false)"); - await Assert.That(source).Contains("Rendering.Overloads.Matches(actualValue, _flag)"); - } -} diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet10_0.verified.txt index 7648459f207..3a4ee3a2b75 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet10_0.verified.txt @@ -43,7 +43,7 @@ public class FileInfoExistsAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}exist"; + return $"{(_negated ? "not " : "")} exist"; } } @@ -79,7 +79,7 @@ public class FileInfoIsReadOnlyAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be read-only"; + return $"{(_negated ? "not " : "")} be read-only"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/ProcessAssertionGeneratorTests.GeneratesProcessAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/ProcessAssertionGeneratorTests.GeneratesProcessAssertions.DotNet10_0.verified.txt index 983e73965c4..a9a587071ba 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/ProcessAssertionGeneratorTests.GeneratesProcessAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/ProcessAssertionGeneratorTests.GeneratesProcessAssertions.DotNet10_0.verified.txt @@ -43,7 +43,7 @@ public class ProcessHasExitedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}have exited"; + return $"{(_negated ? "not " : "")} have exited"; } } @@ -79,7 +79,7 @@ public class ProcessRespondingAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be responding"; + return $"{(_negated ? "not " : "")} be responding"; } } @@ -115,7 +115,7 @@ public class ProcessEnableRaisingEventsAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be readable"; + return $"{(_negated ? "not " : "")} be readable"; } } @@ -79,7 +79,7 @@ public class StreamCanWriteAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be writable"; + return $"{(_negated ? "not " : "")} be writable"; } } @@ -115,7 +115,7 @@ public class StreamCanSeekAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be seekable"; + return $"{(_negated ? "not " : "")} be seekable"; } } @@ -151,7 +151,7 @@ public class StreamCanTimeoutAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}support timeout"; + return $"{(_negated ? "not " : "")} support timeout"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet10_0.verified.txt index cb6e1d1307b..c675e696189 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet10_0.verified.txt @@ -39,7 +39,7 @@ public class TaskIsCompletedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be completed"; + return $"{(_negated ? "not " : "")} be completed"; } } @@ -71,7 +71,7 @@ public class TaskIsCanceledAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be canceled"; + return $"{(_negated ? "not " : "")} be canceled"; } } @@ -103,7 +103,7 @@ public class TaskIsFaultedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be faulted"; + return $"{(_negated ? "not " : "")} be faulted"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/ThreadAssertionGeneratorTests.GeneratesThreadAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/ThreadAssertionGeneratorTests.GeneratesThreadAssertions.DotNet10_0.verified.txt index c8ff87e3ead..b414d544488 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/ThreadAssertionGeneratorTests.GeneratesThreadAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/ThreadAssertionGeneratorTests.GeneratesThreadAssertions.DotNet10_0.verified.txt @@ -43,7 +43,7 @@ public class ThreadIsAliveAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be alive"; + return $"{(_negated ? "not " : "")} be alive"; } } @@ -79,7 +79,7 @@ public class ThreadIsBackgroundAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a background thread"; + return $"{(_negated ? "not " : "")} be a background thread"; } } @@ -115,7 +115,7 @@ public class ThreadIsThreadPoolThreadAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a class"; + return $"{(_negated ? "not " : "")} be a class"; } } @@ -79,7 +79,7 @@ public class TypeIsInterfaceAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be an interface"; + return $"{(_negated ? "not " : "")} be an interface"; } } @@ -115,7 +115,7 @@ public class TypeIsAbstractAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be abstract"; + return $"{(_negated ? "not " : "")} be abstract"; } } @@ -151,7 +151,7 @@ public class TypeIsSealedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be sealed"; + return $"{(_negated ? "not " : "")} be sealed"; } } @@ -187,7 +187,7 @@ public class TypeIsValueTypeAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a value type"; + return $"{(_negated ? "not " : "")} be a value type"; } } @@ -223,7 +223,7 @@ public class TypeIsEnumAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be an enum"; + return $"{(_negated ? "not " : "")} be an enum"; } } @@ -259,7 +259,7 @@ public class TypeIsPrimitiveAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a primitive type"; + return $"{(_negated ? "not " : "")} be a primitive type"; } } @@ -295,7 +295,7 @@ public class TypeIsPublicAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be public"; + return $"{(_negated ? "not " : "")} be public"; } } @@ -331,7 +331,7 @@ public class TypeIsGenericTypeAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a generic type"; + return $"{(_negated ? "not " : "")} be a generic type"; } } @@ -367,7 +367,7 @@ public class TypeIsGenericTypeDefinitionAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a generic type definition"; + return $"{(_negated ? "not " : "")} be a generic type definition"; } } @@ -403,7 +403,7 @@ public class TypeIsArrayAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be an array"; + return $"{(_negated ? "not " : "")} be an array"; } } @@ -439,7 +439,7 @@ public class TypeIsByRefAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a by-ref type"; + return $"{(_negated ? "not " : "")} be a by-ref type"; } } @@ -475,7 +475,7 @@ public class TypeIsPointerAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a pointer type"; + return $"{(_negated ? "not " : "")} be a pointer type"; } } @@ -511,7 +511,7 @@ public class TypeIsNestedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested type"; + return $"{(_negated ? "not " : "")} be a nested type"; } } @@ -547,7 +547,7 @@ public class TypeIsNestedPublicAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested public type"; + return $"{(_negated ? "not " : "")} be a nested public type"; } } @@ -583,7 +583,7 @@ public class TypeIsNestedPrivateAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested private type"; + return $"{(_negated ? "not " : "")} be a nested private type"; } } @@ -619,7 +619,7 @@ public class TypeIsNestedAssemblyAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested assembly type"; + return $"{(_negated ? "not " : "")} be a nested assembly type"; } } @@ -655,7 +655,7 @@ public class TypeIsNestedFamilyAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested family type"; + return $"{(_negated ? "not " : "")} be a nested family type"; } } @@ -691,7 +691,7 @@ public class TypeIsVisibleAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be visible"; + return $"{(_negated ? "not " : "")} be visible"; } } @@ -727,7 +727,7 @@ public class TypeIsConstructedGenericTypeAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a constructed generic type"; + return $"{(_negated ? "not " : "")} be a constructed generic type"; } } @@ -763,7 +763,7 @@ public class TypeContainsGenericParametersAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}contain generic parameters"; + return $"{(_negated ? "not " : "")} contain generic parameters"; } } @@ -799,7 +799,7 @@ public class TypeIsSerializableAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be serializable"; + return $"{(_negated ? "not " : "")} be serializable"; } } @@ -835,7 +835,7 @@ public class TypeIsCOMObjectAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a COM object"; + return $"{(_negated ? "not " : "")} be a COM object"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet10_0.verified.txt index d51d59a2add..4dbf3aa8313 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet10_0.verified.txt @@ -43,7 +43,7 @@ public class UriIsAbsoluteUriAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be an absolute URI"; + return $"{(_negated ? "not " : "")} be an absolute URI"; } } @@ -79,7 +79,7 @@ public class UriIsFileAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a file URI"; + return $"{(_negated ? "not " : "")} be a file URI"; } } @@ -115,7 +115,7 @@ public class UriIsUncAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a UNC URI"; + return $"{(_negated ? "not " : "")} be a UNC URI"; } } @@ -151,7 +151,7 @@ public class UriIsLoopbackAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a loopback URI"; + return $"{(_negated ? "not " : "")} be a loopback URI"; } } @@ -187,7 +187,7 @@ public class UriIsDefaultPortAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}use the default port"; + return $"{(_negated ? "not " : "")} use the default port"; } } @@ -223,7 +223,7 @@ public class UriUserEscapedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be user-escaped"; + return $"{(_negated ? "not " : "")} be user-escaped"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/WeakReferenceAssertionGeneratorTests.GeneratesWeakReferenceAssertions.DotNet10_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/WeakReferenceAssertionGeneratorTests.GeneratesWeakReferenceAssertions.DotNet10_0.verified.txt index e61820f053f..073d551b3c2 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/WeakReferenceAssertionGeneratorTests.GeneratesWeakReferenceAssertions.DotNet10_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/WeakReferenceAssertionGeneratorTests.GeneratesWeakReferenceAssertions.DotNet10_0.verified.txt @@ -43,7 +43,7 @@ public class WeakReferenceIsAliveAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be alive"; + return $"{(_negated ? "not " : "")} be alive"; } } @@ -79,7 +79,7 @@ public class WeakReferenceTrackResurrectionAssertion : Assertion -/// [AssertionFrom] expectation messages written in the documented style, which spells out the leading "to" -/// (and, with NegateLogic, the "not") that the generator otherwise adds itself. -/// -[AssertionFrom(nameof(string.IsNullOrEmpty), CustomName = "IsBlankDocumentedStyle", ExpectationMessage = "to be null or empty")] -[AssertionFrom(nameof(string.IsNullOrEmpty), CustomName = "IsNotBlankDocumentedStyle", NegateLogic = true, ExpectationMessage = "to be null or empty")] -[AssertionFrom(nameof(string.IsNullOrWhiteSpace), CustomName = "HasTextDocumentedStyle", NegateLogic = true, ExpectationMessage = "to not be null, empty, or whitespace")] -[AssertionFrom(nameof(string.StartsWith), CustomName = "BeginsWithTextDocumentedStyle", ExpectationMessage = "to start with '{value}'")] -[AssertionFrom(nameof(string.Contains), CustomName = "LacksTextDocumentedStyle", NegateLogic = true, ExpectationMessage = "to not contain '{value}'")] -[AssertionFrom(nameof(string.Contains), CustomName = "HasTextSharingMethod", ExpectationMessage = "contain '{value}'")] -public static partial class DocumentedStyleStringAssertions; - -// Same target method as an attribute above, but declared on another class and with another message. -[AssertionFrom(nameof(string.IsNullOrEmpty), CustomName = "IsVacant", ExpectationMessage = "be vacant")] -public static partial class SharedMethodStringAssertions; - -[AssertionFrom(typeof(ExpectationMessageMethods), nameof(ExpectationMessageMethods.HasText), ExpectationMessage = "contain {{text}} ({text})")] -[AssertionFrom(typeof(ExpectationMessageMethods), nameof(ExpectationMessageMethods.HasSuffix), ExpectationMessage = "end with {suffix}")] -[AssertionFrom(typeof(ExpectationMessageMethods), nameof(ExpectationMessageMethods.IsEmptyText), ExpectationMessage = "be \"blank\" \\ empty")] -[AssertionFrom(typeof(ExpectationMessageMethods), nameof(ExpectationMessageMethods.HasWanted), ExpectationMessage = "contain {önskad}")] -[AssertionFrom(typeof(ExpectationMessageMethods), nameof(ExpectationMessageMethods.HasPrefix), ExpectationMessage = "start with {_prefix}")] -public static partial class EscapingStringAssertions; - -// Three attributes on one method, each with its own message: the condition classes get numbered names. -[AssertionFrom(nameof(string.EndsWith), CustomName = "EndsWithFirst", ExpectationMessage = "end with {value} (first)")] -[AssertionFrom(nameof(string.EndsWith), CustomName = "EndsWithSecond", ExpectationMessage = "end with {value} (second)")] -[AssertionFrom(nameof(string.EndsWith), CustomName = "EndsWithThird", ExpectationMessage = "end with {value} (third)")] -public static partial class ThreeMessagesStringAssertions; - -/// -/// Bool-returning methods for the [AssertionFrom] declarations above, so each has its own target method and parameter -/// names that the messages refer to. -/// -public static class ExpectationMessageMethods -{ - public static bool HasText(string value, string text) => value.Contains(text); - - public static bool HasSuffix(string value, string ending) => value.EndsWith(ending, StringComparison.Ordinal); - - public static bool IsEmptyText(string value) => value.Length == 0; - - public static bool HasWanted(string value, string önskad) => value.Contains(önskad); - - public static bool HasPrefix(string value, string prefix) => value.StartsWith(prefix, StringComparison.Ordinal); -} - -public class AssertionFromExpectationMessageTests -{ - [Test] - public async Task Leading_To_Is_Not_Repeated() - { - await Assert.That(async () => await Assert.That("Foo").IsBlankDocumentedStyle()) - .Throws() - .WithMessageContaining("Expected to be null or empty"); - } - - [Test] - public async Task Leading_To_Is_Not_Repeated_When_Negated() - { - await Assert.That(async () => await Assert.That("").IsNotBlankDocumentedStyle()) - .Throws() - .WithMessageContaining("Expected to not be null or empty"); - } - - [Test] - public async Task Spelled_Out_Negation_Is_Not_Repeated() - { - await Assert.That(async () => await Assert.That(" ").HasTextDocumentedStyle()) - .Throws() - .WithMessageContaining("Expected to not be null, empty, or whitespace"); - } - - [Test] - public async Task Parameter_Placeholder_Is_Substituted() - { - await Assert.That(async () => await Assert.That("hello").BeginsWithTextDocumentedStyle("xyz")) - .Throws() - .WithMessageContaining("Expected to start with 'xyz'"); - } - - [Test] - public async Task Parameter_Placeholder_Is_Substituted_With_Spelled_Out_Negation() - { - await Assert.That(async () => await Assert.That("hello").LacksTextDocumentedStyle("ell")) - .Throws() - .WithMessageContaining("Expected to not contain 'ell'"); - } - - [Test] - public async Task Escaped_Braces_Stay_Literal() - { - await Assert.That(async () => await Assert.That("hello").HasText("xyz")) - .Throws() - .WithMessageContaining("Expected to contain {text} (xyz)"); - } - - [Test] - public async Task Placeholder_That_Names_No_Parameter_Stays_Literal() - { - await Assert.That(async () => await Assert.That("hello").HasSuffix("xyz")) - .Throws() - .WithMessageContaining("Expected to end with {suffix}"); - } - - [Test] - public async Task Quotes_And_Backslashes_Stay_Literal() - { - await Assert.That(async () => await Assert.That("Foo").IsEmptyText()) - .Throws() - .WithMessageContaining("Expected to be \"blank\" \\ empty"); - } - - [Test] - public async Task Attributes_Sharing_A_Method_Keep_Their_Own_Messages() - { - await Assert.That(async () => await Assert.That("hello").HasTextSharingMethod("xyz")) - .Throws() - .WithMessageContaining("Expected to contain 'xyz'"); - - await Assert.That(async () => await Assert.That("hello").LacksTextDocumentedStyle("ell")) - .Throws() - .WithMessageContaining("Expected to not contain 'ell'"); - } - - [Test] - public async Task Attributes_Sharing_A_Method_Across_Classes_Keep_Their_Own_Messages() - { - await Assert.That(async () => await Assert.That("Foo").IsVacant()) - .Throws() - .WithMessageContaining("Expected to be vacant"); - - await Assert.That(async () => await Assert.That("Foo").IsBlankDocumentedStyle()) - .Throws() - .WithMessageContaining("Expected to be null or empty"); - } - - [Test] - public async Task Non_Ascii_Placeholder_Is_Substituted() - { - await Assert.That(async () => await Assert.That("hello").HasWanted("xyz")) - .Throws() - .WithMessageContaining("Expected to contain xyz"); - } - - [Test] - public async Task Generated_Field_Placeholder_Is_Still_Substituted() - { - await Assert.That(async () => await Assert.That("hello").HasPrefix("xyz")) - .Throws() - .WithMessageContaining("Expected to start with xyz"); - } - - [Test] - public async Task Three_Attributes_Sharing_A_Method_Keep_Their_Own_Messages() - { - await Assert.That(async () => await Assert.That("hello").EndsWithFirst("xyz")) - .Throws() - .WithMessageContaining("Expected to end with xyz (first)"); - - await Assert.That(async () => await Assert.That("hello").EndsWithSecond("xyz")) - .Throws() - .WithMessageContaining("Expected to end with xyz (second)"); - - await Assert.That(async () => await Assert.That("hello").EndsWithThird("xyz")) - .Throws() - .WithMessageContaining("Expected to end with xyz (third)"); - } - - [Test] - public async Task Classes_Sharing_A_Method_Get_Numbered_Names() - { - var assembly = typeof(AssertionFromExpectationMessageTests).Assembly; - - await Assert.That(assembly.GetType("TUnit.Assertions.Extensions.StringEndsWithWithStringAssertion")).IsNotNull(); - await Assert.That(assembly.GetType("TUnit.Assertions.Extensions.StringEndsWithWithStringAssertion2")).IsNotNull(); - await Assert.That(assembly.GetType("TUnit.Assertions.Extensions.StringEndsWithWithStringAssertion3")).IsNotNull(); - } -} diff --git a/tests/TUnit.Assertions.Tests/AssertionGroupTests.cs b/tests/TUnit.Assertions.Tests/AssertionGroupTests.cs index 5cfa5bb2c9f..2744571af6c 100644 --- a/tests/TUnit.Assertions.Tests/AssertionGroupTests.cs +++ b/tests/TUnit.Assertions.Tests/AssertionGroupTests.cs @@ -50,17 +50,20 @@ public async Task Complex_Or_With_Delegates() } } - [Test] - public async Task And_Condition_Throws_As_Expected() - { - var value = "Foo"; + // Disabled until [AssertionFrom] custom expectation messages get their leading "to", which this message + // relies on - await Assert.That(async () => - await Assert.That(value).IsNullOrEmpty().And.IsEqualTo("Foo") - ).Throws() - .And - .HasMessageContaining("to be null or empty") - .And - .HasMessageContaining("Foo"); - } + //[Test] + //public async Task And_Condition_Throws_As_Expected() + //{ + // var value = "Foo"; + + // await Assert.That(async () => + // await Assert.That(value).IsNullOrEmpty().And.IsEqualTo("Foo") + // ).Throws() + // .And + // .HasMessageContaining("to be null or empty") + // .And + // .HasMessageContaining("Foo"); + //} } diff --git a/tests/TUnit.Assertions.Tests/StringNullabilityAssertionTests.cs b/tests/TUnit.Assertions.Tests/StringNullabilityAssertionTests.cs index 6bc99046ddd..2456ef9e74c 100644 --- a/tests/TUnit.Assertions.Tests/StringNullabilityAssertionTests.cs +++ b/tests/TUnit.Assertions.Tests/StringNullabilityAssertionTests.cs @@ -82,22 +82,6 @@ await Assert.That(async () => await Assert.That(emptyString).IsNotNullOrEmpty()) .Throws(); } - [Test] - public async Task IsNullOrEmpty_Failure_Messages_Read_As_Sentences() - { - // [AssertionFrom] expectation messages omit the leading "to"; the generated assertion adds it. - var value = "Foo"; - var emptyString = ""; - - await Assert.That(async () => await Assert.That(value).IsNullOrEmpty()) - .Throws() - .WithMessageContaining("Expected to be null or empty"); - - await Assert.That(async () => await Assert.That(emptyString).IsNotNullOrEmpty()) - .Throws() - .WithMessageContaining("Expected to not be null or empty"); - } - [Test] public async Task IsNotNullOrEmpty_WithNonEmptyString_Passes() { diff --git a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt index 2585928dfd4..a3bc5816772 100644 --- a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -1994,8 +1994,10 @@ namespace .Conditions [.("IsSubnormal", ExpectationMessage="be subnormal")] public static class SingleAssertionExtensions { } public sealed class SingleItemSource : . { } - [("Has no members. Memory and ReadOnlyMemory assertions such as IsEmpty() are " + - "on MemoryAssertionBase.")] + [.(typeof(), "IsEmpty", CustomName="IsNotEmpty", ExpectationMessage="be empty", NegateLogic=true)] + [.(typeof(), "IsEmpty", ExpectationMessage="be empty")] + [.(typeof(), "IsEmpty", CustomName="IsNotEmpty", ExpectationMessage="be empty", NegateLogic=true)] + [.(typeof(), "IsEmpty", ExpectationMessage="be empty")] public static class SpanAssertionExtensions { } [.("Contains")] public class StringContainsAssertion : . @@ -6099,8 +6101,6 @@ namespace .Extensions public static .<., TItem> SetEquals(this .<.> source, . other, [.("other")] string? expression = null) { } public static .<., TItem> SetEquals(this .<.> source, . other, [.("other")] string? expression = null) { } } - [("Has no members. Memory and ReadOnlyMemory assertions such as IsEmpty() are " + - "on MemoryAssertionBase.")] public static class SpanAssertionExtensions { } public static class StreamAssertionExtensions { diff --git a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt index b14401b9cb5..f088c0c775e 100644 --- a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -1976,8 +1976,10 @@ namespace .Conditions [.("IsSubnormal", ExpectationMessage="be subnormal")] public static class SingleAssertionExtensions { } public sealed class SingleItemSource : . { } - [("Has no members. Memory and ReadOnlyMemory assertions such as IsEmpty() are " + - "on MemoryAssertionBase.")] + [.(typeof(), "IsEmpty", CustomName="IsNotEmpty", ExpectationMessage="be empty", NegateLogic=true)] + [.(typeof(), "IsEmpty", ExpectationMessage="be empty")] + [.(typeof(), "IsEmpty", CustomName="IsNotEmpty", ExpectationMessage="be empty", NegateLogic=true)] + [.(typeof(), "IsEmpty", ExpectationMessage="be empty")] public static class SpanAssertionExtensions { } [.("Contains")] public class StringContainsAssertion : . @@ -6007,8 +6009,6 @@ namespace .Extensions public static .<., TItem> SetEquals(this .<.> source, . other, [.("other")] string? expression = null) { } public static .<., TItem> SetEquals(this .<.> source, . other, [.("other")] string? expression = null) { } } - [("Has no members. Memory and ReadOnlyMemory assertions such as IsEmpty() are " + - "on MemoryAssertionBase.")] public static class SpanAssertionExtensions { } public static class StreamAssertionExtensions { diff --git a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt index 9bd6b424f9e..3af6e98b1a7 100644 --- a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -1994,8 +1994,10 @@ namespace .Conditions [.("IsSubnormal", ExpectationMessage="be subnormal")] public static class SingleAssertionExtensions { } public sealed class SingleItemSource : . { } - [("Has no members. Memory and ReadOnlyMemory assertions such as IsEmpty() are " + - "on MemoryAssertionBase.")] + [.(typeof(), "IsEmpty", CustomName="IsNotEmpty", ExpectationMessage="be empty", NegateLogic=true)] + [.(typeof(), "IsEmpty", ExpectationMessage="be empty")] + [.(typeof(), "IsEmpty", CustomName="IsNotEmpty", ExpectationMessage="be empty", NegateLogic=true)] + [.(typeof(), "IsEmpty", ExpectationMessage="be empty")] public static class SpanAssertionExtensions { } [.("Contains")] public class StringContainsAssertion : . @@ -6099,8 +6101,6 @@ namespace .Extensions public static .<., TItem> SetEquals(this .<.> source, . other, [.("other")] string? expression = null) { } public static .<., TItem> SetEquals(this .<.> source, . other, [.("other")] string? expression = null) { } } - [("Has no members. Memory and ReadOnlyMemory assertions such as IsEmpty() are " + - "on MemoryAssertionBase.")] public static class SpanAssertionExtensions { } public static class StreamAssertionExtensions { From faf29b0ab25e0d9f7d811b7437ac5127f434798a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Mon, 19 Oct 2026 14:42:18 +0000 Subject: [PATCH 43/49] test: keep main's .NET 8, .NET 9 and .NET Framework assertion generator snapshots The production revert restored main's .NET 10 snapshots but left the other frameworks expecting the reverted generator's wording. --- ...sAssemblyAssertions.DotNet8_0.verified.txt | 4 +- ...sAssemblyAssertions.DotNet9_0.verified.txt | 4 +- ...atesAssemblyAssertions.Net4_7.verified.txt | 4 +- ...tionTokenAssertions.DotNet8_0.verified.txt | 4 +- ...tionTokenAssertions.DotNet9_0.verified.txt | 4 +- ...llationTokenAssertions.Net4_7.verified.txt | 4 +- ...ratesCharAssertions.DotNet8_0.verified.txt | 28 +++++------ ...ratesCharAssertions.DotNet9_0.verified.txt | 28 +++++------ ...eneratesCharAssertions.Net4_7.verified.txt | 28 +++++------ ...ltureInfoAssertions.DotNet8_0.verified.txt | 4 +- ...ltureInfoAssertions.DotNet9_0.verified.txt | 4 +- ...sCultureInfoAssertions.Net4_7.verified.txt | 4 +- ...sDateTimeAssertions.DotNet8_0.verified.txt | 2 +- ...sDateTimeAssertions.DotNet9_0.verified.txt | 2 +- ...atesDateTimeAssertions.Net4_7.verified.txt | 2 +- ...ctoryInfoAssertions.DotNet8_0.verified.txt | 2 +- ...ctoryInfoAssertions.DotNet9_0.verified.txt | 2 +- ...irectoryInfoAssertions.Net4_7.verified.txt | 2 +- ...sEncodingAssertions.DotNet8_0.verified.txt | 2 +- ...sEncodingAssertions.DotNet9_0.verified.txt | 2 +- ...atesEncodingAssertions.Net4_7.verified.txt | 2 +- ...sFileInfoAssertions.DotNet8_0.verified.txt | 4 +- ...sFileInfoAssertions.DotNet9_0.verified.txt | 4 +- ...atesFileInfoAssertions.Net4_7.verified.txt | 4 +- ...esProcessAssertions.DotNet8_0.verified.txt | 6 +-- ...esProcessAssertions.DotNet9_0.verified.txt | 6 +-- ...ratesProcessAssertions.Net4_7.verified.txt | 6 +-- ...tesStreamAssertions.DotNet8_0.verified.txt | 8 ++-- ...tesStreamAssertions.DotNet9_0.verified.txt | 8 ++-- ...eratesStreamAssertions.Net4_7.verified.txt | 8 ++-- ...ratesTaskAssertions.DotNet8_0.verified.txt | 6 +-- ...ratesTaskAssertions.DotNet9_0.verified.txt | 6 +-- ...eneratesTaskAssertions.Net4_7.verified.txt | 6 +-- ...tesThreadAssertions.DotNet8_0.verified.txt | 6 +-- ...tesThreadAssertions.DotNet9_0.verified.txt | 6 +-- ...eratesThreadAssertions.Net4_7.verified.txt | 6 +-- ...eZoneInfoAssertions.DotNet8_0.verified.txt | 2 +- ...eZoneInfoAssertions.DotNet9_0.verified.txt | 2 +- ...TimeZoneInfoAssertions.Net4_7.verified.txt | 2 +- ...ratesTypeAssertions.DotNet8_0.verified.txt | 46 +++++++++---------- ...ratesTypeAssertions.DotNet9_0.verified.txt | 46 +++++++++---------- ...eneratesTypeAssertions.Net4_7.verified.txt | 46 +++++++++---------- ...eratesUriAssertions.DotNet8_0.verified.txt | 12 ++--- ...eratesUriAssertions.DotNet9_0.verified.txt | 12 ++--- ...GeneratesUriAssertions.Net4_7.verified.txt | 12 ++--- ...ReferenceAssertions.DotNet8_0.verified.txt | 4 +- ...ReferenceAssertions.DotNet9_0.verified.txt | 4 +- ...eakReferenceAssertions.Net4_7.verified.txt | 4 +- 48 files changed, 210 insertions(+), 210 deletions(-) diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/AssemblyAssertionGeneratorTests.GeneratesAssemblyAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/AssemblyAssertionGeneratorTests.GeneratesAssemblyAssertions.DotNet8_0.verified.txt index 9fd25097b08..4c5d4d312fb 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/AssemblyAssertionGeneratorTests.GeneratesAssemblyAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/AssemblyAssertionGeneratorTests.GeneratesAssemblyAssertions.DotNet8_0.verified.txt @@ -43,7 +43,7 @@ public class AssemblyIsDynamicAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be dynamic"; + return $"{(_negated ? "not " : "")} be dynamic"; } } @@ -79,7 +79,7 @@ public class AssemblyIsFullyTrustedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be dynamic"; + return $"{(_negated ? "not " : "")} be dynamic"; } } @@ -79,7 +79,7 @@ public class AssemblyIsFullyTrustedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be dynamic"; + return $"{(_negated ? "not " : "")} be dynamic"; } } @@ -79,7 +79,7 @@ public class AssemblyIsFullyTrustedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a letter"; + return $"{(_negated ? "not " : "")} be a letter"; } } @@ -69,7 +69,7 @@ public class CharIsDigitWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a digit"; + return $"{(_negated ? "not " : "")} be a digit"; } } @@ -100,7 +100,7 @@ public class CharIsWhiteSpaceWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be whitespace"; + return $"{(_negated ? "not " : "")} be whitespace"; } } @@ -131,7 +131,7 @@ public class CharIsUpperWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be uppercase"; + return $"{(_negated ? "not " : "")} be uppercase"; } } @@ -162,7 +162,7 @@ public class CharIsLowerWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be lowercase"; + return $"{(_negated ? "not " : "")} be lowercase"; } } @@ -193,7 +193,7 @@ public class CharIsControlWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a control character"; + return $"{(_negated ? "not " : "")} be a control character"; } } @@ -224,7 +224,7 @@ public class CharIsPunctuationWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be punctuation"; + return $"{(_negated ? "not " : "")} be punctuation"; } } @@ -255,7 +255,7 @@ public class CharIsSymbolWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a symbol"; + return $"{(_negated ? "not " : "")} be a symbol"; } } @@ -286,7 +286,7 @@ public class CharIsNumberWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a number"; + return $"{(_negated ? "not " : "")} be a number"; } } @@ -317,7 +317,7 @@ public class CharIsSeparatorWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a separator"; + return $"{(_negated ? "not " : "")} be a separator"; } } @@ -348,7 +348,7 @@ public class CharIsSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a surrogate"; + return $"{(_negated ? "not " : "")} be a surrogate"; } } @@ -379,7 +379,7 @@ public class CharIsHighSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a high surrogate"; + return $"{(_negated ? "not " : "")} be a high surrogate"; } } @@ -410,7 +410,7 @@ public class CharIsLowSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a low surrogate"; + return $"{(_negated ? "not " : "")} be a low surrogate"; } } @@ -441,7 +441,7 @@ public class CharIsLetterOrDigitWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a letter or digit"; + return $"{(_negated ? "not " : "")} be a letter or digit"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.DotNet9_0.verified.txt index 3e7877ad611..4d320d531d6 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.DotNet9_0.verified.txt @@ -38,7 +38,7 @@ public class CharIsLetterWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a letter"; + return $"{(_negated ? "not " : "")} be a letter"; } } @@ -69,7 +69,7 @@ public class CharIsDigitWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a digit"; + return $"{(_negated ? "not " : "")} be a digit"; } } @@ -100,7 +100,7 @@ public class CharIsWhiteSpaceWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be whitespace"; + return $"{(_negated ? "not " : "")} be whitespace"; } } @@ -131,7 +131,7 @@ public class CharIsUpperWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be uppercase"; + return $"{(_negated ? "not " : "")} be uppercase"; } } @@ -162,7 +162,7 @@ public class CharIsLowerWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be lowercase"; + return $"{(_negated ? "not " : "")} be lowercase"; } } @@ -193,7 +193,7 @@ public class CharIsControlWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a control character"; + return $"{(_negated ? "not " : "")} be a control character"; } } @@ -224,7 +224,7 @@ public class CharIsPunctuationWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be punctuation"; + return $"{(_negated ? "not " : "")} be punctuation"; } } @@ -255,7 +255,7 @@ public class CharIsSymbolWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a symbol"; + return $"{(_negated ? "not " : "")} be a symbol"; } } @@ -286,7 +286,7 @@ public class CharIsNumberWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a number"; + return $"{(_negated ? "not " : "")} be a number"; } } @@ -317,7 +317,7 @@ public class CharIsSeparatorWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a separator"; + return $"{(_negated ? "not " : "")} be a separator"; } } @@ -348,7 +348,7 @@ public class CharIsSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a surrogate"; + return $"{(_negated ? "not " : "")} be a surrogate"; } } @@ -379,7 +379,7 @@ public class CharIsHighSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a high surrogate"; + return $"{(_negated ? "not " : "")} be a high surrogate"; } } @@ -410,7 +410,7 @@ public class CharIsLowSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a low surrogate"; + return $"{(_negated ? "not " : "")} be a low surrogate"; } } @@ -441,7 +441,7 @@ public class CharIsLetterOrDigitWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a letter or digit"; + return $"{(_negated ? "not " : "")} be a letter or digit"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.Net4_7.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.Net4_7.verified.txt index 3e7877ad611..4d320d531d6 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.Net4_7.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/CharAssertionGeneratorTests.GeneratesCharAssertions.Net4_7.verified.txt @@ -38,7 +38,7 @@ public class CharIsLetterWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a letter"; + return $"{(_negated ? "not " : "")} be a letter"; } } @@ -69,7 +69,7 @@ public class CharIsDigitWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a digit"; + return $"{(_negated ? "not " : "")} be a digit"; } } @@ -100,7 +100,7 @@ public class CharIsWhiteSpaceWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be whitespace"; + return $"{(_negated ? "not " : "")} be whitespace"; } } @@ -131,7 +131,7 @@ public class CharIsUpperWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be uppercase"; + return $"{(_negated ? "not " : "")} be uppercase"; } } @@ -162,7 +162,7 @@ public class CharIsLowerWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be lowercase"; + return $"{(_negated ? "not " : "")} be lowercase"; } } @@ -193,7 +193,7 @@ public class CharIsControlWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a control character"; + return $"{(_negated ? "not " : "")} be a control character"; } } @@ -224,7 +224,7 @@ public class CharIsPunctuationWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be punctuation"; + return $"{(_negated ? "not " : "")} be punctuation"; } } @@ -255,7 +255,7 @@ public class CharIsSymbolWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a symbol"; + return $"{(_negated ? "not " : "")} be a symbol"; } } @@ -286,7 +286,7 @@ public class CharIsNumberWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a number"; + return $"{(_negated ? "not " : "")} be a number"; } } @@ -317,7 +317,7 @@ public class CharIsSeparatorWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a separator"; + return $"{(_negated ? "not " : "")} be a separator"; } } @@ -348,7 +348,7 @@ public class CharIsSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a surrogate"; + return $"{(_negated ? "not " : "")} be a surrogate"; } } @@ -379,7 +379,7 @@ public class CharIsHighSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a high surrogate"; + return $"{(_negated ? "not " : "")} be a high surrogate"; } } @@ -410,7 +410,7 @@ public class CharIsLowSurrogateWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a low surrogate"; + return $"{(_negated ? "not " : "")} be a low surrogate"; } } @@ -441,7 +441,7 @@ public class CharIsLetterOrDigitWithCharAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a letter or digit"; + return $"{(_negated ? "not " : "")} be a letter or digit"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/CultureInfoAssertionGeneratorTests.GeneratesCultureInfoAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/CultureInfoAssertionGeneratorTests.GeneratesCultureInfoAssertions.DotNet8_0.verified.txt index 6d98165ab97..da206ed108b 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/CultureInfoAssertionGeneratorTests.GeneratesCultureInfoAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/CultureInfoAssertionGeneratorTests.GeneratesCultureInfoAssertions.DotNet8_0.verified.txt @@ -43,7 +43,7 @@ public class CultureInfoIsNeutralCultureAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be during daylight saving time"; + return $"{(_negated ? "not " : "")} be during daylight saving time"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.DotNet9_0.verified.txt index 16c6521b8fc..f29fad142d7 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.DotNet9_0.verified.txt @@ -38,7 +38,7 @@ public class DateTimeIsDaylightSavingTimeAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be during daylight saving time"; + return $"{(_negated ? "not " : "")} be during daylight saving time"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.Net4_7.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.Net4_7.verified.txt index 16c6521b8fc..f29fad142d7 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.Net4_7.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/DateTimeAssertionGeneratorTests.GeneratesDateTimeAssertions.Net4_7.verified.txt @@ -38,7 +38,7 @@ public class DateTimeIsDaylightSavingTimeAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be during daylight saving time"; + return $"{(_negated ? "not " : "")} be during daylight saving time"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet8_0.verified.txt index d81d25faf67..499386d8aa7 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet8_0.verified.txt @@ -43,7 +43,7 @@ public class DirectoryInfoExistsAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}exist"; + return $"{(_negated ? "not " : "")} exist"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet9_0.verified.txt index d81d25faf67..499386d8aa7 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.DotNet9_0.verified.txt @@ -43,7 +43,7 @@ public class DirectoryInfoExistsAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}exist"; + return $"{(_negated ? "not " : "")} exist"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.Net4_7.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.Net4_7.verified.txt index d81d25faf67..499386d8aa7 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.Net4_7.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/DirectoryInfoAssertionGeneratorTests.GeneratesDirectoryInfoAssertions.Net4_7.verified.txt @@ -43,7 +43,7 @@ public class DirectoryInfoExistsAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}exist"; + return $"{(_negated ? "not " : "")} exist"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet8_0.verified.txt index 576e8eefe7f..88212af472f 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet8_0.verified.txt @@ -43,7 +43,7 @@ public class EncodingIsSingleByteAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be single-byte encoding"; + return $"{(_negated ? "not " : "")} be single-byte encoding"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet9_0.verified.txt index 576e8eefe7f..88212af472f 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.DotNet9_0.verified.txt @@ -43,7 +43,7 @@ public class EncodingIsSingleByteAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be single-byte encoding"; + return $"{(_negated ? "not " : "")} be single-byte encoding"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.Net4_7.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.Net4_7.verified.txt index 576e8eefe7f..88212af472f 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.Net4_7.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/EncodingAssertionGeneratorTests.GeneratesEncodingAssertions.Net4_7.verified.txt @@ -43,7 +43,7 @@ public class EncodingIsSingleByteAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be single-byte encoding"; + return $"{(_negated ? "not " : "")} be single-byte encoding"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet8_0.verified.txt index 7648459f207..3a4ee3a2b75 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet8_0.verified.txt @@ -43,7 +43,7 @@ public class FileInfoExistsAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}exist"; + return $"{(_negated ? "not " : "")} exist"; } } @@ -79,7 +79,7 @@ public class FileInfoIsReadOnlyAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be read-only"; + return $"{(_negated ? "not " : "")} be read-only"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet9_0.verified.txt index 7648459f207..3a4ee3a2b75 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.DotNet9_0.verified.txt @@ -43,7 +43,7 @@ public class FileInfoExistsAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}exist"; + return $"{(_negated ? "not " : "")} exist"; } } @@ -79,7 +79,7 @@ public class FileInfoIsReadOnlyAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be read-only"; + return $"{(_negated ? "not " : "")} be read-only"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.Net4_7.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.Net4_7.verified.txt index 7648459f207..3a4ee3a2b75 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.Net4_7.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/FileInfoAssertionGeneratorTests.GeneratesFileInfoAssertions.Net4_7.verified.txt @@ -43,7 +43,7 @@ public class FileInfoExistsAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}exist"; + return $"{(_negated ? "not " : "")} exist"; } } @@ -79,7 +79,7 @@ public class FileInfoIsReadOnlyAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be read-only"; + return $"{(_negated ? "not " : "")} be read-only"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/ProcessAssertionGeneratorTests.GeneratesProcessAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/ProcessAssertionGeneratorTests.GeneratesProcessAssertions.DotNet8_0.verified.txt index 983e73965c4..a9a587071ba 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/ProcessAssertionGeneratorTests.GeneratesProcessAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/ProcessAssertionGeneratorTests.GeneratesProcessAssertions.DotNet8_0.verified.txt @@ -43,7 +43,7 @@ public class ProcessHasExitedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}have exited"; + return $"{(_negated ? "not " : "")} have exited"; } } @@ -79,7 +79,7 @@ public class ProcessRespondingAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be responding"; + return $"{(_negated ? "not " : "")} be responding"; } } @@ -115,7 +115,7 @@ public class ProcessEnableRaisingEventsAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}have exited"; + return $"{(_negated ? "not " : "")} have exited"; } } @@ -79,7 +79,7 @@ public class ProcessRespondingAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be responding"; + return $"{(_negated ? "not " : "")} be responding"; } } @@ -115,7 +115,7 @@ public class ProcessEnableRaisingEventsAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}have exited"; + return $"{(_negated ? "not " : "")} have exited"; } } @@ -79,7 +79,7 @@ public class ProcessRespondingAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be responding"; + return $"{(_negated ? "not " : "")} be responding"; } } @@ -115,7 +115,7 @@ public class ProcessEnableRaisingEventsAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be readable"; + return $"{(_negated ? "not " : "")} be readable"; } } @@ -79,7 +79,7 @@ public class StreamCanWriteAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be writable"; + return $"{(_negated ? "not " : "")} be writable"; } } @@ -115,7 +115,7 @@ public class StreamCanSeekAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be seekable"; + return $"{(_negated ? "not " : "")} be seekable"; } } @@ -151,7 +151,7 @@ public class StreamCanTimeoutAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}support timeout"; + return $"{(_negated ? "not " : "")} support timeout"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.DotNet9_0.verified.txt index 0fa0d712ce0..145efb8cc08 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.DotNet9_0.verified.txt @@ -43,7 +43,7 @@ public class StreamCanReadAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be readable"; + return $"{(_negated ? "not " : "")} be readable"; } } @@ -79,7 +79,7 @@ public class StreamCanWriteAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be writable"; + return $"{(_negated ? "not " : "")} be writable"; } } @@ -115,7 +115,7 @@ public class StreamCanSeekAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be seekable"; + return $"{(_negated ? "not " : "")} be seekable"; } } @@ -151,7 +151,7 @@ public class StreamCanTimeoutAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}support timeout"; + return $"{(_negated ? "not " : "")} support timeout"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.Net4_7.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.Net4_7.verified.txt index 0fa0d712ce0..145efb8cc08 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.Net4_7.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/StreamAssertionGeneratorTests.GeneratesStreamAssertions.Net4_7.verified.txt @@ -43,7 +43,7 @@ public class StreamCanReadAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be readable"; + return $"{(_negated ? "not " : "")} be readable"; } } @@ -79,7 +79,7 @@ public class StreamCanWriteAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be writable"; + return $"{(_negated ? "not " : "")} be writable"; } } @@ -115,7 +115,7 @@ public class StreamCanSeekAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be seekable"; + return $"{(_negated ? "not " : "")} be seekable"; } } @@ -151,7 +151,7 @@ public class StreamCanTimeoutAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}support timeout"; + return $"{(_negated ? "not " : "")} support timeout"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet8_0.verified.txt index cb6e1d1307b..c675e696189 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet8_0.verified.txt @@ -39,7 +39,7 @@ public class TaskIsCompletedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be completed"; + return $"{(_negated ? "not " : "")} be completed"; } } @@ -71,7 +71,7 @@ public class TaskIsCanceledAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be canceled"; + return $"{(_negated ? "not " : "")} be canceled"; } } @@ -103,7 +103,7 @@ public class TaskIsFaultedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be faulted"; + return $"{(_negated ? "not " : "")} be faulted"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet9_0.verified.txt index cb6e1d1307b..c675e696189 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.DotNet9_0.verified.txt @@ -39,7 +39,7 @@ public class TaskIsCompletedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be completed"; + return $"{(_negated ? "not " : "")} be completed"; } } @@ -71,7 +71,7 @@ public class TaskIsCanceledAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be canceled"; + return $"{(_negated ? "not " : "")} be canceled"; } } @@ -103,7 +103,7 @@ public class TaskIsFaultedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be faulted"; + return $"{(_negated ? "not " : "")} be faulted"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.Net4_7.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.Net4_7.verified.txt index cb6e1d1307b..c675e696189 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.Net4_7.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/TaskAssertionGeneratorTests.GeneratesTaskAssertions.Net4_7.verified.txt @@ -39,7 +39,7 @@ public class TaskIsCompletedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be completed"; + return $"{(_negated ? "not " : "")} be completed"; } } @@ -71,7 +71,7 @@ public class TaskIsCanceledAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be canceled"; + return $"{(_negated ? "not " : "")} be canceled"; } } @@ -103,7 +103,7 @@ public class TaskIsFaultedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be faulted"; + return $"{(_negated ? "not " : "")} be faulted"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/ThreadAssertionGeneratorTests.GeneratesThreadAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/ThreadAssertionGeneratorTests.GeneratesThreadAssertions.DotNet8_0.verified.txt index c8ff87e3ead..b414d544488 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/ThreadAssertionGeneratorTests.GeneratesThreadAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/ThreadAssertionGeneratorTests.GeneratesThreadAssertions.DotNet8_0.verified.txt @@ -43,7 +43,7 @@ public class ThreadIsAliveAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be alive"; + return $"{(_negated ? "not " : "")} be alive"; } } @@ -79,7 +79,7 @@ public class ThreadIsBackgroundAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a background thread"; + return $"{(_negated ? "not " : "")} be a background thread"; } } @@ -115,7 +115,7 @@ public class ThreadIsThreadPoolThreadAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be alive"; + return $"{(_negated ? "not " : "")} be alive"; } } @@ -79,7 +79,7 @@ public class ThreadIsBackgroundAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a background thread"; + return $"{(_negated ? "not " : "")} be a background thread"; } } @@ -115,7 +115,7 @@ public class ThreadIsThreadPoolThreadAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be alive"; + return $"{(_negated ? "not " : "")} be alive"; } } @@ -79,7 +79,7 @@ public class ThreadIsBackgroundAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a background thread"; + return $"{(_negated ? "not " : "")} be a background thread"; } } @@ -115,7 +115,7 @@ public class ThreadIsThreadPoolThreadAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a class"; + return $"{(_negated ? "not " : "")} be a class"; } } @@ -79,7 +79,7 @@ public class TypeIsInterfaceAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be an interface"; + return $"{(_negated ? "not " : "")} be an interface"; } } @@ -115,7 +115,7 @@ public class TypeIsAbstractAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be abstract"; + return $"{(_negated ? "not " : "")} be abstract"; } } @@ -151,7 +151,7 @@ public class TypeIsSealedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be sealed"; + return $"{(_negated ? "not " : "")} be sealed"; } } @@ -187,7 +187,7 @@ public class TypeIsValueTypeAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a value type"; + return $"{(_negated ? "not " : "")} be a value type"; } } @@ -223,7 +223,7 @@ public class TypeIsEnumAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be an enum"; + return $"{(_negated ? "not " : "")} be an enum"; } } @@ -259,7 +259,7 @@ public class TypeIsPrimitiveAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a primitive type"; + return $"{(_negated ? "not " : "")} be a primitive type"; } } @@ -295,7 +295,7 @@ public class TypeIsPublicAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be public"; + return $"{(_negated ? "not " : "")} be public"; } } @@ -331,7 +331,7 @@ public class TypeIsGenericTypeAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a generic type"; + return $"{(_negated ? "not " : "")} be a generic type"; } } @@ -367,7 +367,7 @@ public class TypeIsGenericTypeDefinitionAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a generic type definition"; + return $"{(_negated ? "not " : "")} be a generic type definition"; } } @@ -403,7 +403,7 @@ public class TypeIsArrayAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be an array"; + return $"{(_negated ? "not " : "")} be an array"; } } @@ -439,7 +439,7 @@ public class TypeIsByRefAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a by-ref type"; + return $"{(_negated ? "not " : "")} be a by-ref type"; } } @@ -475,7 +475,7 @@ public class TypeIsPointerAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a pointer type"; + return $"{(_negated ? "not " : "")} be a pointer type"; } } @@ -511,7 +511,7 @@ public class TypeIsNestedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested type"; + return $"{(_negated ? "not " : "")} be a nested type"; } } @@ -547,7 +547,7 @@ public class TypeIsNestedPublicAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested public type"; + return $"{(_negated ? "not " : "")} be a nested public type"; } } @@ -583,7 +583,7 @@ public class TypeIsNestedPrivateAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested private type"; + return $"{(_negated ? "not " : "")} be a nested private type"; } } @@ -619,7 +619,7 @@ public class TypeIsNestedAssemblyAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested assembly type"; + return $"{(_negated ? "not " : "")} be a nested assembly type"; } } @@ -655,7 +655,7 @@ public class TypeIsNestedFamilyAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested family type"; + return $"{(_negated ? "not " : "")} be a nested family type"; } } @@ -691,7 +691,7 @@ public class TypeIsVisibleAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be visible"; + return $"{(_negated ? "not " : "")} be visible"; } } @@ -727,7 +727,7 @@ public class TypeIsConstructedGenericTypeAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a constructed generic type"; + return $"{(_negated ? "not " : "")} be a constructed generic type"; } } @@ -763,7 +763,7 @@ public class TypeContainsGenericParametersAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}contain generic parameters"; + return $"{(_negated ? "not " : "")} contain generic parameters"; } } @@ -799,7 +799,7 @@ public class TypeIsSerializableAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be serializable"; + return $"{(_negated ? "not " : "")} be serializable"; } } @@ -835,7 +835,7 @@ public class TypeIsCOMObjectAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a COM object"; + return $"{(_negated ? "not " : "")} be a COM object"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.DotNet9_0.verified.txt index aba36d8286e..bc6c2118114 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.DotNet9_0.verified.txt @@ -43,7 +43,7 @@ public class TypeIsClassAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a class"; + return $"{(_negated ? "not " : "")} be a class"; } } @@ -79,7 +79,7 @@ public class TypeIsInterfaceAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be an interface"; + return $"{(_negated ? "not " : "")} be an interface"; } } @@ -115,7 +115,7 @@ public class TypeIsAbstractAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be abstract"; + return $"{(_negated ? "not " : "")} be abstract"; } } @@ -151,7 +151,7 @@ public class TypeIsSealedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be sealed"; + return $"{(_negated ? "not " : "")} be sealed"; } } @@ -187,7 +187,7 @@ public class TypeIsValueTypeAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a value type"; + return $"{(_negated ? "not " : "")} be a value type"; } } @@ -223,7 +223,7 @@ public class TypeIsEnumAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be an enum"; + return $"{(_negated ? "not " : "")} be an enum"; } } @@ -259,7 +259,7 @@ public class TypeIsPrimitiveAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a primitive type"; + return $"{(_negated ? "not " : "")} be a primitive type"; } } @@ -295,7 +295,7 @@ public class TypeIsPublicAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be public"; + return $"{(_negated ? "not " : "")} be public"; } } @@ -331,7 +331,7 @@ public class TypeIsGenericTypeAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a generic type"; + return $"{(_negated ? "not " : "")} be a generic type"; } } @@ -367,7 +367,7 @@ public class TypeIsGenericTypeDefinitionAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a generic type definition"; + return $"{(_negated ? "not " : "")} be a generic type definition"; } } @@ -403,7 +403,7 @@ public class TypeIsArrayAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be an array"; + return $"{(_negated ? "not " : "")} be an array"; } } @@ -439,7 +439,7 @@ public class TypeIsByRefAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a by-ref type"; + return $"{(_negated ? "not " : "")} be a by-ref type"; } } @@ -475,7 +475,7 @@ public class TypeIsPointerAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a pointer type"; + return $"{(_negated ? "not " : "")} be a pointer type"; } } @@ -511,7 +511,7 @@ public class TypeIsNestedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested type"; + return $"{(_negated ? "not " : "")} be a nested type"; } } @@ -547,7 +547,7 @@ public class TypeIsNestedPublicAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested public type"; + return $"{(_negated ? "not " : "")} be a nested public type"; } } @@ -583,7 +583,7 @@ public class TypeIsNestedPrivateAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested private type"; + return $"{(_negated ? "not " : "")} be a nested private type"; } } @@ -619,7 +619,7 @@ public class TypeIsNestedAssemblyAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested assembly type"; + return $"{(_negated ? "not " : "")} be a nested assembly type"; } } @@ -655,7 +655,7 @@ public class TypeIsNestedFamilyAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested family type"; + return $"{(_negated ? "not " : "")} be a nested family type"; } } @@ -691,7 +691,7 @@ public class TypeIsVisibleAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be visible"; + return $"{(_negated ? "not " : "")} be visible"; } } @@ -727,7 +727,7 @@ public class TypeIsConstructedGenericTypeAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a constructed generic type"; + return $"{(_negated ? "not " : "")} be a constructed generic type"; } } @@ -763,7 +763,7 @@ public class TypeContainsGenericParametersAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}contain generic parameters"; + return $"{(_negated ? "not " : "")} contain generic parameters"; } } @@ -799,7 +799,7 @@ public class TypeIsSerializableAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be serializable"; + return $"{(_negated ? "not " : "")} be serializable"; } } @@ -835,7 +835,7 @@ public class TypeIsCOMObjectAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a COM object"; + return $"{(_negated ? "not " : "")} be a COM object"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.Net4_7.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.Net4_7.verified.txt index aba36d8286e..bc6c2118114 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.Net4_7.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/TypeAssertionGeneratorTests.GeneratesTypeAssertions.Net4_7.verified.txt @@ -43,7 +43,7 @@ public class TypeIsClassAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a class"; + return $"{(_negated ? "not " : "")} be a class"; } } @@ -79,7 +79,7 @@ public class TypeIsInterfaceAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be an interface"; + return $"{(_negated ? "not " : "")} be an interface"; } } @@ -115,7 +115,7 @@ public class TypeIsAbstractAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be abstract"; + return $"{(_negated ? "not " : "")} be abstract"; } } @@ -151,7 +151,7 @@ public class TypeIsSealedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be sealed"; + return $"{(_negated ? "not " : "")} be sealed"; } } @@ -187,7 +187,7 @@ public class TypeIsValueTypeAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a value type"; + return $"{(_negated ? "not " : "")} be a value type"; } } @@ -223,7 +223,7 @@ public class TypeIsEnumAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be an enum"; + return $"{(_negated ? "not " : "")} be an enum"; } } @@ -259,7 +259,7 @@ public class TypeIsPrimitiveAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a primitive type"; + return $"{(_negated ? "not " : "")} be a primitive type"; } } @@ -295,7 +295,7 @@ public class TypeIsPublicAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be public"; + return $"{(_negated ? "not " : "")} be public"; } } @@ -331,7 +331,7 @@ public class TypeIsGenericTypeAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a generic type"; + return $"{(_negated ? "not " : "")} be a generic type"; } } @@ -367,7 +367,7 @@ public class TypeIsGenericTypeDefinitionAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a generic type definition"; + return $"{(_negated ? "not " : "")} be a generic type definition"; } } @@ -403,7 +403,7 @@ public class TypeIsArrayAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be an array"; + return $"{(_negated ? "not " : "")} be an array"; } } @@ -439,7 +439,7 @@ public class TypeIsByRefAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a by-ref type"; + return $"{(_negated ? "not " : "")} be a by-ref type"; } } @@ -475,7 +475,7 @@ public class TypeIsPointerAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a pointer type"; + return $"{(_negated ? "not " : "")} be a pointer type"; } } @@ -511,7 +511,7 @@ public class TypeIsNestedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested type"; + return $"{(_negated ? "not " : "")} be a nested type"; } } @@ -547,7 +547,7 @@ public class TypeIsNestedPublicAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested public type"; + return $"{(_negated ? "not " : "")} be a nested public type"; } } @@ -583,7 +583,7 @@ public class TypeIsNestedPrivateAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested private type"; + return $"{(_negated ? "not " : "")} be a nested private type"; } } @@ -619,7 +619,7 @@ public class TypeIsNestedAssemblyAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested assembly type"; + return $"{(_negated ? "not " : "")} be a nested assembly type"; } } @@ -655,7 +655,7 @@ public class TypeIsNestedFamilyAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a nested family type"; + return $"{(_negated ? "not " : "")} be a nested family type"; } } @@ -691,7 +691,7 @@ public class TypeIsVisibleAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be visible"; + return $"{(_negated ? "not " : "")} be visible"; } } @@ -727,7 +727,7 @@ public class TypeIsConstructedGenericTypeAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a constructed generic type"; + return $"{(_negated ? "not " : "")} be a constructed generic type"; } } @@ -763,7 +763,7 @@ public class TypeContainsGenericParametersAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}contain generic parameters"; + return $"{(_negated ? "not " : "")} contain generic parameters"; } } @@ -799,7 +799,7 @@ public class TypeIsSerializableAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be serializable"; + return $"{(_negated ? "not " : "")} be serializable"; } } @@ -835,7 +835,7 @@ public class TypeIsCOMObjectAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a COM object"; + return $"{(_negated ? "not " : "")} be a COM object"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet8_0.verified.txt index d51d59a2add..4dbf3aa8313 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet8_0.verified.txt @@ -43,7 +43,7 @@ public class UriIsAbsoluteUriAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be an absolute URI"; + return $"{(_negated ? "not " : "")} be an absolute URI"; } } @@ -79,7 +79,7 @@ public class UriIsFileAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a file URI"; + return $"{(_negated ? "not " : "")} be a file URI"; } } @@ -115,7 +115,7 @@ public class UriIsUncAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a UNC URI"; + return $"{(_negated ? "not " : "")} be a UNC URI"; } } @@ -151,7 +151,7 @@ public class UriIsLoopbackAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a loopback URI"; + return $"{(_negated ? "not " : "")} be a loopback URI"; } } @@ -187,7 +187,7 @@ public class UriIsDefaultPortAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}use the default port"; + return $"{(_negated ? "not " : "")} use the default port"; } } @@ -223,7 +223,7 @@ public class UriUserEscapedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be user-escaped"; + return $"{(_negated ? "not " : "")} be user-escaped"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet9_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet9_0.verified.txt index d51d59a2add..4dbf3aa8313 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet9_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.DotNet9_0.verified.txt @@ -43,7 +43,7 @@ public class UriIsAbsoluteUriAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be an absolute URI"; + return $"{(_negated ? "not " : "")} be an absolute URI"; } } @@ -79,7 +79,7 @@ public class UriIsFileAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a file URI"; + return $"{(_negated ? "not " : "")} be a file URI"; } } @@ -115,7 +115,7 @@ public class UriIsUncAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a UNC URI"; + return $"{(_negated ? "not " : "")} be a UNC URI"; } } @@ -151,7 +151,7 @@ public class UriIsLoopbackAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a loopback URI"; + return $"{(_negated ? "not " : "")} be a loopback URI"; } } @@ -187,7 +187,7 @@ public class UriIsDefaultPortAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}use the default port"; + return $"{(_negated ? "not " : "")} use the default port"; } } @@ -223,7 +223,7 @@ public class UriUserEscapedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be user-escaped"; + return $"{(_negated ? "not " : "")} be user-escaped"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.Net4_7.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.Net4_7.verified.txt index d51d59a2add..4dbf3aa8313 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.Net4_7.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/UriAssertionGeneratorTests.GeneratesUriAssertions.Net4_7.verified.txt @@ -43,7 +43,7 @@ public class UriIsAbsoluteUriAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be an absolute URI"; + return $"{(_negated ? "not " : "")} be an absolute URI"; } } @@ -79,7 +79,7 @@ public class UriIsFileAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a file URI"; + return $"{(_negated ? "not " : "")} be a file URI"; } } @@ -115,7 +115,7 @@ public class UriIsUncAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a UNC URI"; + return $"{(_negated ? "not " : "")} be a UNC URI"; } } @@ -151,7 +151,7 @@ public class UriIsLoopbackAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be a loopback URI"; + return $"{(_negated ? "not " : "")} be a loopback URI"; } } @@ -187,7 +187,7 @@ public class UriIsDefaultPortAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}use the default port"; + return $"{(_negated ? "not " : "")} use the default port"; } } @@ -223,7 +223,7 @@ public class UriUserEscapedAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be user-escaped"; + return $"{(_negated ? "not " : "")} be user-escaped"; } } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/WeakReferenceAssertionGeneratorTests.GeneratesWeakReferenceAssertions.DotNet8_0.verified.txt b/tests/TUnit.Assertions.SourceGenerator.Tests/WeakReferenceAssertionGeneratorTests.GeneratesWeakReferenceAssertions.DotNet8_0.verified.txt index e61820f053f..073d551b3c2 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/WeakReferenceAssertionGeneratorTests.GeneratesWeakReferenceAssertions.DotNet8_0.verified.txt +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/WeakReferenceAssertionGeneratorTests.GeneratesWeakReferenceAssertions.DotNet8_0.verified.txt @@ -43,7 +43,7 @@ public class WeakReferenceIsAliveAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be alive"; + return $"{(_negated ? "not " : "")} be alive"; } } @@ -79,7 +79,7 @@ public class WeakReferenceTrackResurrectionAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be alive"; + return $"{(_negated ? "not " : "")} be alive"; } } @@ -79,7 +79,7 @@ public class WeakReferenceTrackResurrectionAssertion : Assertion protected override string GetExpectation() { - return $"to {(_negated ? "not " : "")}be alive"; + return $"{(_negated ? "not " : "")} be alive"; } } @@ -79,7 +79,7 @@ public class WeakReferenceTrackResurrectionAssertion : Assertion Date: Mon, 19 Oct 2026 14:42:18 +0000 Subject: [PATCH 44/49] test: resolve DotNetCli lazily without blocking on async reads Resolve the dotnet path on first use instead of in the type initializer, discard the child's output with event-based reads instead of blocking on ReadToEndAsync().Result, and kill the whole process tree on timeout. A test pins that the resolved path runs an SDK for this repository. --- tests/SharedTestHelpers/DotNetCli.cs | 18 ++++++++++------ tests/TUnit.Engine.Tests/DotNetCliTests.cs | 25 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 tests/TUnit.Engine.Tests/DotNetCliTests.cs diff --git a/tests/SharedTestHelpers/DotNetCli.cs b/tests/SharedTestHelpers/DotNetCli.cs index 65d19aee88c..c1b63e0878f 100644 --- a/tests/SharedTestHelpers/DotNetCli.cs +++ b/tests/SharedTestHelpers/DotNetCli.cs @@ -18,8 +18,12 @@ namespace TUnit.Tests.Shared; /// internal static class DotNetCli { + // Resolved on first use rather than in the type initializer, so an unexpected failure surfaces as itself + // instead of a TypeInitializationException that also breaks every later use of the type + private static readonly Lazy ResolvedPath = new(Resolve); + /// The dotnet executable to start child processes with: a full path, or dotnet to use PATH. - public static string Path { get; } = Resolve(); + public static string Path => ResolvedPath.Value; /// /// Prefers DOTNET_HOST_PATH, then the muxer of the installation running this process when it resolves an @@ -65,18 +69,18 @@ private static bool ResolvesSdk(string muxer) return false; } - // Drain both streams so neither can fill up and block the process - var output = process.StandardOutput.ReadToEndAsync(); - var error = process.StandardError.ReadToEndAsync(); + // Drain and discard both streams so neither can fill up and block the process + process.OutputDataReceived += static (_, _) => { }; + process.ErrorDataReceived += static (_, _) => { }; + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); if (!process.WaitForExit(30_000)) { - process.Kill(); + process.Kill(entireProcessTree: true); return false; } - _ = output.Result; - _ = error.Result; return process.ExitCode == 0; } catch (Exception exception) when (exception is InvalidOperationException or System.ComponentModel.Win32Exception or IOException) diff --git a/tests/TUnit.Engine.Tests/DotNetCliTests.cs b/tests/TUnit.Engine.Tests/DotNetCliTests.cs new file mode 100644 index 00000000000..b06c68b514e --- /dev/null +++ b/tests/TUnit.Engine.Tests/DotNetCliTests.cs @@ -0,0 +1,25 @@ +using CliWrap; +using CliWrap.Buffered; +using Shouldly; +using TUnit.Tests.Shared; + +namespace TUnit.Engine.Tests; + +/// +/// Checks that , which every test here starts child builds with, resolves an SDK for this +/// repository. +/// +public class DotNetCliTests +{ + [Test] + public async Task Path_Resolves_An_Sdk_For_This_Repository() + { + var result = await Cli.Wrap(DotNetCli.Path) + .WithArguments("--version") + .WithWorkingDirectory(AppContext.BaseDirectory) + .WithValidation(CommandResultValidation.None) + .ExecuteBufferedAsync(); + + result.ExitCode.ShouldBe(0, $"'{DotNetCli.Path} --version' failed:{Environment.NewLine}{result.StandardOutput}{result.StandardError}"); + } +} From abdd0362b62bda7f0df22ed85d5e3eef1f2dd542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Mon, 19 Oct 2026 14:42:18 +0000 Subject: [PATCH 45/49] ci: keep the test framework lists in TestBaseModule Modules that ran the modern frameworks, plus net472 on Windows, each spelled the list out; they now use ModernFrameworks or ModernAndNetFrameworks from the base module. --- .../Modules/Abstract/TestBaseModule.cs | 23 ++++++++++--------- ...ertionsShouldSourceGeneratorTestsModule.cs | 10 +------- ...RunAssertionsSourceGeneratorTestsModule.cs | 15 +----------- .../Modules/RunPublicAPITestsModule.cs | 15 +----------- .../Modules/RunSourceGeneratorTestsModule.cs | 15 +----------- 5 files changed, 16 insertions(+), 62 deletions(-) diff --git a/tools/TUnit.Pipeline/Modules/Abstract/TestBaseModule.cs b/tools/TUnit.Pipeline/Modules/Abstract/TestBaseModule.cs index 7e56d007556..95353b87e1c 100644 --- a/tools/TUnit.Pipeline/Modules/Abstract/TestBaseModule.cs +++ b/tools/TUnit.Pipeline/Modules/Abstract/TestBaseModule.cs @@ -11,20 +11,21 @@ namespace TUnit.Pipeline.Modules.Abstract; public abstract class TestBaseModule : Module> { + /// Every modern TFM the repo targets. + protected static IReadOnlyList ModernFrameworks { get; } = ["net10.0", "net9.0", "net8.0"]; + + /// + /// , plus net472 on Windows, the only OS that can run .NET Framework tests. + /// + protected static IReadOnlyList ModernAndNetFrameworks { get; } = + IsWindows ? [.. ModernFrameworks, "net472"] : ModernFrameworks; + /// - /// Frameworks to run. Defaults to every modern TFM the repo targets; frameworks the project did not + /// Frameworks to run. Defaults to ; frameworks the project did not /// build are skipped (see ), so net10.0-only projects run once. - /// Override only to add a legacy TFM (net472 on Windows) or to deliberately narrow the set. + /// Override with to add net472 on Windows, or to deliberately narrow the set. /// - protected virtual IEnumerable TestableFrameworks - { - get - { - yield return "net10.0"; - yield return "net9.0"; - yield return "net8.0"; - } - } + protected virtual IEnumerable TestableFrameworks => ModernFrameworks; /// True on Windows, where legacy .NET Framework TFMs (net4xx) can be tested. protected static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); diff --git a/tools/TUnit.Pipeline/Modules/RunAssertionsShouldSourceGeneratorTestsModule.cs b/tools/TUnit.Pipeline/Modules/RunAssertionsShouldSourceGeneratorTestsModule.cs index 4c9b785947f..1c133054a0c 100644 --- a/tools/TUnit.Pipeline/Modules/RunAssertionsShouldSourceGeneratorTestsModule.cs +++ b/tools/TUnit.Pipeline/Modules/RunAssertionsShouldSourceGeneratorTestsModule.cs @@ -15,15 +15,7 @@ public class RunAssertionsShouldSourceGeneratorTestsModule : TestBaseModule // in TUnit.Assertions.Should.SourceGenerator.Tests.csproj. net472 is // intentionally omitted there: the generator output is consumer-TFM-independent and the // .NET Framework BCL trips the in-memory compilation harness for user-declared generics. - protected override IEnumerable TestableFrameworks - { - get - { - yield return "net10.0"; - yield return "net9.0"; - yield return "net8.0"; - } - } + protected override IEnumerable TestableFrameworks => ModernFrameworks; protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken) { diff --git a/tools/TUnit.Pipeline/Modules/RunAssertionsSourceGeneratorTestsModule.cs b/tools/TUnit.Pipeline/Modules/RunAssertionsSourceGeneratorTestsModule.cs index 55460f337c4..a32ce1a0c42 100644 --- a/tools/TUnit.Pipeline/Modules/RunAssertionsSourceGeneratorTestsModule.cs +++ b/tools/TUnit.Pipeline/Modules/RunAssertionsSourceGeneratorTestsModule.cs @@ -13,20 +13,7 @@ public class RunAssertionsSourceGeneratorTestsModule : TestBaseModule { // Generator output snapshots verify behaviour across consumer TFMs — keep aligned with // in TUnit.Assertions.SourceGenerator.Tests.csproj. - protected override IEnumerable TestableFrameworks - { - get - { - yield return "net10.0"; - yield return "net9.0"; - yield return "net8.0"; - - if (IsWindows) - { - yield return "net472"; - } - } - } + protected override IEnumerable TestableFrameworks => ModernAndNetFrameworks; protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken) { diff --git a/tools/TUnit.Pipeline/Modules/RunPublicAPITestsModule.cs b/tools/TUnit.Pipeline/Modules/RunPublicAPITestsModule.cs index 6b33c7a6afd..be38fad3083 100644 --- a/tools/TUnit.Pipeline/Modules/RunPublicAPITestsModule.cs +++ b/tools/TUnit.Pipeline/Modules/RunPublicAPITestsModule.cs @@ -13,20 +13,7 @@ public class RunPublicAPITestsModule : TestBaseModule { // Public API snapshots cover every supported consumer TFM — keep aligned with // in TUnit.PublicAPI.csproj. - protected override IEnumerable TestableFrameworks - { - get - { - yield return "net10.0"; - yield return "net9.0"; - yield return "net8.0"; - - if (IsWindows) - { - yield return "net472"; - } - } - } + protected override IEnumerable TestableFrameworks => ModernAndNetFrameworks; protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken) { diff --git a/tools/TUnit.Pipeline/Modules/RunSourceGeneratorTestsModule.cs b/tools/TUnit.Pipeline/Modules/RunSourceGeneratorTestsModule.cs index a69661249ad..365d2ac2171 100644 --- a/tools/TUnit.Pipeline/Modules/RunSourceGeneratorTestsModule.cs +++ b/tools/TUnit.Pipeline/Modules/RunSourceGeneratorTestsModule.cs @@ -13,20 +13,7 @@ public class RunSourceGeneratorTestsModule : TestBaseModule { // Generator output snapshots verify behaviour across consumer TFMs — keep aligned with // in TUnit.Core.SourceGenerator.Tests.csproj. - protected override IEnumerable TestableFrameworks - { - get - { - yield return "net10.0"; - yield return "net9.0"; - yield return "net8.0"; - - if (IsWindows) - { - yield return "net472"; - } - } - } + protected override IEnumerable TestableFrameworks => ModernAndNetFrameworks; protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken) { From 46dc9513f5b15e27630f8d4eed6124e0d2326564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Mon, 19 Oct 2026 15:07:00 +0000 Subject: [PATCH 46/49] test: build generator test references through one shared helper TestHostReferences.CreateReferences turns the test host's assemblies, optionally filtered, plus any extra assemblies into metadata references. The Core, Assertions, Assertions.Should and Mocks generator tests used to each repeat that. --- tests/SharedTestHelpers/TestHostReferences.cs | 19 +++++++++++ .../ShouldExtensionGeneratorTests.cs | 14 +++----- .../ReferencesHelper.cs | 17 ++++------ .../TestHostReferencesTests.cs | 32 +++++++++++++++++++ .../ReferencesHelper.cs | 13 +++----- .../SnapshotTestBase.cs | 11 ++----- 6 files changed, 68 insertions(+), 38 deletions(-) create mode 100644 tests/TUnit.Assertions.SourceGenerator.Tests/TestHostReferencesTests.cs diff --git a/tests/SharedTestHelpers/TestHostReferences.cs b/tests/SharedTestHelpers/TestHostReferences.cs index f766b372c54..c2d5e164c3c 100644 --- a/tests/SharedTestHelpers/TestHostReferences.cs +++ b/tests/SharedTestHelpers/TestHostReferences.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; +using Microsoft.CodeAnalysis; namespace TUnit.Tests.Shared; @@ -24,6 +26,23 @@ internal static class TestHostReferences /// The assembly paths to reference, sorted by file name and then by path. public static IReadOnlyList AssemblyPaths { get; } = Resolve(); + /// + /// Metadata references to , followed by . + /// + public static List CreateReferences(params Assembly[] additionalAssemblies) => + CreateReferences(static _ => true, additionalAssemblies); + + /// + /// Metadata references to the that accepts, + /// followed by . + /// + public static List CreateReferences(Func includeHostAssembly, params Assembly[] additionalAssemblies) => + AssemblyPaths + .Where(includeHostAssembly) + .Concat(additionalAssemblies.Select(assembly => assembly.Location)) + .Select(path => MetadataReference.CreateFromFile(path)) + .ToList(); + /// /// Reads TRUSTED_PLATFORM_ASSEMBLIES on .NET, or the loaded assemblies on .NET Framework, and sorts them. /// diff --git a/tests/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.cs b/tests/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.cs index 2477f45b35a..4eb2cebf0ed 100644 --- a/tests/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.cs +++ b/tests/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.cs @@ -472,16 +472,10 @@ await Verify(combined) return combined; } - private static IEnumerable GetReferences() - { - foreach (var path in TestHostReferences.AssemblyPaths) - { - yield return MetadataReference.CreateFromFile(path); - } - - yield return MetadataReference.CreateFromFile(typeof(Assertion<>).Assembly.Location); - yield return MetadataReference.CreateFromFile(typeof(TUnit.Assertions.Should.Core.IShouldSource).Assembly.Location); - } + private static IEnumerable GetReferences() => + TestHostReferences.CreateReferences( + typeof(Assertion<>).Assembly, + typeof(TUnit.Assertions.Should.Core.IShouldSource).Assembly); private static int CountOccurrences(string value, string search) { diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/ReferencesHelper.cs b/tests/TUnit.Assertions.SourceGenerator.Tests/ReferencesHelper.cs index 55779c08397..f7b0c084611 100644 --- a/tests/TUnit.Assertions.SourceGenerator.Tests/ReferencesHelper.cs +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/ReferencesHelper.cs @@ -8,15 +8,10 @@ namespace TUnit.Assertions.SourceGenerator.Tests; internal class ReferencesHelper { public static readonly List References = - TestHostReferences.AssemblyPaths - .Select(path => MetadataReference.CreateFromFile(path)) - .Concat([ - // add your app/lib specifics, e.g.: - MetadataReference.CreateFromFile(typeof(Attribute).Assembly.Location), - MetadataReference.CreateFromFile(typeof(Assert).Assembly.Location), - MetadataReference.CreateFromFile(typeof(Polyfill).Assembly.Location), - MetadataReference.CreateFromFile(typeof(System.Runtime.CompilerServices.CallerArgumentExpressionAttribute).Assembly.Location), - MetadataReference.CreateFromFile(typeof(Uri).Assembly.Location), - ]) - .ToList(); + TestHostReferences.CreateReferences( + typeof(Attribute).Assembly, + typeof(Assert).Assembly, + typeof(Polyfill).Assembly, + typeof(System.Runtime.CompilerServices.CallerArgumentExpressionAttribute).Assembly, + typeof(Uri).Assembly); } diff --git a/tests/TUnit.Assertions.SourceGenerator.Tests/TestHostReferencesTests.cs b/tests/TUnit.Assertions.SourceGenerator.Tests/TestHostReferencesTests.cs new file mode 100644 index 00000000000..4cea025a89d --- /dev/null +++ b/tests/TUnit.Assertions.SourceGenerator.Tests/TestHostReferencesTests.cs @@ -0,0 +1,32 @@ +using TUnit.Tests.Shared; + +namespace TUnit.Assertions.SourceGenerator.Tests; + +/// +/// Pins how builds the metadata +/// references every generator test compilation uses. +/// +internal class TestHostReferencesTests +{ + [Test] + public async Task CreateReferences_Lists_The_Host_Assemblies_Then_The_Additional_Ones() + { + var additional = typeof(TestHostReferencesTests).Assembly; + + var paths = TestHostReferences.CreateReferences(additional).Select(reference => reference.FilePath).ToArray(); + + await Assert.That(paths.Take(TestHostReferences.AssemblyPaths.Count)).IsEquivalentTo(TestHostReferences.AssemblyPaths, TUnit.Assertions.Enums.CollectionOrdering.Matching); + await Assert.That(paths.Skip(TestHostReferences.AssemblyPaths.Count)).IsEquivalentTo(new[] { additional.Location }, TUnit.Assertions.Enums.CollectionOrdering.Matching); + } + + [Test] + public async Task CreateReferences_Leaves_Out_The_Host_Assemblies_The_Filter_Excludes() + { + var excluded = TestHostReferences.AssemblyPaths[0]; + + var paths = TestHostReferences.CreateReferences(path => path != excluded).Select(reference => reference.FilePath).ToArray(); + + await Assert.That(paths).DoesNotContain(excluded); + await Assert.That(paths.Length).IsEqualTo(TestHostReferences.AssemblyPaths.Count - 1); + } +} diff --git a/tests/TUnit.Core.SourceGenerator.Tests/ReferencesHelper.cs b/tests/TUnit.Core.SourceGenerator.Tests/ReferencesHelper.cs index 3823028505c..ea37a8e8e01 100644 --- a/tests/TUnit.Core.SourceGenerator.Tests/ReferencesHelper.cs +++ b/tests/TUnit.Core.SourceGenerator.Tests/ReferencesHelper.cs @@ -8,13 +8,8 @@ namespace TUnit.Core.SourceGenerator.Tests; internal class ReferencesHelper { public static readonly List References = - TestHostReferences.AssemblyPaths - .Select(path => MetadataReference.CreateFromFile(path)) - .Concat([ - // add your app/lib specifics, e.g.: - MetadataReference.CreateFromFile(typeof(Attribute).Assembly.Location), - MetadataReference.CreateFromFile(typeof(Assert).Assembly.Location), - MetadataReference.CreateFromFile(typeof(Polyfill).Assembly.Location), - ]) - .ToList(); + TestHostReferences.CreateReferences( + typeof(Attribute).Assembly, + typeof(Assert).Assembly, + typeof(Polyfill).Assembly); } diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/SnapshotTestBase.cs b/tests/TUnit.Mocks.SourceGenerator.Tests/SnapshotTestBase.cs index e66f0cea7c1..2261096039d 100644 --- a/tests/TUnit.Mocks.SourceGenerator.Tests/SnapshotTestBase.cs +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/SnapshotTestBase.cs @@ -40,14 +40,9 @@ private static List LoadReferences() } var mocksFileName = Path.GetFileName(mocksAssembly.Location); - var refs = TestHostReferences.AssemblyPaths - .Where(path => !string.Equals(Path.GetFileName(path), mocksFileName, StringComparison.OrdinalIgnoreCase)) - .Select(path => MetadataReference.CreateFromFile(path)) - .ToList(); - - refs.Add(MetadataReference.CreateFromFile(mocksAssembly.Location)); - - return refs; + return TestHostReferences.CreateReferences( + path => !string.Equals(Path.GetFileName(path), mocksFileName, StringComparison.OrdinalIgnoreCase), + mocksAssembly); } /// From fd3143e9dffd8c53769208616f7c6bae6e079ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Mon, 19 Oct 2026 15:15:31 +0000 Subject: [PATCH 47/49] test: explain the reference sort and scrub wrapped forward-slash Windows paths Note why TestHostReferences sorts ordinally, and pin that the PublicAPI source path scrubber also handles a wrapped C:/ path. --- tests/SharedTestHelpers/TestHostReferences.cs | 2 ++ tests/TUnit.PublicAPI/SourcePathScrubberTests.cs | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/tests/SharedTestHelpers/TestHostReferences.cs b/tests/SharedTestHelpers/TestHostReferences.cs index c2d5e164c3c..186a8d30bf0 100644 --- a/tests/SharedTestHelpers/TestHostReferences.cs +++ b/tests/SharedTestHelpers/TestHostReferences.cs @@ -54,6 +54,8 @@ private static string[] Resolve() .Where(assembly => !assembly.IsDynamic && !string.IsNullOrWhiteSpace(assembly.Location)) .Select(assembly => assembly.Location); + // Ordinal on purpose: reference order shows up in generator output, so a culture-aware sort, or no sort, would + // reorder snapshots between hosts. A snapshot that reorders after a change here is this sort, not the generator. return paths .OrderBy(Path.GetFileName, StringComparer.Ordinal) .ThenBy(path => path, StringComparer.Ordinal) diff --git a/tests/TUnit.PublicAPI/SourcePathScrubberTests.cs b/tests/TUnit.PublicAPI/SourcePathScrubberTests.cs index 2f72fdd3c01..3dfef76be08 100644 --- a/tests/TUnit.PublicAPI/SourcePathScrubberTests.cs +++ b/tests/TUnit.PublicAPI/SourcePathScrubberTests.cs @@ -37,6 +37,17 @@ public async Task Scrubs_Wrapped_Windows_Caller_File_Path() await Assert.That(scrubbed).IsEqualTo("[.After(., \"PATH_SCRUBBED\", 61)]"); } + [Test] + public async Task Scrubs_Wrapped_Windows_Caller_File_Path_With_Forward_Slashes() + { + var text = "[.After(., \"C:/Users/dev/source/repos/TUnit/TUnit.OpenTelemetry/\" +\r\n" + + " \"AutoStart/TUnitOpenTelemetryAutoStart.cs\", 61)]"; + + var scrubbed = Tests.ScrubSourcePaths(text); + + await Assert.That(scrubbed).IsEqualTo("[.After(., \"PATH_SCRUBBED\", 61)]"); + } + [Test] [Arguments("public const string Prefix = \"/v1\";")] [Arguments("[Route(\"/v1\", 2)]")] From 4b4934292cdfcfd6afde576b2730cd2703e90edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Mon, 19 Oct 2026 18:46:29 +0000 Subject: [PATCH 48/49] test: only scrub source paths passed to hook attributes A rooted .cs literal followed by a number in any other attribute, such as [Route("/api/File.cs", 2)], was scrubbed too. The pattern now also requires the literal to be an argument of a Before/After(Every) hook. --- tests/TUnit.PublicAPI/SourcePathScrubberTests.cs | 2 ++ tests/TUnit.PublicAPI/Tests.cs | 11 ++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/TUnit.PublicAPI/SourcePathScrubberTests.cs b/tests/TUnit.PublicAPI/SourcePathScrubberTests.cs index 3dfef76be08..6077c58e3bf 100644 --- a/tests/TUnit.PublicAPI/SourcePathScrubberTests.cs +++ b/tests/TUnit.PublicAPI/SourcePathScrubberTests.cs @@ -54,6 +54,8 @@ public async Task Scrubs_Wrapped_Windows_Caller_File_Path_With_Forward_Slashes() [Arguments("public const string Template = \"/api/File.cs\";")] [Arguments("[Description(\"C:\\\\docs\\\\readme.md\")]")] [Arguments("[Description(\"File.cs\", 39)]")] + [Arguments("[Route(\"/api/File.cs\", 2)]")] + [Arguments("[Route(\"C:/api/File.cs\", 2)]")] public async Task Leaves_Unrelated_Strings_Alone(string line) { await Assert.That(Tests.ScrubSourcePaths(line)).IsEqualTo(line); diff --git a/tests/TUnit.PublicAPI/Tests.cs b/tests/TUnit.PublicAPI/Tests.cs index 0d87dfd99fc..9e847eb10f1 100644 --- a/tests/TUnit.PublicAPI/Tests.cs +++ b/tests/TUnit.PublicAPI/Tests.cs @@ -71,8 +71,9 @@ await VerifyTUnit.Verify(publicApi) /// /// Scrubs source path literals (hook attributes' [CallerFilePath] argument) before the URL regex mangles /// them. CI builds embed deterministic "/_/..." paths; local builds embed the full checkout path, which the - /// generator wraps into "..." + "..." pieces once it exceeds 80 characters. Only a rooted ".cs" path followed by - /// the [CallerLineNumber] argument matches, so public string values such as "/v1" are left alone. + /// generator wraps into "..." + "..." pieces once it exceeds 80 characters. Only a rooted ".cs" path passed to a + /// hook attribute and followed by the [CallerLineNumber] argument matches, so public string values such as + /// "/v1" and other attributes' arguments are left alone. /// internal static string ScrubSourcePaths(string text) => SourcePathRegex().Replace(text, @"""PATH_SCRUBBED"""); @@ -98,11 +99,11 @@ private string Scrub(string text) /// - /// A rooted ".cs" string literal, possibly wrapped into "..." + "..." pieces, followed by the caller line number - /// argument. See . + /// The first string argument of a Before/After(Every) hook attribute that is a rooted ".cs" literal, possibly + /// wrapped into "..." + "..." pieces, followed by the caller line number argument. See . /// private const string SourcePathPattern = - @"""(?:/|[A-Za-z]:(?:\\\\|/))[^""\r\n]*(?:""[ \t]*\+[ \t]*\r?\n[ \t]*""[^""\r\n]*)*\.cs""(?=,[ \t]*\d+[,)])"; + @"(?<=\b(?:Before|After)(?:Every)?\([^""\r\n]*)""(?:/|[A-Za-z]:(?:\\\\|/))[^""\r\n]*(?:""[ \t]*\+[ \t]*\r?\n[ \t]*""[^""\r\n]*)*\.cs""(?=,[ \t]*\d+[,)])"; #if NET [GeneratedRegex(SourcePathPattern)] From 8e4ecb190fd6ae0f8239f5fb55de5f0e002a6978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sundstr=C3=B6m?= Date: Mon, 19 Oct 2026 21:05:38 +0000 Subject: [PATCH 49/49] test: build the Should generator test references once Every test compiled against a freshly created set of file-backed references. Keep them in a static list, as the other generator test projects already do. --- .../ShouldExtensionGeneratorTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.cs b/tests/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.cs index 4eb2cebf0ed..8d5f666e10f 100644 --- a/tests/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.cs +++ b/tests/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.cs @@ -449,7 +449,7 @@ public sealed class OverloadResolutionPriorityAttribute : System.Attribute var compilation = CSharpCompilation.Create( assemblyName: "GeneratorTest", syntaxTrees: inputTrees, - references: GetReferences(), + references: References, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); var driver = CSharpGeneratorDriver.Create(new ShouldExtensionGenerator()); @@ -472,7 +472,8 @@ await Verify(combined) return combined; } - private static IEnumerable GetReferences() => + // Built once: every test compiles against the same references, and each one reads its file from disk + private static readonly List References = TestHostReferences.CreateReferences( typeof(Assertion<>).Assembly, typeof(TUnit.Assertions.Should.Core.IShouldSource).Assembly);