diff --git a/README.md b/README.md index 70411c07bc..938320af94 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ See the [documentation](https://tunit.dev/docs/getting-started/attributes) for m | `TUnit.Core` | Shared test library components without an execution engine | | `TUnit.Engine` | Execution engine for test projects | | `TUnit.Assertions` | Standalone assertions — works with other test frameworks too | +| `TUnit.Assertions.Should` | Optional FluentAssertions-style `value.Should().BeEqualTo(...)` syntax over `TUnit.Assertions` (beta) | | `TUnit.Playwright` | Playwright integration with automatic browser lifecycle management | ## Migrating from xUnit, NUnit, or MSTest? diff --git a/SharedTestHelpers/AnalyzerTestCompatibility.cs b/SharedTestHelpers/AnalyzerTestCompatibility.cs new file mode 100644 index 0000000000..57470524d0 --- /dev/null +++ b/SharedTestHelpers/AnalyzerTestCompatibility.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +using System.Reflection; + +namespace TUnit.Tests.Shared; + +/// +/// Shared resolution helper for the analyzer-test framework's reference-assembly mismatch. +/// +/// +/// +/// The Microsoft.CodeAnalysis.Testing 1.1.2 framework targets net9.0 reference assemblies. Loading +/// TUnit's net10.0 builds via typeof(...).Assembly.Location raises CS1705 (referenced +/// assembly has a higher System.Runtime version), which the verifier suppresses via +/// CompilerDiagnostics = None. The suppression silently breaks symbol resolution for every +/// extension method (IsEqualTo, Throws, etc.) in the test compilation — analyzers find nothing +/// to flag and tests report "expected 1 diagnostic, actual 0" with no compiler errors visible. +/// +/// +/// The fix is to load the netstandard2.0 builds (compatible with both Net90 ref assemblies and the +/// test runtime). Each analyzer test csproj copies the relevant netstandard2.0 build into the test +/// bin via a <None Include="..." Link="X.netstandard2.0.dll" CopyToOutputDirectory="..."> +/// item; this helper resolves that copy and fails fast if it is missing. Falling back to the +/// runtime assembly path would silently reintroduce the CS1705/symbol-resolution failure this +/// helper exists to avoid. +/// +/// +/// Linked into multiple test projects via <Compile Include="..\SharedTestHelpers\...">; +/// see TUnit.Assertions.Should.SourceGenerator for the same pattern with +/// CovarianceHelper/EquatableArray. +/// +/// +internal static class AnalyzerTestCompatibility +{ + public static string GetCompatibleDllPath(string assemblyName, Assembly fallback) + { + var ns20Path = Path.Combine(AppContext.BaseDirectory, $"{assemblyName}.netstandard2.0.dll"); + if (!File.Exists(ns20Path)) + { + throw new FileNotFoundException( + $"netstandard2.0 build of {assemblyName} not found at '{ns20Path}'. " + + "Run 'dotnet build' before running analyzer tests.", + ns20Path); + } + + return ns20Path; + } + + public static string GetSystemTextJson9DllPath() + { + const string fileName = "System.Text.Json.9.0.dll"; + var path = Path.Combine(AppContext.BaseDirectory, fileName); + if (!File.Exists(path)) + { + throw new FileNotFoundException( + $"System.Text.Json 9.0 reference not found at '{path}'. " + + "Run 'dotnet build' before running analyzer tests.", + path); + } + + return path; + } +} diff --git a/TUnit.Analyzers.Tests/AnalyzerTestHelpers.cs b/TUnit.Analyzers.Tests/AnalyzerTestHelpers.cs index cfc1a476b4..2c7c8faf8f 100644 --- a/TUnit.Analyzers.Tests/AnalyzerTestHelpers.cs +++ b/TUnit.Analyzers.Tests/AnalyzerTestHelpers.cs @@ -34,9 +34,12 @@ public static CSharpAnalyzerTest CreateAnalyzerTest< csTest.TestState.AdditionalReferences .AddRange( [ - MetadataReference.CreateFromFile(typeof(TUnitAttribute).Assembly.Location), + MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly)), +#if NET8_0 + MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetSystemTextJson9DllPath()), +#endif MetadataReference.CreateFromFile(typeof(CircuitState).Assembly.Location), - MetadataReference.CreateFromFile(typeof(ProjectReferenceEnum).Assembly.Location) + MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.TestProject.Library", typeof(ProjectReferenceEnum).Assembly)) ] ); @@ -145,9 +148,12 @@ public static CSharpSuppressorTest CreateSuppresso test.TestState.AdditionalReferences .AddRange([ - MetadataReference.CreateFromFile(typeof(TUnitAttribute).Assembly.Location), + MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly)), +#if NET8_0 + MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetSystemTextJson9DllPath()), +#endif MetadataReference.CreateFromFile(typeof(CircuitState).Assembly.Location), - MetadataReference.CreateFromFile(typeof(ProjectReferenceEnum).Assembly.Location) + MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.TestProject.Library", typeof(ProjectReferenceEnum).Assembly)) ]); return test; diff --git a/TUnit.Analyzers.Tests/TUnit.Analyzers.Tests.csproj b/TUnit.Analyzers.Tests/TUnit.Analyzers.Tests.csproj index 0f0db63bd6..bcabd116ef 100644 --- a/TUnit.Analyzers.Tests/TUnit.Analyzers.Tests.csproj +++ b/TUnit.Analyzers.Tests/TUnit.Analyzers.Tests.csproj @@ -12,6 +12,7 @@ + @@ -22,6 +23,32 @@ + + + + + + + + + diff --git a/TUnit.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs b/TUnit.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs index eca856d259..4dbe1b840a 100644 --- a/TUnit.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs +++ b/TUnit.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs @@ -32,17 +32,21 @@ public static Task VerifyAnalyzerAsync([StringSyntax("c#")] string source, param /// public static async Task VerifyAnalyzerAsync([StringSyntax("c#")] string source, Action configureTest, params DiagnosticResult[] expected) { + // Microsoft.Bcl.AsyncInterfaces is required by TUnit.Core.netstandard2.0.dll's typeforward + // of IAsyncEnumerable; without it, tests that use IAsyncEnumerable in their source see + // CS0012/CS0508 alongside the expected analyzer diagnostic. var test = new Test { TestCode = source, - ReferenceAssemblies = ReferenceAssemblies.Net.Net90, + ReferenceAssemblies = ReferenceAssemblies.Net.Net90 + .AddPackages([new PackageIdentity("Microsoft.Bcl.AsyncInterfaces", "9.0.0")]), TestState = { AdditionalReferences = { - typeof(TUnitAttribute).Assembly.Location, + TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly), typeof(CircuitState).Assembly.Location, - typeof(ProjectReferenceEnum).Assembly.Location, + TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.TestProject.Library", typeof(ProjectReferenceEnum).Assembly), }, }, }; diff --git a/TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs b/TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs index a15d99f96b..a368c17244 100644 --- a/TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs +++ b/TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs @@ -47,7 +47,7 @@ params DiagnosticResult[] expected { AdditionalReferences = { - typeof(TUnitAttribute).Assembly.Location, + TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly), }, } }; @@ -97,7 +97,7 @@ Action configureTest { AdditionalReferences = { - typeof(TUnitAttribute).Assembly.Location, + TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly), }, }, CodeActionValidationMode = CodeActionValidationMode.SemanticStructure, diff --git a/TUnit.AspNetCore.Analyzers.Tests/TUnit.AspNetCore.Analyzers.Tests.csproj b/TUnit.AspNetCore.Analyzers.Tests/TUnit.AspNetCore.Analyzers.Tests.csproj index f08a2d3984..30e3034b02 100644 --- a/TUnit.AspNetCore.Analyzers.Tests/TUnit.AspNetCore.Analyzers.Tests.csproj +++ b/TUnit.AspNetCore.Analyzers.Tests/TUnit.AspNetCore.Analyzers.Tests.csproj @@ -20,5 +20,16 @@ + + + + + + diff --git a/TUnit.AspNetCore.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs b/TUnit.AspNetCore.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs index 08543e0cd5..6c277f1fc8 100644 --- a/TUnit.AspNetCore.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs +++ b/TUnit.AspNetCore.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs @@ -33,12 +33,13 @@ public static async Task VerifyAnalyzerAsync([StringSyntax("c#")] string source, var test = new Test { TestCode = source, - ReferenceAssemblies = ReferenceAssemblies.Net.Net90, + ReferenceAssemblies = ReferenceAssemblies.Net.Net90 + .AddPackages([new PackageIdentity("Microsoft.Bcl.AsyncInterfaces", "9.0.0")]), TestState = { AdditionalReferences = { - typeof(TUnit.Core.TUnitAttribute).Assembly.Location, + TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnit.Core.TUnitAttribute).Assembly), }, }, }; diff --git a/TUnit.Assertions.Analyzers.CodeFixers.Tests/TUnit.Assertions.Analyzers.CodeFixers.Tests.csproj b/TUnit.Assertions.Analyzers.CodeFixers.Tests/TUnit.Assertions.Analyzers.CodeFixers.Tests.csproj index c1d0a9af32..f0e477243c 100644 --- a/TUnit.Assertions.Analyzers.CodeFixers.Tests/TUnit.Assertions.Analyzers.CodeFixers.Tests.csproj +++ b/TUnit.Assertions.Analyzers.CodeFixers.Tests/TUnit.Assertions.Analyzers.CodeFixers.Tests.csproj @@ -5,6 +5,24 @@ + + + + + + + diff --git a/TUnit.Assertions.Analyzers.CodeFixers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs b/TUnit.Assertions.Analyzers.CodeFixers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs index 2c8ee11dc2..c05da31c84 100644 --- a/TUnit.Assertions.Analyzers.CodeFixers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs +++ b/TUnit.Assertions.Analyzers.CodeFixers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs @@ -60,8 +60,8 @@ params DiagnosticResult[] expected { AdditionalReferences = { - typeof(TUnitAttribute).Assembly.Location, - typeof(Assert).Assembly.Location, + TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly), + TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Assertions", typeof(Assert).Assembly), }, }, }; @@ -102,8 +102,8 @@ public static async Task VerifyCodeFixAsync( { AdditionalReferences = { - typeof(TUnitAttribute).Assembly.Location, - typeof(Assert).Assembly.Location, + TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly), + TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Assertions", typeof(Assert).Assembly), }, }, CodeActionValidationMode = CodeActionValidationMode.SemanticStructure, diff --git a/TUnit.Assertions.Analyzers.Tests/AnalyzerTestHelpers.cs b/TUnit.Assertions.Analyzers.Tests/AnalyzerTestHelpers.cs index 58efd18d48..e7fd49d332 100644 --- a/TUnit.Assertions.Analyzers.Tests/AnalyzerTestHelpers.cs +++ b/TUnit.Assertions.Analyzers.Tests/AnalyzerTestHelpers.cs @@ -32,8 +32,12 @@ public static CSharpAnalyzerTest CreateAnalyzerTest< csTest.TestState.AdditionalReferences .AddRange( [ - MetadataReference.CreateFromFile(typeof(TUnitAttribute).Assembly.Location), - MetadataReference.CreateFromFile(typeof(Assert).Assembly.Location), + MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly)), + MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Assertions", typeof(Assert).Assembly)), + MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Assertions.Should", typeof(TUnit.Assertions.Should.ShouldExtensions).Assembly)), +#if NET8_0 + MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetSystemTextJson9DllPath()), +#endif ] ); @@ -139,8 +143,12 @@ public static CSharpSuppressorTest CreateSuppresso test.TestState.AdditionalReferences .AddRange([ - MetadataReference.CreateFromFile(typeof(TUnitAttribute).Assembly.Location), - MetadataReference.CreateFromFile(typeof(Assert).Assembly.Location), + MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly)), + MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Assertions", typeof(Assert).Assembly)), + MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Assertions.Should", typeof(TUnit.Assertions.Should.ShouldExtensions).Assembly)), +#if NET8_0 + MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetSystemTextJson9DllPath()), +#endif ]); return test; @@ -152,9 +160,7 @@ private static ReferenceAssemblies GetReferenceAssemblies() return ReferenceAssemblies.NetFramework.Net472.Default; #elif NET8_0 return ReferenceAssemblies.Net.Net80; -#elif NET9_0 - return ReferenceAssemblies.Net.Net90; -#elif NET10_0_OR_GREATER +#elif NET9_0_OR_GREATER return ReferenceAssemblies.Net.Net90; #else return ReferenceAssemblies.Net.Net80; // Default fallback diff --git a/TUnit.Assertions.Analyzers.Tests/ShouldAnalyzerTests.cs b/TUnit.Assertions.Analyzers.Tests/ShouldAnalyzerTests.cs new file mode 100644 index 0000000000..9683f3aaf8 --- /dev/null +++ b/TUnit.Assertions.Analyzers.Tests/ShouldAnalyzerTests.cs @@ -0,0 +1,133 @@ +using AwaitVerifier = TUnit.Assertions.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier; +using MixVerifier = TUnit.Assertions.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier; + +namespace TUnit.Assertions.Analyzers.Tests; + +public class ShouldAnalyzerTests +{ + [Test] + public async Task Should_chain_not_awaited_is_flagged() + { + await AwaitVerifier + .VerifyAnalyzerAsync( + """ + using System.Threading.Tasks; + using TUnit.Assertions.Should; + using TUnit.Assertions.Should.Extensions; + + public class MyClass + { + public async Task MyTest() + { + var one = 1; + {|#0:one.Should()|}.BeEqualTo(1); + } + } + """, + + AwaitVerifier.Diagnostic(Rules.AwaitAssertion) + .WithLocation(0) + ); + } + + [Test] + public async Task Should_chain_awaited_is_clean() + { + await AwaitVerifier + .VerifyAnalyzerAsync( + """ + using System.Threading.Tasks; + using TUnit.Assertions.Should; + using TUnit.Assertions.Should.Extensions; + + public class MyClass + { + public async Task MyTest() + { + var one = 1; + await one.Should().BeEqualTo(1); + } + } + """ + ); + } + + [Test] + public async Task Mixed_And_Or_in_Should_chain_is_flagged() + { + await MixVerifier + .VerifyAnalyzerAsync( + """ + using System.Threading.Tasks; + using TUnit.Assertions.Should; + using TUnit.Assertions.Should.Extensions; + + public class MyClass + { + public async Task MyTest() + { + var one = 1; + {|#0:await one.Should().BeEqualTo(1).And.BeEqualTo(1).Or.BeEqualTo(2)|}; + } + } + """, + + MixVerifier.Diagnostic(Rules.MixAndOrConditionsAssertion) + .WithLocation(0) + ); + } + + [Test] + public async Task Pure_And_in_Should_chain_is_clean() + { + await MixVerifier + .VerifyAnalyzerAsync( + """ + using System.Threading.Tasks; + using TUnit.Assertions.Should; + using TUnit.Assertions.Should.Extensions; + + public class MyClass + { + public async Task MyTest() + { + var one = 1; + await one.Should().BeEqualTo(1).And.NotBeEqualTo(7); + } + } + """ + ); + } + + [Test] + public async Task Unrelated_Should_extension_in_other_namespace_is_not_flagged() + { + // Confirms the analyzer's TUnit-namespace check rules out unrelated `Should()` + // extensions (e.g. FluentAssertions, custom user libraries) from triggering + // TUnitAssertions0002. + await AwaitVerifier + .VerifyAnalyzerAsync( + """ + using System.Threading.Tasks; + + namespace OtherLibrary + { + public class Wrapper { public Wrapper Should() => this; public void BeFoo() { } } + public static class WrapperExtensions + { + public static Wrapper Should(this object value) => new Wrapper(); + } + } + + public class MyClass + { + public async Task MyTest() + { + var one = 1; + OtherLibrary.WrapperExtensions.Should(one).BeFoo(); + } + } + """ + ); + } +} diff --git a/TUnit.Assertions.Analyzers.Tests/TUnit.Assertions.Analyzers.Tests.csproj b/TUnit.Assertions.Analyzers.Tests/TUnit.Assertions.Analyzers.Tests.csproj index 04b80de846..6eb1edc2f8 100644 --- a/TUnit.Assertions.Analyzers.Tests/TUnit.Assertions.Analyzers.Tests.csproj +++ b/TUnit.Assertions.Analyzers.Tests/TUnit.Assertions.Analyzers.Tests.csproj @@ -4,6 +4,37 @@ + + + + + + + + + + @@ -12,6 +43,7 @@ + diff --git a/TUnit.Assertions.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs b/TUnit.Assertions.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs index ea4bd75c56..a9be2616c4 100644 --- a/TUnit.Assertions.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs +++ b/TUnit.Assertions.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs @@ -3,6 +3,8 @@ using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; +using TUnit.Assertions; +using TUnit.Core; namespace TUnit.Assertions.Analyzers.Tests.Verifiers; @@ -45,8 +47,12 @@ public static async Task VerifyAnalyzerAsync([StringSyntax("c#-test")] string so { AdditionalReferences = { - typeof(TUnitAttribute).Assembly.Location, - typeof(Assert).Assembly.Location, + TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly), + TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Assertions", typeof(Assert).Assembly), + TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Assertions.Should", typeof(TUnit.Assertions.Should.ShouldExtensions).Assembly), +#if NET8_0 + TUnit.Tests.Shared.AnalyzerTestCompatibility.GetSystemTextJson9DllPath(), +#endif }, }, CompilerDiagnostics = CompilerDiagnostics.None diff --git a/TUnit.Assertions.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs b/TUnit.Assertions.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs index 8567c9ad6f..e239af0e26 100644 --- a/TUnit.Assertions.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs +++ b/TUnit.Assertions.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs @@ -4,6 +4,8 @@ using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; +using TUnit.Assertions; +using TUnit.Core; namespace TUnit.Assertions.Analyzers.Tests.Verifiers; @@ -38,8 +40,8 @@ params DiagnosticResult[] expected { AdditionalReferences = { - typeof(TUnitAttribute).Assembly.Location, - typeof(Assert).Assembly.Location, + TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly), + TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Assertions", typeof(Assert).Assembly), }, }, }; @@ -72,8 +74,8 @@ public static async Task VerifyCodeFixAsync( { AdditionalReferences = { - typeof(TUnitAttribute).Assembly.Location, - typeof(Assert).Assembly.Location, + TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly), + TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Assertions", typeof(Assert).Assembly), }, }, }; diff --git a/TUnit.Assertions.Analyzers/AwaitAssertionAnalyzer.cs b/TUnit.Assertions.Analyzers/AwaitAssertionAnalyzer.cs index 9a134b30dd..d6d4f3a656 100644 --- a/TUnit.Assertions.Analyzers/AwaitAssertionAnalyzer.cs +++ b/TUnit.Assertions.Analyzers/AwaitAssertionAnalyzer.cs @@ -7,8 +7,8 @@ namespace TUnit.Assertions.Analyzers; /// -/// A sample analyzer that reports the company name being used in class declarations. -/// Traverses through the Syntax Tree and checks the name (identifier) of each class node. +/// Reports Assert.That(...) / value.Should() assertion entries that aren't awaited +/// (which silently no-op) and Assert.Multiple() calls without a using declaration. /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public class AwaitAssertionAnalyzer : ConcurrentDiagnosticAnalyzer @@ -37,7 +37,8 @@ private void AnalyzeOperation(OperationAnalysisContext context) CheckMultipleInvocation(context, invocationOperation); } - if (fullyQualifiedNonGenericMethodName is "global::TUnit.Assertions.Assert.That") + if (fullyQualifiedNonGenericMethodName is "global::TUnit.Assertions.Assert.That" + or "global::TUnit.Assertions.Should.ShouldExtensions.Should") { CheckAssertInvocation(context, invocationOperation); } @@ -67,6 +68,16 @@ private static void CheckMultipleInvocation(OperationAnalysisContext context, II ); } + // Walks the syntactic parent chain. Stops at IBlockOperation/IDelegateCreationOperation as + // negative answers — that means the invocation was used as a statement / lambda body without + // an enclosing await. This catches the common `value.Should();` mistake but produces a false + // positive for split-variable patterns: + // var src = Assert.That(value); // ← walk hits the declaration's block before any await + // await src.IsEqualTo(...); // even though the chain IS awaited here + // The same applies to `var src = value.Should(); await src.X();`. Both Assert.That and + // Should() share this limitation by design — fixing requires usage-site dataflow analysis, + // which is significant complexity for a niche style. Left as a known imprecision so users who + // hit it know it's not their code. private static bool IsAwaited(IInvocationOperation invocationOperation) { var parent = invocationOperation.Parent; diff --git a/TUnit.Assertions.Analyzers/IsNotNullAssertionSuppressor.cs b/TUnit.Assertions.Analyzers/IsNotNullAssertionSuppressor.cs index 50b70698bb..5024822c1b 100644 --- a/TUnit.Assertions.Analyzers/IsNotNullAssertionSuppressor.cs +++ b/TUnit.Assertions.Analyzers/IsNotNullAssertionSuppressor.cs @@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; +using TUnit.Assertions.Analyzers.Extensions; namespace TUnit.Assertions.Analyzers; @@ -81,6 +82,11 @@ private bool IsNullabilityWarning(string diagnosticId) }; } + // Statement-order match only — not control-flow aware. An assertion inside an `if (cond)` or + // `try`/`catch` branch suppresses warnings on subsequent uses even when the assertion may not + // have run on every path. Accepting that imprecision keeps the analyzer cheap; the alternative + // (full dataflow analysis via Roslyn's IFlowAnalysis) is significant complexity for a niche + // false-suppression case. See AwaitAssertionAnalyzer for the symmetric awaitedness check. private bool WasAssertedNotNull( ExpressionSyntax targetExpression, SemanticModel semanticModel, @@ -140,37 +146,31 @@ private bool IsNotNullAssertion( SemanticModel semanticModel, CancellationToken cancellationToken) { - // Pattern: await Assert.That(variable).IsNotNull() - // or: await Assert.That(variable).Contains("test").And.IsNotNull() - // or: Assert.That(variable).IsNotNull().GetAwaiter().GetResult() + // Patterns recognised: + // await Assert.That(variable).IsNotNull() + // await Assert.That(variable).Contains("test").And.IsNotNull() + // Assert.That(variable).IsNotNull().GetAwaiter().GetResult() + // await variable.Should().NotBeNull() + // await variable.Should().Contain("test").And.NotBeNull() var invocations = statement.DescendantNodes().OfType(); foreach (var invocation in invocations) { - // Check if this is a call to IsNotNull() - if (invocation.Expression is not MemberAccessExpressionSyntax { Name.Identifier.Text: "IsNotNull" }) + if (invocation.Expression is not MemberAccessExpressionSyntax { Name.Identifier.Text: var calledName }) { continue; } - // Walk up the expression chain to find Assert.That() call - var assertThatCall = FindAssertThatInChain(invocation); - if (assertThatCall is null) + ExpressionSyntax? targetArgument = calledName switch { - continue; - } - - // Get the argument to Assert.That() - if (assertThatCall.ArgumentList.Arguments.Count != 1) - { - continue; - } - - var argument = assertThatCall.ArgumentList.Arguments[0].Expression; + "IsNotNull" => GetAssertThatArgument(invocation, semanticModel, cancellationToken), + "NotBeNull" => GetShouldReceiver(invocation, semanticModel, cancellationToken), + _ => null, + }; - // Check if the argument matches the target expression - if (ExpressionsMatch(argument, targetExpression, semanticModel, cancellationToken)) + if (targetArgument is not null + && ExpressionsMatch(targetArgument, targetExpression, semanticModel, cancellationToken)) { return true; } @@ -179,6 +179,48 @@ private bool IsNotNullAssertion( return false; } + private static ExpressionSyntax? GetAssertThatArgument( + InvocationExpressionSyntax invocation, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + var assertThatCall = FindAssertThatInChain(invocation); + if (assertThatCall is null + || assertThatCall.ArgumentList.Arguments.Count != 1 + || !IsTUnitMethod(assertThatCall, semanticModel, cancellationToken, "global::TUnit.Assertions.Assert.That")) + { + return null; + } + + return assertThatCall.ArgumentList.Arguments[0].Expression; + } + + private static ExpressionSyntax? GetShouldReceiver( + InvocationExpressionSyntax invocation, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + var shouldCall = FindShouldInChain(invocation); + if (shouldCall is null + || !IsTUnitMethod(shouldCall, semanticModel, cancellationToken, "global::TUnit.Assertions.Should.ShouldExtensions.Should")) + { + return null; + } + + // Should is an extension method — its receiver is the value being asserted. + return shouldCall.Expression is MemberAccessExpressionSyntax memberAccess + ? memberAccess.Expression + : null; + } + + private static bool IsTUnitMethod( + InvocationExpressionSyntax invocation, + SemanticModel semanticModel, + CancellationToken cancellationToken, + string fullyQualifiedNonGenericName) + => semanticModel.GetSymbolInfo(invocation, cancellationToken).Symbol is IMethodSymbol symbol + && symbol.GloballyQualifiedNonGeneric() == fullyQualifiedNonGenericName; + private bool ExpressionsMatch( ExpressionSyntax assertArgument, ExpressionSyntax targetExpression, @@ -215,31 +257,44 @@ private bool SymbolsMatch( return symbol1 is not null && SymbolEqualityComparer.Default.Equals(symbol1, symbol2); } - private InvocationExpressionSyntax? FindAssertThatInChain(InvocationExpressionSyntax invocation) + private static InvocationExpressionSyntax? FindAssertThatInChain(InvocationExpressionSyntax invocation) + => FindInvocationInChain(invocation, identifierName: "That", parentName: "Assert"); + + // Should() is an extension method, so its receiver is the asserted value (any expression). + // parentName MUST stay null — constraining it would break the suppressor for user-defined + // assertion entry points and for Should() reached via using-aliases / namespace imports. + private static InvocationExpressionSyntax? FindShouldInChain(InvocationExpressionSyntax invocation) + => FindInvocationInChain(invocation, identifierName: "Should", parentName: null); + + /// + /// Walks up an expression chain looking for an invocation whose member-access name is + /// . When is non-null the + /// invocation must also be of the form {parentName}.{identifierName}(...); for + /// extension methods (Should) the receiver is arbitrary so parentName is null. + /// + private static InvocationExpressionSyntax? FindInvocationInChain( + InvocationExpressionSyntax invocation, + string identifierName, + string? parentName) { - // Walk up the expression chain looking for Assert.That() var current = invocation.Expression; while (current is not null) { if (current is InvocationExpressionSyntax invocationExpr) { - // Check if this is Assert.That() - if (invocationExpr.Expression is MemberAccessExpressionSyntax - { - Name.Identifier.Text: "That", - Expression: IdentifierNameSyntax { Identifier.Text: "Assert" } - }) + if (invocationExpr.Expression is MemberAccessExpressionSyntax memberExpr + && memberExpr.Name.Identifier.Text == identifierName + && (parentName is null + || (memberExpr.Expression is IdentifierNameSyntax id && id.Identifier.Text == parentName))) { return invocationExpr; } - // Continue walking up from this invocation current = invocationExpr.Expression; } else if (current is MemberAccessExpressionSyntax memberAccess) { - // Move to the expression being accessed current = memberAccess.Expression; } else diff --git a/TUnit.Assertions.Analyzers/MixAndOrOperatorsAnalyzer.cs b/TUnit.Assertions.Analyzers/MixAndOrOperatorsAnalyzer.cs index b468e9a81a..96b52bbd2d 100644 --- a/TUnit.Assertions.Analyzers/MixAndOrOperatorsAnalyzer.cs +++ b/TUnit.Assertions.Analyzers/MixAndOrOperatorsAnalyzer.cs @@ -7,8 +7,9 @@ namespace TUnit.Assertions.Analyzers; /// -/// A sample analyzer that reports the company name being used in class declarations. -/// Traverses through the Syntax Tree and checks the name (identifier) of each class node. +/// Reports awaited assertion chains that mix .And and .Or combinators without +/// explicit grouping — the runtime throws MixedAndOrAssertionsException, this surfaces it +/// at compile time. Covers both Assert.That and value.Should() entry points. /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public class MixAndOrOperatorsAnalyzer : ConcurrentDiagnosticAnalyzer @@ -28,12 +29,14 @@ private static void AnalyzeOperation(OperationAnalysisContext context) return; } - // Check if the awaited type implements IAssertionSource or inherits from Assertion + // Check if the awaited type implements IAssertionSource/IShouldSource or + // inherits from Assertion. ShouldAssertion is covered by the IShouldSource branch + // (it implements that interface), so it doesn't need a separate base-type check. var awaitedType = awaitOperation.Operation.Type; var isAssertionSource = awaitedType?.AllInterfaces.Any(x => - x.GloballyQualifiedNonGeneric() is "global::TUnit.Assertions.Core.IAssertionSource") == true; - var isAssertion = awaitedType?.BaseType != null && - IsAssertionType(awaitedType.BaseType); + x.GloballyQualifiedNonGeneric() is "global::TUnit.Assertions.Core.IAssertionSource" + or "global::TUnit.Assertions.Should.Core.IShouldSource") == true; + var isAssertion = awaitedType?.BaseType != null && IsAssertionType(awaitedType.BaseType); if (!isAssertionSource && !isAssertion) { diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/GlobalSetup.cs b/TUnit.Assertions.Should.SourceGenerator.Tests/GlobalSetup.cs new file mode 100644 index 0000000000..35a1207a39 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/GlobalSetup.cs @@ -0,0 +1,12 @@ +using DiffEngine; + +namespace TUnit.Assertions.Should.SourceGenerator.Tests; + +public class GlobalSetup +{ + [Before(TestSession)] + public static void SetUp() + { + DiffRunner.Disabled = true; + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/GlobalUsings.cs b/TUnit.Assertions.Should.SourceGenerator.Tests/GlobalUsings.cs new file mode 100644 index 0000000000..5a50de2ef6 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using VerifyTUnit; +global using VerifyTests; +global using static VerifyTUnit.Verifier; diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/NameConjugatorTests.cs b/TUnit.Assertions.Should.SourceGenerator.Tests/NameConjugatorTests.cs new file mode 100644 index 0000000000..9adcc81acb --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/NameConjugatorTests.cs @@ -0,0 +1,54 @@ +namespace TUnit.Assertions.Should.SourceGenerator.Tests; + +/// +/// Direct unit tests for the pure-function . The end-to-end +/// generator tests in exercise the same rules +/// indirectly, but only for the rules each test snippet happens to hit; these cases lock +/// in the full conjugation table — including the -es drops that surfaced as a bug +/// during PR review. +/// +public class NameConjugatorTests +{ + [Test] + [Arguments("IsEqualTo", "BeEqualTo")] + [Arguments("IsZero", "BeZero")] + [Arguments("IsEmpty", "BeEmpty")] + [Arguments("IsNotNull", "NotBeNull")] + [Arguments("IsNotEqualTo", "NotBeEqualTo")] + [Arguments("IsNotEmpty", "NotBeEmpty")] + [Arguments("HasNotBeenCalled", "NotHaveBeenCalled")] + [Arguments("HasCount", "HaveCount")] + [Arguments("HasFiles", "HaveFiles")] + [Arguments("DoesNotContain", "NotContain")] + [Arguments("DoesNotMatch", "NotMatch")] + [Arguments("DoesMatch", "Match")] + [Arguments("Contains", "Contain")] + [Arguments("StartsWith", "StartWith")] + [Arguments("EndsWith", "EndWith")] + [Arguments("Throws", "Throw")] + [Arguments("Matches", "Match")] + [Arguments("MatchesRegex", "MatchRegex")] + [Arguments("Washes", "Wash")] + [Arguments("Catches", "Catch")] + [Arguments("Fixes", "Fix")] + [Arguments("Buzzes", "Buzz")] + [Arguments("Normalizes", "Normalize")] + [Arguments("Analyzes", "Analyze")] + [Arguments("Authorizes", "Authorize")] + [Arguments("Goes", "Go")] + [Arguments("Passes", "Pass")] + [Arguments("Writes", "Write")] + [Arguments("Applies", "Apply")] + [Arguments("Tries", "Try")] + [Arguments("Dies", "Die")] + [Arguments("Ties", "Tie")] + [Arguments("Pass", "Pass")] + [Arguments("Issue", "Issue")] + [Arguments("Is", "Be")] + [Arguments("Has", "Have")] + [Arguments("", "")] + public async Task Conjugate_returns_expected(string input, string expected) + { + await Assert.That(NameConjugator.Conjugate(input)).IsEqualTo(expected); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.CallerArgumentExpression_attribute_is_propagated.DotNet10_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.CallerArgumentExpression_attribute_is_propagated.DotNet10_0.verified.txt new file mode 100644 index 0000000000..b3f7d6a0d2 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.CallerArgumentExpression_attribute_is_propagated.DotNet10_0.verified.txt @@ -0,0 +1,40 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should.Extensions; + +public static partial class ShouldMyBetweenExtensions +{ + + public static global::TUnit.Assertions.Should.Core.ShouldAssertion BeBetween(this global::TUnit.Assertions.Should.Core.IShouldSource source, TValue min, TValue max, [global::System.Runtime.CompilerServices.CallerArgumentExpression("min")] string? minExpression = null, [global::System.Runtime.CompilerServices.CallerArgumentExpression("max")] string? maxExpression = null) + { + var innerContext = source.Context; + innerContext.ExpressionBuilder.Append(".BeBetween("); + var __added = false; + if (minExpression is not null) + { + if (__added) innerContext.ExpressionBuilder.Append(", "); + innerContext.ExpressionBuilder.Append(minExpression); + __added = true; + } + if (maxExpression is not null) + { + if (__added) innerContext.ExpressionBuilder.Append(", "); + innerContext.ExpressionBuilder.Append(maxExpression); + __added = true; + } + innerContext.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.MyBetweenAssertion(innerContext, min, max); + var __tunit_should_because = source.ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(innerContext, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.CallerArgumentExpression_attribute_is_propagated.DotNet8_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.CallerArgumentExpression_attribute_is_propagated.DotNet8_0.verified.txt new file mode 100644 index 0000000000..b3f7d6a0d2 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.CallerArgumentExpression_attribute_is_propagated.DotNet8_0.verified.txt @@ -0,0 +1,40 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should.Extensions; + +public static partial class ShouldMyBetweenExtensions +{ + + public static global::TUnit.Assertions.Should.Core.ShouldAssertion BeBetween(this global::TUnit.Assertions.Should.Core.IShouldSource source, TValue min, TValue max, [global::System.Runtime.CompilerServices.CallerArgumentExpression("min")] string? minExpression = null, [global::System.Runtime.CompilerServices.CallerArgumentExpression("max")] string? maxExpression = null) + { + var innerContext = source.Context; + innerContext.ExpressionBuilder.Append(".BeBetween("); + var __added = false; + if (minExpression is not null) + { + if (__added) innerContext.ExpressionBuilder.Append(", "); + innerContext.ExpressionBuilder.Append(minExpression); + __added = true; + } + if (maxExpression is not null) + { + if (__added) innerContext.ExpressionBuilder.Append(", "); + innerContext.ExpressionBuilder.Append(maxExpression); + __added = true; + } + innerContext.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.MyBetweenAssertion(innerContext, min, max); + var __tunit_should_because = source.ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(innerContext, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.CallerArgumentExpression_attribute_is_propagated.DotNet9_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.CallerArgumentExpression_attribute_is_propagated.DotNet9_0.verified.txt new file mode 100644 index 0000000000..b3f7d6a0d2 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.CallerArgumentExpression_attribute_is_propagated.DotNet9_0.verified.txt @@ -0,0 +1,40 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should.Extensions; + +public static partial class ShouldMyBetweenExtensions +{ + + public static global::TUnit.Assertions.Should.Core.ShouldAssertion BeBetween(this global::TUnit.Assertions.Should.Core.IShouldSource source, TValue min, TValue max, [global::System.Runtime.CompilerServices.CallerArgumentExpression("min")] string? minExpression = null, [global::System.Runtime.CompilerServices.CallerArgumentExpression("max")] string? maxExpression = null) + { + var innerContext = source.Context; + innerContext.ExpressionBuilder.Append(".BeBetween("); + var __added = false; + if (minExpression is not null) + { + if (__added) innerContext.ExpressionBuilder.Append(", "); + innerContext.ExpressionBuilder.Append(minExpression); + __added = true; + } + if (maxExpression is not null) + { + if (__added) innerContext.ExpressionBuilder.Append(", "); + innerContext.ExpressionBuilder.Append(maxExpression); + __added = true; + } + innerContext.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.MyBetweenAssertion(innerContext, min, max); + var __tunit_should_because = source.ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(innerContext, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.GenericAssertionExtension_emits_method_generic_param.DotNet10_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.GenericAssertionExtension_emits_method_generic_param.DotNet10_0.verified.txt new file mode 100644 index 0000000000..e9e994dc97 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.GenericAssertionExtension_emits_method_generic_param.DotNet10_0.verified.txt @@ -0,0 +1,28 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should.Extensions; + +public static partial class ShouldMyEqualsExtensions +{ + + public static global::TUnit.Assertions.Should.Core.ShouldAssertion BeEqualTo(this global::TUnit.Assertions.Should.Core.IShouldSource source, TValue expected, [global::System.Runtime.CompilerServices.CallerArgumentExpression("expected")] string? expectedExpression = null) + { + var innerContext = source.Context; + innerContext.ExpressionBuilder.Append(".BeEqualTo("); + innerContext.ExpressionBuilder.Append(expectedExpression); + innerContext.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.MyEqualsAssertion(innerContext, expected); + var __tunit_should_because = source.ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(innerContext, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.GenericAssertionExtension_emits_method_generic_param.DotNet8_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.GenericAssertionExtension_emits_method_generic_param.DotNet8_0.verified.txt new file mode 100644 index 0000000000..e9e994dc97 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.GenericAssertionExtension_emits_method_generic_param.DotNet8_0.verified.txt @@ -0,0 +1,28 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should.Extensions; + +public static partial class ShouldMyEqualsExtensions +{ + + public static global::TUnit.Assertions.Should.Core.ShouldAssertion BeEqualTo(this global::TUnit.Assertions.Should.Core.IShouldSource source, TValue expected, [global::System.Runtime.CompilerServices.CallerArgumentExpression("expected")] string? expectedExpression = null) + { + var innerContext = source.Context; + innerContext.ExpressionBuilder.Append(".BeEqualTo("); + innerContext.ExpressionBuilder.Append(expectedExpression); + innerContext.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.MyEqualsAssertion(innerContext, expected); + var __tunit_should_because = source.ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(innerContext, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.GenericAssertionExtension_emits_method_generic_param.DotNet9_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.GenericAssertionExtension_emits_method_generic_param.DotNet9_0.verified.txt new file mode 100644 index 0000000000..e9e994dc97 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.GenericAssertionExtension_emits_method_generic_param.DotNet9_0.verified.txt @@ -0,0 +1,28 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should.Extensions; + +public static partial class ShouldMyEqualsExtensions +{ + + public static global::TUnit.Assertions.Should.Core.ShouldAssertion BeEqualTo(this global::TUnit.Assertions.Should.Core.IShouldSource source, TValue expected, [global::System.Runtime.CompilerServices.CallerArgumentExpression("expected")] string? expectedExpression = null) + { + var innerContext = source.Context; + innerContext.ExpressionBuilder.Append(".BeEqualTo("); + innerContext.ExpressionBuilder.Append(expectedExpression); + innerContext.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.MyEqualsAssertion(innerContext, expected); + var __tunit_should_because = source.ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(innerContext, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.IsNot_prefix_conjugates_to_NotBe.DotNet10_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.IsNot_prefix_conjugates_to_NotBe.DotNet10_0.verified.txt new file mode 100644 index 0000000000..4ed689d27c --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.IsNot_prefix_conjugates_to_NotBe.DotNet10_0.verified.txt @@ -0,0 +1,27 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should.Extensions; + +public static partial class ShouldNotEmptyExtensions +{ + + public static global::TUnit.Assertions.Should.Core.ShouldAssertion NotBeEmpty(this global::TUnit.Assertions.Should.Core.IShouldSource source) + { + var innerContext = source.Context; + innerContext.ExpressionBuilder.Append(".NotBeEmpty("); + innerContext.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.NotEmptyAssertion(innerContext); + var __tunit_should_because = source.ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(innerContext, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.IsNot_prefix_conjugates_to_NotBe.DotNet8_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.IsNot_prefix_conjugates_to_NotBe.DotNet8_0.verified.txt new file mode 100644 index 0000000000..4ed689d27c --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.IsNot_prefix_conjugates_to_NotBe.DotNet8_0.verified.txt @@ -0,0 +1,27 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should.Extensions; + +public static partial class ShouldNotEmptyExtensions +{ + + public static global::TUnit.Assertions.Should.Core.ShouldAssertion NotBeEmpty(this global::TUnit.Assertions.Should.Core.IShouldSource source) + { + var innerContext = source.Context; + innerContext.ExpressionBuilder.Append(".NotBeEmpty("); + innerContext.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.NotEmptyAssertion(innerContext); + var __tunit_should_because = source.ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(innerContext, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.IsNot_prefix_conjugates_to_NotBe.DotNet9_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.IsNot_prefix_conjugates_to_NotBe.DotNet9_0.verified.txt new file mode 100644 index 0000000000..4ed689d27c --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.IsNot_prefix_conjugates_to_NotBe.DotNet9_0.verified.txt @@ -0,0 +1,27 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should.Extensions; + +public static partial class ShouldNotEmptyExtensions +{ + + public static global::TUnit.Assertions.Should.Core.ShouldAssertion NotBeEmpty(this global::TUnit.Assertions.Should.Core.IShouldSource source) + { + var innerContext = source.Context; + innerContext.ExpressionBuilder.Append(".NotBeEmpty("); + innerContext.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.NotEmptyAssertion(innerContext); + var __tunit_should_because = source.ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(innerContext, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.Obsolete_attribute_is_forwarded.DotNet10_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.Obsolete_attribute_is_forwarded.DotNet10_0.verified.txt new file mode 100644 index 0000000000..d77d861405 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.Obsolete_attribute_is_forwarded.DotNet10_0.verified.txt @@ -0,0 +1,28 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should.Extensions; + +public static partial class ShouldLegacyExtensions +{ + + [global::System.Obsolete("Use IsModern instead.")] + public static global::TUnit.Assertions.Should.Core.ShouldAssertion BeLegacy(this global::TUnit.Assertions.Should.Core.IShouldSource source) + { + var innerContext = source.Context; + innerContext.ExpressionBuilder.Append(".BeLegacy("); + innerContext.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.LegacyAssertion(innerContext); + var __tunit_should_because = source.ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(innerContext, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.Obsolete_attribute_is_forwarded.DotNet8_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.Obsolete_attribute_is_forwarded.DotNet8_0.verified.txt new file mode 100644 index 0000000000..d77d861405 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.Obsolete_attribute_is_forwarded.DotNet8_0.verified.txt @@ -0,0 +1,28 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should.Extensions; + +public static partial class ShouldLegacyExtensions +{ + + [global::System.Obsolete("Use IsModern instead.")] + public static global::TUnit.Assertions.Should.Core.ShouldAssertion BeLegacy(this global::TUnit.Assertions.Should.Core.IShouldSource source) + { + var innerContext = source.Context; + innerContext.ExpressionBuilder.Append(".BeLegacy("); + innerContext.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.LegacyAssertion(innerContext); + var __tunit_should_because = source.ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(innerContext, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.Obsolete_attribute_is_forwarded.DotNet9_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.Obsolete_attribute_is_forwarded.DotNet9_0.verified.txt new file mode 100644 index 0000000000..d77d861405 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.Obsolete_attribute_is_forwarded.DotNet9_0.verified.txt @@ -0,0 +1,28 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should.Extensions; + +public static partial class ShouldLegacyExtensions +{ + + [global::System.Obsolete("Use IsModern instead.")] + public static global::TUnit.Assertions.Should.Core.ShouldAssertion BeLegacy(this global::TUnit.Assertions.Should.Core.IShouldSource source) + { + var innerContext = source.Context; + innerContext.ExpressionBuilder.Append(".BeLegacy("); + innerContext.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.LegacyAssertion(innerContext); + var __tunit_should_because = source.ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(innerContext, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.ShouldNameAttribute_overrides_conjugation.DotNet10_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.ShouldNameAttribute_overrides_conjugation.DotNet10_0.verified.txt new file mode 100644 index 0000000000..ac426aeaba --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.ShouldNameAttribute_overrides_conjugation.DotNet10_0.verified.txt @@ -0,0 +1,27 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should.Extensions; + +public static partial class ShouldOddExtensions +{ + + public static global::TUnit.Assertions.Should.Core.ShouldAssertion BeAnOddNumber(this global::TUnit.Assertions.Should.Core.IShouldSource source) + { + var innerContext = source.Context; + innerContext.ExpressionBuilder.Append(".BeAnOddNumber("); + innerContext.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.OddAssertion(innerContext); + var __tunit_should_because = source.ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(innerContext, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.ShouldNameAttribute_overrides_conjugation.DotNet8_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.ShouldNameAttribute_overrides_conjugation.DotNet8_0.verified.txt new file mode 100644 index 0000000000..ac426aeaba --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.ShouldNameAttribute_overrides_conjugation.DotNet8_0.verified.txt @@ -0,0 +1,27 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should.Extensions; + +public static partial class ShouldOddExtensions +{ + + public static global::TUnit.Assertions.Should.Core.ShouldAssertion BeAnOddNumber(this global::TUnit.Assertions.Should.Core.IShouldSource source) + { + var innerContext = source.Context; + innerContext.ExpressionBuilder.Append(".BeAnOddNumber("); + innerContext.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.OddAssertion(innerContext); + var __tunit_should_because = source.ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(innerContext, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.ShouldNameAttribute_overrides_conjugation.DotNet9_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.ShouldNameAttribute_overrides_conjugation.DotNet9_0.verified.txt new file mode 100644 index 0000000000..ac426aeaba --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.ShouldNameAttribute_overrides_conjugation.DotNet9_0.verified.txt @@ -0,0 +1,27 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should.Extensions; + +public static partial class ShouldOddExtensions +{ + + public static global::TUnit.Assertions.Should.Core.ShouldAssertion BeAnOddNumber(this global::TUnit.Assertions.Should.Core.IShouldSource source) + { + var innerContext = source.Context; + innerContext.ExpressionBuilder.Append(".BeAnOddNumber("); + innerContext.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.OddAssertion(innerContext); + var __tunit_should_because = source.ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(innerContext, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.SimpleAssertionExtension_emits_conjugated_extension_method.DotNet10_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.SimpleAssertionExtension_emits_conjugated_extension_method.DotNet10_0.verified.txt new file mode 100644 index 0000000000..ef26605920 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.SimpleAssertionExtension_emits_conjugated_extension_method.DotNet10_0.verified.txt @@ -0,0 +1,27 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should.Extensions; + +public static partial class ShouldStringIsEmptyExtensions +{ + + public static global::TUnit.Assertions.Should.Core.ShouldAssertion BeEmpty(this global::TUnit.Assertions.Should.Core.IShouldSource source) + { + var innerContext = source.Context; + innerContext.ExpressionBuilder.Append(".BeEmpty("); + innerContext.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.StringIsEmptyAssertion(innerContext); + var __tunit_should_because = source.ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(innerContext, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.SimpleAssertionExtension_emits_conjugated_extension_method.DotNet8_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.SimpleAssertionExtension_emits_conjugated_extension_method.DotNet8_0.verified.txt new file mode 100644 index 0000000000..ef26605920 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.SimpleAssertionExtension_emits_conjugated_extension_method.DotNet8_0.verified.txt @@ -0,0 +1,27 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should.Extensions; + +public static partial class ShouldStringIsEmptyExtensions +{ + + public static global::TUnit.Assertions.Should.Core.ShouldAssertion BeEmpty(this global::TUnit.Assertions.Should.Core.IShouldSource source) + { + var innerContext = source.Context; + innerContext.ExpressionBuilder.Append(".BeEmpty("); + innerContext.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.StringIsEmptyAssertion(innerContext); + var __tunit_should_because = source.ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(innerContext, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.SimpleAssertionExtension_emits_conjugated_extension_method.DotNet9_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.SimpleAssertionExtension_emits_conjugated_extension_method.DotNet9_0.verified.txt new file mode 100644 index 0000000000..ef26605920 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.SimpleAssertionExtension_emits_conjugated_extension_method.DotNet9_0.verified.txt @@ -0,0 +1,27 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should.Extensions; + +public static partial class ShouldStringIsEmptyExtensions +{ + + public static global::TUnit.Assertions.Should.Core.ShouldAssertion BeEmpty(this global::TUnit.Assertions.Should.Core.IShouldSource source) + { + var innerContext = source.Context; + innerContext.ExpressionBuilder.Append(".BeEmpty("); + innerContext.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.StringIsEmptyAssertion(innerContext); + var __tunit_should_because = source.ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(innerContext, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.Wrapper_generation_deduplicates_overridden_instance_methods.DotNet10_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.Wrapper_generation_deduplicates_overridden_instance_methods.DotNet10_0.verified.txt new file mode 100644 index 0000000000..669ace7528 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.Wrapper_generation_deduplicates_overridden_instance_methods.DotNet10_0.verified.txt @@ -0,0 +1,25 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; + +namespace MyNamespace; + +partial class ShouldDerivedSource +{ + + public global::TUnit.Assertions.Should.Core.ShouldAssertion BeReady() + { + Context.ExpressionBuilder.Append(".BeReady("); + Context.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.BaseWrappedAssertion(Context); + var __tunit_should_because = ((global::TUnit.Assertions.Should.Core.IShouldSource)this).ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(Context, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.Wrapper_generation_deduplicates_overridden_instance_methods.DotNet8_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.Wrapper_generation_deduplicates_overridden_instance_methods.DotNet8_0.verified.txt new file mode 100644 index 0000000000..669ace7528 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.Wrapper_generation_deduplicates_overridden_instance_methods.DotNet8_0.verified.txt @@ -0,0 +1,25 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; + +namespace MyNamespace; + +partial class ShouldDerivedSource +{ + + public global::TUnit.Assertions.Should.Core.ShouldAssertion BeReady() + { + Context.ExpressionBuilder.Append(".BeReady("); + Context.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.BaseWrappedAssertion(Context); + var __tunit_should_because = ((global::TUnit.Assertions.Should.Core.IShouldSource)this).ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(Context, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.Wrapper_generation_deduplicates_overridden_instance_methods.DotNet9_0.verified.txt b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.Wrapper_generation_deduplicates_overridden_instance_methods.DotNet9_0.verified.txt new file mode 100644 index 0000000000..669ace7528 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.Wrapper_generation_deduplicates_overridden_instance_methods.DotNet9_0.verified.txt @@ -0,0 +1,25 @@ +// +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; + +namespace MyNamespace; + +partial class ShouldDerivedSource +{ + + public global::TUnit.Assertions.Should.Core.ShouldAssertion BeReady() + { + Context.ExpressionBuilder.Append(".BeReady("); + Context.ExpressionBuilder.Append(")"); + var inner = new global::MyNamespace.BaseWrappedAssertion(Context); + var __tunit_should_because = ((global::TUnit.Assertions.Should.Core.IShouldSource)this).ConsumeBecauseMessage(); + if (__tunit_should_because is not null) + { + inner.Because(__tunit_should_because); + } + return new global::TUnit.Assertions.Should.Core.ShouldAssertion(Context, inner); + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.cs b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.cs new file mode 100644 index 0000000000..693db3a3c7 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/ShouldExtensionGeneratorTests.cs @@ -0,0 +1,311 @@ +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using TUnit.Assertions.Core; + +namespace TUnit.Assertions.Should.SourceGenerator.Tests; + +/// +/// Locks in the key behaviours of by compiling small +/// inline snippets, running the generator, and asserting on the emitted source. Failing tests +/// here mean the Should-flavored API surface has shifted in a user-visible way — any change +/// is intentional and should be reflected in the assertions below. +/// +/// +/// The generator scans for extension methods on ; the test +/// snippets declare those methods explicitly because TUnit.Assertions.SourceGenerator +/// (which would normally synthesise them from [AssertionExtension]) doesn't run inside +/// the test's in-memory compilation. +/// +public class ShouldExtensionGeneratorTests +{ + [Test] + public async Task SimpleAssertionExtension_emits_conjugated_extension_method() + { + var output = await RunGenerator(""" + using TUnit.Assertions.Core; + + namespace MyNamespace; + + public class StringIsEmptyAssertion : Assertion + { + public StringIsEmptyAssertion(AssertionContext context) : base(context) { } + protected override Task CheckAsync(EvaluationMetadata metadata) + => Task.FromResult(AssertionResult.Passed); + protected override string GetExpectation() => "to be empty"; + } + + public static class StringIsEmptyExtensions + { + public static StringIsEmptyAssertion IsEmpty(this IAssertionSource source) + => new(source.Context); + } + """); + + // Conjugation: IsEmpty -> BeEmpty (Is* -> Be* rule). + await Assert.That(output).Contains("BeEmpty"); + // Should-flavored surface — first param is IShouldSource, return is ShouldAssertion. + await Assert.That(output).Contains("this global::TUnit.Assertions.Should.Core.IShouldSource source"); + await Assert.That(output).Contains("global::TUnit.Assertions.Should.Core.ShouldAssertion"); + // Inner assertion is constructed with the existing TUnit.Assertions class. + await Assert.That(output).Contains("new global::MyNamespace.StringIsEmptyAssertion(innerContext)"); + } + + [Test] + public async Task GenericAssertionExtension_emits_method_generic_param() + { + var output = await RunGenerator(""" + using TUnit.Assertions.Core; + using System.Runtime.CompilerServices; + + namespace MyNamespace; + + public class MyEqualsAssertion : Assertion + { + public MyEqualsAssertion(AssertionContext context, TValue expected) : base(context) { } + protected override Task CheckAsync(EvaluationMetadata metadata) + => Task.FromResult(AssertionResult.Passed); + protected override string GetExpectation() => "to be equal"; + } + + public static class MyEqualsExtensions + { + public static MyEqualsAssertion IsEqualTo( + this IAssertionSource source, + TValue expected, + [CallerArgumentExpression(nameof(expected))] string? expectedExpression = null) + => new(source.Context, expected); + } + """); + + await Assert.That(output).Contains("BeEqualTo"); + await Assert.That(output).Contains("IShouldSource"); + await Assert.That(output).Contains("TValue expected"); + } + + [Test] + public async Task ShouldNameAttribute_overrides_conjugation() + { + var output = await RunGenerator(""" + using TUnit.Assertions.Core; + using TUnit.Assertions.Should.Attributes; + + namespace MyNamespace; + + [ShouldName("BeAnOddNumber")] + public class OddAssertion : Assertion + { + public OddAssertion(AssertionContext context) : base(context) { } + protected override Task CheckAsync(EvaluationMetadata metadata) + => Task.FromResult(AssertionResult.Passed); + protected override string GetExpectation() => "to be odd"; + } + + public static class OddExtensions + { + public static OddAssertion IsOdd(this IAssertionSource source) + => new(source.Context); + } + """); + + await Assert.That(output).Contains("BeAnOddNumber"); + // Default conjugation would have produced "BeOdd" — verify it didn't sneak in alongside. + await Assert.That(output).DoesNotContain("public static global::TUnit.Assertions.Should.Core.ShouldAssertion BeOdd("); + } + + [Test] + public async Task Obsolete_attribute_is_forwarded() + { + var output = await RunGenerator(""" + using System; + using TUnit.Assertions.Core; + + namespace MyNamespace; + + public class LegacyAssertion : Assertion + { + public LegacyAssertion(AssertionContext context) : base(context) { } + protected override Task CheckAsync(EvaluationMetadata metadata) + => Task.FromResult(AssertionResult.Passed); + protected override string GetExpectation() => "to be legacy"; + } + + public static class LegacyExtensions + { + [Obsolete("Use IsModern instead.")] + public static LegacyAssertion IsLegacy(this IAssertionSource source) + => new(source.Context); + } + """); + + await Assert.That(output).Contains("Obsolete"); + await Assert.That(output).Contains("Use IsModern instead."); + } + + [Test] + public async Task Wrapper_generation_deduplicates_overridden_instance_methods() + { + var output = await RunGenerator(""" + using TUnit.Assertions.Core; + using TUnit.Assertions.Should.Attributes; + using TUnit.Assertions.Should.Core; + + namespace MyNamespace; + + public class BaseWrappedAssertion : Assertion + { + public BaseWrappedAssertion(AssertionContext context) : base(context) { } + public virtual BaseWrappedAssertion IsReady() => new(Context); + protected override Task CheckAsync(EvaluationMetadata metadata) + => Task.FromResult(AssertionResult.Passed); + protected override string GetExpectation() => "to be ready"; + } + + public sealed class DerivedWrappedAssertion : BaseWrappedAssertion + { + public DerivedWrappedAssertion(AssertionContext context) : base(context) { } + public override BaseWrappedAssertion IsReady() => new(Context); + } + + [ShouldGeneratePartial(typeof(DerivedWrappedAssertion))] + public sealed partial class ShouldDerivedSource : IShouldSource + { + public AssertionContext Context { get; } + public ShouldDerivedSource(AssertionContext context) => Context = context; + string? IShouldSource.ConsumeBecauseMessage() => null; + } + """); + + await Assert.That(CountOccurrences(output, " BeReady(")).IsEqualTo(1); + } + + [Test] + public async Task IsNot_prefix_conjugates_to_NotBe() + { + var output = await RunGenerator(""" + using TUnit.Assertions.Core; + + namespace MyNamespace; + + public class NotEmptyAssertion : Assertion + { + public NotEmptyAssertion(AssertionContext context) : base(context) { } + protected override Task CheckAsync(EvaluationMetadata metadata) + => Task.FromResult(AssertionResult.Passed); + protected override string GetExpectation() => "to not be empty"; + } + + public static class NotEmptyExtensions + { + public static NotEmptyAssertion IsNotEmpty(this IAssertionSource source) + => new(source.Context); + } + """); + + await Assert.That(output).Contains("NotBeEmpty"); + } + + [Test] + public async Task CallerArgumentExpression_attribute_is_propagated() + { + var output = await RunGenerator(""" + using TUnit.Assertions.Core; + using System.Runtime.CompilerServices; + + namespace MyNamespace; + + public class MyBetweenAssertion : Assertion + { + public MyBetweenAssertion(AssertionContext context, TValue min, TValue max) : base(context) { } + protected override Task CheckAsync(EvaluationMetadata metadata) + => Task.FromResult(AssertionResult.Passed); + protected override string GetExpectation() => "to be between"; + } + + public static class MyBetweenExtensions + { + public static MyBetweenAssertion IsBetween( + this IAssertionSource source, + TValue min, + TValue max, + [CallerArgumentExpression(nameof(min))] string? minExpression = null, + [CallerArgumentExpression(nameof(max))] string? maxExpression = null) + => new(source.Context, min, max); + } + """); + + await Assert.That(output).Contains("BeBetween"); + await Assert.That(output).Contains("CallerArgumentExpression(\"min\")"); + await Assert.That(output).Contains("CallerArgumentExpression(\"max\")"); + } + + /// + /// Compiles with the Should-generator's input dependencies, + /// runs , snapshots the full generated source via + /// Verify (per-TFM .verified.txt files), and returns the concatenated output so + /// callers can additionally string-match key tokens. The Verify snapshot catches + /// formatting/ordering/global:: regressions that token-level checks miss; the + /// inline Contains assertions remain as explicit guard-rails for the user-visible + /// API tokens that any change should call out deliberately. + /// + private static async Task RunGenerator(string userSource, [CallerMemberName] string testName = "") + { + var compilation = CSharpCompilation.Create( + assemblyName: "GeneratorTest", + syntaxTrees: [CSharpSyntaxTree.ParseText(userSource)], + references: GetReferences(), + options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var driver = CSharpGeneratorDriver.Create(new ShouldExtensionGenerator()); + driver = (CSharpGeneratorDriver)driver.RunGeneratorsAndUpdateCompilation( + compilation, out var updatedCompilation, out var diagnostics); + + await Assert.That(diagnostics.Length).IsEqualTo(0) + .Because("Generator should not emit diagnostics for valid input"); + + var trees = updatedCompilation.SyntaxTrees + .Where(t => t != compilation.SyntaxTrees[0]) + .Select(t => t.ToString()); + + var combined = string.Join("\n//------\n", trees); + + await Verify(combined) + .UseFileName($"{nameof(ShouldExtensionGeneratorTests)}.{testName}") + .UniqueForTargetFrameworkAndVersion(); + + return 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()) + { + if (asm.IsDynamic || string.IsNullOrWhiteSpace(asm.Location)) + { + continue; + } + yield return MetadataReference.CreateFromFile(asm.Location); + } + + yield return MetadataReference.CreateFromFile(typeof(Assertion<>).Assembly.Location); + yield return MetadataReference.CreateFromFile(typeof(TUnit.Assertions.Should.Core.IShouldSource).Assembly.Location); + } + + private static int CountOccurrences(string value, string search) + { + var count = 0; + var index = 0; + while ((index = value.IndexOf(search, index, StringComparison.Ordinal)) >= 0) + { + count++; + index += search.Length; + } + return count; + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator.Tests/TUnit.Assertions.Should.SourceGenerator.Tests.csproj b/TUnit.Assertions.Should.SourceGenerator.Tests/TUnit.Assertions.Should.SourceGenerator.Tests.csproj new file mode 100644 index 0000000000..1302b34d0f --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator.Tests/TUnit.Assertions.Should.SourceGenerator.Tests.csproj @@ -0,0 +1,43 @@ + + + + + net8.0;net9.0;net10.0 + + + + + + $(NoWarn);MSB3277 + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/TUnit.Assertions.Should.SourceGenerator/NameConjugator.cs b/TUnit.Assertions.Should.SourceGenerator/NameConjugator.cs new file mode 100644 index 0000000000..bf6d946fdd --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator/NameConjugator.cs @@ -0,0 +1,159 @@ +namespace TUnit.Assertions.Should.SourceGenerator; + +/// +/// Pure-function transformer from Is*/Has*/Does*/3rd-person-singular method +/// names produced by [AssertionExtension] to FluentAssertions-style imperative +/// Should-flavored equivalents (BeEqualTo, HaveCount, NotContain, etc.). +/// +internal static class NameConjugator +{ + /// + /// Conjugate an [AssertionExtension] method name into its Should-flavored counterpart. + /// Names that don't match any rule are returned unchanged — this is the intended forward-compat + /// behaviour for unrecognised naming conventions; callers can layer [ShouldName] overrides + /// on top to rename specific assertions. + /// + public static string Conjugate(string methodName) + { + if (string.IsNullOrEmpty(methodName)) + { + return methodName; + } + + // Order: longest-prefix-first so negated prefixes win over their positive forms. + if (TryReplacePrefix(methodName, "IsNot", "NotBe", out var s)) return s; + if (TryReplacePrefix(methodName, "Is", "Be", out s)) return s; + if (TryReplacePrefix(methodName, "HasNot", "NotHave", out s)) return s; + if (TryReplacePrefix(methodName, "Has", "Have", out s)) return s; + if (TryReplacePrefix(methodName, "DoesNot", "Not", out s)) return s; + if (TryReplacePrefix(methodName, "Does", "", out s)) return s; + if (TryDropTrailingS(methodName, out s)) return s; + + return methodName; + } + + /// + /// Replaces with only when it ends + /// at a CamelCase word boundary (next char uppercase, or end of name). Prevents matches like + /// "Issue" → "Besue". + /// + private static bool TryReplacePrefix(string name, string prefix, string replacement, out string result) + { + if (!name.StartsWith(prefix, System.StringComparison.Ordinal) + || (name.Length > prefix.Length && !char.IsUpper(name[prefix.Length]))) + { + result = name; + return false; + } + + result = replacement + name.Substring(prefix.Length); + return true; + } + + /// + /// First-word trailing-s drop. ContainsContain, StartsWithStartWith. + /// Recognises English -es third-person endings (MatchesMatch, + /// WashesWash, FixesFix, BuzzesBuzz, + /// GoesGo, PassesPass) and consonant+-ies + /// endings (AppliesApply). + /// Plain -ss endings stay put so Pass remains Pass. + /// + private static bool TryDropTrailingS(string name, out string result) + { + var firstWordEnd = name.Length; + for (var i = 1; i < name.Length; i++) + { + if (char.IsUpper(name[i])) + { + firstWordEnd = i; + break; + } + } + + if (firstWordEnd < 2 || name[firstWordEnd - 1] != 's') + { + result = name; + return false; + } + + if (TryReplaceTrailingIes(name, firstWordEnd, out result)) + { + return true; + } + + var dropCount = GetTrailingSDropCount(name, firstWordEnd); + if (dropCount == 0) + { + result = name; + return false; + } + + result = name.Substring(0, firstWordEnd - dropCount) + name.Substring(firstWordEnd); + return true; + } + + private static bool TryReplaceTrailingIes(string name, int firstWordEnd, out string result) + { + if (firstWordEnd < 5 + || name[firstWordEnd - 3] != 'i' + || name[firstWordEnd - 2] != 'e' + || IsVowel(name[firstWordEnd - 4])) + { + result = name; + return false; + } + + result = name.Substring(0, firstWordEnd - 3) + "y" + name.Substring(firstWordEnd); + return true; + } + + private static bool IsVowel(char c) + => c is 'a' or 'e' or 'i' or 'o' or 'u' or 'A' or 'E' or 'I' or 'O' or 'U'; + + private static bool IsVowelOrY(char c) + => IsVowel(c) || c is 'y' or 'Y'; + + /// + /// Returns how many trailing letters of the first word should be removed: 2 for an English + /// -es 3rd-person ending where the stem ends in a sibilant (ch, sh, + /// ss, x, z) or in o; 1 for a plain -s; 0 to leave the + /// name unchanged (e.g. plain -ss, which is a stem rather than a verb form). + /// + private static int GetTrailingSDropCount(string name, int firstWordEnd) + { + if (firstWordEnd >= 3 && name[firstWordEnd - 2] == 'e') + { + var c = name[firstWordEnd - 3]; + if (c == 'x' || c == 'o') + { + return 2; + } + if (c == 'z') + { + // "buzzes" adds -es to a z-ending stem, but "normalizes" is + // "normalize" + s; keep the stem's silent e when z follows a vowel-like char. + return firstWordEnd >= 4 && IsVowelOrY(name[firstWordEnd - 4]) ? 1 : 2; + } + if (firstWordEnd >= 4) + { + var c2 = name[firstWordEnd - 4]; + if (c == 'h' && (c2 == 'c' || c2 == 's')) + { + return 2; // ches / shes + } + if (c == 's' && c2 == 's') + { + return 2; // sses + } + } + return 1; // generic -es (e.g. Writes -> Write) + } + + if (name[firstWordEnd - 2] == 's') + { + return 0; // plain -ss; not a verb form + } + + return 1; + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator/ShouldExtensionGenerator.cs b/TUnit.Assertions.Should.SourceGenerator/ShouldExtensionGenerator.cs new file mode 100644 index 0000000000..ad54707268 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator/ShouldExtensionGenerator.cs @@ -0,0 +1,1220 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using Microsoft.CodeAnalysis; +using TUnit.Core.SourceGenerator.Models; + +namespace TUnit.Assertions.Should.SourceGenerator; + +/// +/// Source generator that scans the current compilation and every referenced assembly for +/// public extension methods on IAssertionSource<T> whose return type derives +/// from Assertion<TReturn> and whose body is a "simple factory" — meaning the +/// non-CAE method parameters map 1-to-1 (by name and type) onto a public constructor of +/// the return type, after the leading AssertionContext<T> parameter. For each +/// match, emits a Should-flavored counterpart on IShouldSource<T>. +/// +/// This unifies four assertion sources: classes with [AssertionExtension], methods +/// with [GenerateAssertion], types decorated with [AssertionFrom<T>], +/// and any hand-written extension methods whose body is just new T(ctx, args). +/// Methods that don't fit the factory template (context mapping, transformations, etc.) +/// are silently skipped — they couldn't be wrapped without inspecting their body anyway. +/// +/// +[Generator] +public sealed class ShouldExtensionGenerator : IIncrementalGenerator +{ + private const string AssertionSourceFullName = "TUnit.Assertions.Core.IAssertionSource`1"; + private const string AssertionBaseFullName = "TUnit.Assertions.Core.Assertion`1"; + private const string AssertionContextFullName = "TUnit.Assertions.Core.AssertionContext`1"; + private const string ShouldExtensionsNamespace = "TUnit.Assertions.Should.Extensions"; + private const string ShouldNameAttributeFullName = "TUnit.Assertions.Should.Attributes.ShouldNameAttribute"; + private const string CallerArgumentExpressionAttributeName = "CallerArgumentExpressionAttribute"; + private const string RequiresUnreferencedCodeAttributeName = "RequiresUnreferencedCodeAttribute"; + private const string UnconditionalSuppressMessageAttributeName = "UnconditionalSuppressMessageAttribute"; + private const string DynamicallyAccessedMembersAttributeName = "DynamicallyAccessedMembersAttribute"; + private const string ShouldGeneratePartialAttributeFullName = "TUnit.Assertions.Should.Attributes.ShouldGeneratePartialAttribute"; + + private static readonly SymbolDisplayFormat NoGlobalFormat = + SymbolDisplayFormat.FullyQualifiedFormat + .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted) + .AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); + + private static readonly SymbolDisplayFormat NameWithoutTypeArgsFormat = + SymbolDisplayFormat.FullyQualifiedFormat + .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted) + .WithGenericsOptions(SymbolDisplayGenericsOptions.None); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // CompilationProvider fires per-keystroke. Cross-assembly cost is absorbed by the + // ConditionalWeakTable cache below; the current-compilation walk still re-runs every + // keystroke. A SyntaxProvider-based pipeline (paired with ForAttributeWithMetadataName + // for [AssertionExtension] declarations and a custom-syntax filter for hand-rolled + // extensions on IAssertionSource) would eliminate that cost. Deferred to a follow-up + // — the structural refactor is non-trivial and the current bound (~types-with-extension- + // methods in current compilation) is acceptable for v1. + var provider = context.CompilationProvider.Select((compilation, _) => Collect(compilation)); + + context.RegisterSourceOutput(provider, static (ctx, payload) => + { + var emittedHints = new HashSet(StringComparer.Ordinal); + + // Wrappers first: they own the return types they cover, and their method names + // win over extension methods at call sites anyway. + foreach (var wrapper in payload.Wrappers) + { + EmitWrapperPartial(ctx, wrapper, emittedHints); + } + + foreach (var group in payload.Methods.GroupBy(m => m.ContainerName, StringComparer.Ordinal)) + { + EmitContainer(ctx, group.Key, group.ToArray(), emittedHints); + } + }); + } + + /// + /// Caches the data extracted from each referenced assembly, keyed on the + /// instance. Roslyn typically reuses the same + /// MetadataReference across compilations as long as the underlying assembly + /// hasn't been rebuilt, so cache hits eliminate the expensive cross-assembly walk on + /// every keystroke. The cache stores raw walk results (no dedup applied) so that the + /// dedup sets — built from the union of all references plus the current compilation — + /// can be applied at merge time without invalidating cache entries. + /// + /// uses weak keys so entries become + /// eligible for GC the moment Roslyn drops the underlying MetadataReference (e.g. + /// when the dependency assembly is rebuilt). A ConcurrentDictionary would pin + /// stale references for the lifetime of the IDE process and cause unbounded memory + /// growth across long sessions with frequent rebuilds. + /// + /// + private static readonly ConditionalWeakTable s_referenceCache = new(); + + // IMPORTANT: keep ReferenceData compilation-independent. Do not cache live ISymbol + // instances here; symbols are tied to the compilation that produced them and become stale + // when Roslyn creates the next compilation. + private sealed record ReferenceData( + EquatableArray Methods, + EquatableArray Wrappers, + EquatableArray AlreadyBakedNames); + + private static GeneratorPayload Collect(Compilation compilation) + { + var assertionSource = compilation.GetTypeByMetadataName(AssertionSourceFullName); + var assertionBase = compilation.GetTypeByMetadataName(AssertionBaseFullName); + var assertionContext = compilation.GetTypeByMetadataName(AssertionContextFullName); + var shouldNameAttr = compilation.GetTypeByMetadataName(ShouldNameAttributeFullName); + var partialMarker = compilation.GetTypeByMetadataName(ShouldGeneratePartialAttributeFullName); + + if (assertionSource is null || assertionBase is null || assertionContext is null) + { + return new GeneratorPayload( + new EquatableArray(Array.Empty()), + new EquatableArray(Array.Empty())); + } + + // Phase 1 — per-reference scan, cached by MetadataReference identity. + // Skip references that don't transitively reference TUnit.Assertions: the BCL plus arbitrary + // NuGet packages can't possibly contain extension methods on IAssertionSource, and the + // closure pre-filter is the single biggest perf win for the generator. + var assertionsAssembly = assertionSource.ContainingAssembly; + var refResults = new List(); + foreach (var refAssembly in compilation.SourceModule.ReferencedAssemblySymbols) + { + if (!ReferencesAssertionsAssembly(refAssembly, assertionsAssembly)) + { + continue; + } + refResults.Add(GetOrComputeReferenceData( + compilation, refAssembly, assertionSource, assertionBase, assertionContext, shouldNameAttr, partialMarker)); + } + + // Phase 2 — union dedup sets across all references. + var alreadyBaked = new HashSet(StringComparer.Ordinal); + foreach (var r in refResults) + { + foreach (var name in r.AlreadyBakedNames) + { + alreadyBaked.Add(name); + } + } + + // Phase 3 — walk the current compilation (cannot be cached: changes on every edit). + var localMethods = ImmutableArray.CreateBuilder(); + var localCtx = new CollectionContext( + compilation, + assertionSource, + assertionBase, + assertionContext, + shouldNameAttr, + alreadyBaked, + localMethods); + WalkNamespace(compilation.Assembly.GlobalNamespace, localCtx); + + var localWrappers = new List(); + if (partialMarker is not null) + { + WalkForWrappers(compilation.Assembly.GlobalNamespace, partialMarker, assertionBase, assertionContext, localWrappers, isCurrentAssembly: true); + } + + // Phase 4 — merge and apply post-walk dedup. Wrapper instance methods and Should-flavored + // extensions co-exist by design: the wrapper's [ShouldGeneratePartial] only emits methods + // whose source overload exactly matches a public ctor on the inner assertion (the simple- + // factory rule), so overloads with optional/default parameters land only on the extension + // surface. Instance methods take overload-resolution precedence at call sites, so there's + // no ambiguity. References whose ShouldNameExtensions counterpart is already baked are + // dropped to prevent CS0121. + var allMethods = ImmutableArray.CreateBuilder(); + foreach (var m in localMethods) + { + allMethods.Add(m); + } + foreach (var r in refResults) + { + foreach (var m in r.Methods) + { + if (alreadyBaked.Contains($"Should{m.ContainerName}")) + { + continue; + } + allMethods.Add(m); + } + } + + return new GeneratorPayload( + new EquatableArray(allMethods.ToArray()), + new EquatableArray(localWrappers.ToArray())); + } + + /// + /// Returns the cached for , or + /// performs a one-shot scan and stores the result. The scan is dedup-free — the union dedup + /// is applied at merge time, so a cache entry remains valid even when other references in + /// the compilation change. + /// + private static ReferenceData GetOrComputeReferenceData( + Compilation compilation, + IAssemblySymbol refAssembly, + INamedTypeSymbol assertionSource, + INamedTypeSymbol assertionBase, + INamedTypeSymbol assertionContext, + INamedTypeSymbol? shouldNameAttr, + INamedTypeSymbol? partialMarker) + { + var metadataRef = compilation.GetMetadataReference(refAssembly); + if (metadataRef is null) + { + return ScanReference(refAssembly, compilation, assertionSource, assertionBase, assertionContext, shouldNameAttr, partialMarker); + } + + if (s_referenceCache.TryGetValue(metadataRef, out var cached)) + { + return cached; + } + + var fresh = ScanReference(refAssembly, compilation, assertionSource, assertionBase, assertionContext, shouldNameAttr, partialMarker); + + // Concurrent races between two compilations seeing the same uncached MetadataReference + // are harmless — both compute the same ReferenceData; first writer wins. Catching + // ArgumentException is the documented way to handle the "already added" case on + // ConditionalWeakTable.Add (no TryAdd overload exists in netstandard2.0). + try + { + s_referenceCache.Add(metadataRef, fresh); + } + catch (ArgumentException) + { + } + return fresh; + } + + private static ReferenceData ScanReference( + IAssemblySymbol refAssembly, + Compilation compilation, + INamedTypeSymbol assertionSource, + INamedTypeSymbol assertionBase, + INamedTypeSymbol assertionContext, + INamedTypeSymbol? shouldNameAttr, + INamedTypeSymbol? partialMarker) + { + var methods = ImmutableArray.CreateBuilder(); + var ctx = new CollectionContext( + compilation, + assertionSource, + assertionBase, + assertionContext, + shouldNameAttr, + new HashSet(StringComparer.Ordinal), // no per-reference dedup; applied at merge + methods); + WalkNamespace(refAssembly.GlobalNamespace, ctx); + + var wrappers = new List(); + if (partialMarker is not null) + { + WalkForWrappers(refAssembly.GlobalNamespace, partialMarker, assertionBase, assertionContext, wrappers, isCurrentAssembly: false); + } + + var bakedNames = new List(); + var bakedNs = LookupNamespace(refAssembly.GlobalNamespace, ShouldExtensionsNamespace); + if (bakedNs is not null) + { + foreach (var t in bakedNs.GetTypeMembers()) + { + bakedNames.Add(t.Name); + } + } + + return new ReferenceData( + new EquatableArray(methods.ToArray()), + new EquatableArray(wrappers.ToArray()), + new EquatableArray(bakedNames.ToArray())); + } + + private static void WalkForWrappers( + INamespaceSymbol ns, + INamedTypeSymbol marker, + INamedTypeSymbol assertionBase, + INamedTypeSymbol assertionContext, + List builder, + bool isCurrentAssembly) + { + foreach (var type in ns.GetTypeMembers()) + { + CollectWrapper(type, marker, assertionBase, assertionContext, builder, isCurrentAssembly); + } + foreach (var nested in ns.GetNamespaceMembers()) + { + WalkForWrappers(nested, marker, assertionBase, assertionContext, builder, isCurrentAssembly); + } + } + + private static void CollectWrapper( + INamedTypeSymbol type, + INamedTypeSymbol marker, + INamedTypeSymbol assertionBase, + INamedTypeSymbol assertionContext, + List builder, + bool isCurrentAssembly) + { + foreach (var nested in type.GetTypeMembers()) + { + CollectWrapper(nested, marker, assertionBase, assertionContext, builder, isCurrentAssembly); + } + + // Read the wrapped type from [ShouldGeneratePartial(typeof(...))]. The attribute's + // single ctor argument names the wrapped definition explicitly, so the wrapper class + // is free to construct its own AssertionContext rather than piggybacking on the + // wrapped type's constructor. For 1-arity generics the open form is supplied + // (typeof(Foo<>)) and we substitute the wrapper class's type parameter to close it. + INamedTypeSymbol? wrappedType = null; + ITypeSymbol? wrappedAssertionTypeArg = null; + foreach (var attr in type.GetAttributes()) + { + if (!SymbolEqualityComparer.Default.Equals(attr.AttributeClass, marker)) continue; + if (attr.ConstructorArguments.Length != 1) continue; + if (attr.ConstructorArguments[0].Value is not INamedTypeSymbol declared) continue; + + var closed = CloseWrappedType(declared, type); + if (closed is null) continue; + if (!DerivesFromAssertion(closed, assertionBase, out var typeArg)) continue; + + wrappedType = closed; + wrappedAssertionTypeArg = typeArg; + break; + } + + if (wrappedType is null || wrappedAssertionTypeArg is null) + { + return; + } + + // Wrappers from referenced assemblies are still collected — their return-type keys + // feed the dedup set so the main extension-method scan skips already-baked extensions. + // The IsCurrentAssembly flag on WrapperData controls whether the emission step actually + // generates partial methods (only true for wrappers in this compilation). + var methods = ImmutableArray.CreateBuilder(); + foreach (var sourceMember in EnumerateInstanceMethods(wrappedType)) + { + if (TryDescribeWrapperMethod(sourceMember, wrappedAssertionTypeArg, assertionBase, assertionContext, out var data)) + { + methods.Add(data); + } + } + + if (methods.Count == 0 && isCurrentAssembly) + { + return; + } + + builder.Add(new WrapperData( + ContainingNamespace: type.ContainingNamespace?.ToDisplayString(NoGlobalFormat) ?? string.Empty, + ClassName: type.Name, + ClassGenericParams: new EquatableArray(type.TypeParameters.Select(tp => GenericParamData.From(tp, NoGlobalFormat)).ToList()), + ClassGenericSuffix: type.IsGenericType ? "<" + string.Join(", ", type.TypeParameters.Select(tp => tp.Name)) + ">" : string.Empty, + AssertionTypeArgDisplay: wrappedAssertionTypeArg.ToDisplayString(NoGlobalFormat), + Methods: new EquatableArray(methods), + IsCurrentAssembly: isCurrentAssembly)); + } + + /// + /// Closes against 's type parameters. + /// Already-closed types pass through unchanged. Open generics whose arity matches the wrapper + /// are constructed by substituting the wrapper's type parameters in declaration order — this + /// covers the typical typeof(Foo<>) on a 1-arity wrapper case. Anything else + /// returns null so the caller skips emission. + /// + private static INamedTypeSymbol? CloseWrappedType(INamedTypeSymbol declared, INamedTypeSymbol wrapper) + { + if (!declared.IsUnboundGenericType && !declared.IsGenericType) + { + return declared; + } + if (!declared.IsUnboundGenericType) + { + return declared; // already closed + } + if (declared.TypeParameters.Length != wrapper.TypeParameters.Length) + { + return null; + } + return declared.OriginalDefinition.Construct(wrapper.TypeParameters.Cast().ToArray()); + } + + private static IEnumerable EnumerateInstanceMethods(INamedTypeSymbol type) + { + var seen = new HashSet(StringComparer.Ordinal); + for (var current = type; + current is not null && current.SpecialType != SpecialType.System_Object; + current = current.BaseType) + { + foreach (var member in current.GetMembers()) + { + if (member is IMethodSymbol m + && m.MethodKind == MethodKind.Ordinary + && !m.IsStatic + && m.DeclaredAccessibility == Accessibility.Public + && seen.Add(GetMethodSignatureKey(m))) + { + yield return m; + } + } + } + } + + private static string GetMethodSignatureKey(IMethodSymbol method) + { + var sb = new StringBuilder(method.Name); + sb.Append('`').Append(method.Arity); + foreach (var parameter in method.Parameters) + { + sb.Append('|') + .Append(parameter.RefKind) + .Append(':') + .Append(parameter.Type.ToDisplayString(NoGlobalFormat)); + } + return sb.ToString(); + } + + private static bool TryDescribeWrapperMethod( + IMethodSymbol method, + ITypeSymbol assertionTypeArg, + INamedTypeSymbol assertionBase, + INamedTypeSymbol assertionContext, + out WrapperMethodData data) + { + data = null!; + + // Skip methods with method-level generic parameters for v1 — emitting them requires + // propagating type-arg references that appear in the return type's generic arguments + // (e.g. IsAssignableTo returns IsAssignableToAssertion) and + // the inference works less reliably without explicit declaration site info. + if (method.TypeParameters.Length > 0) + { + return false; + } + + if (method.ReturnType is not INamedTypeSymbol returnType + || !DerivesFromAssertion(returnType, assertionBase, out var returnedAssertionArg)) + { + return false; + } + + // Wrapper instance methods only make sense when the underlying assertion's value type + // matches the wrapper's wrapped type — anything else would require a context Map. + if (!SymbolEqualityComparer.Default.Equals(returnedAssertionArg, assertionTypeArg)) + { + return false; + } + + var paramData = ImmutableArray.CreateBuilder(); + var ctorCandidates = new List(); + foreach (var p in method.Parameters) + { + var caeTarget = TryGetCallerArgumentExpressionTarget(p); + paramData.Add(new ParameterData( + Name: p.Name, + TypeName: p.Type.ToDisplayString(NoGlobalFormat), + HasDefaultValue: p.HasExplicitDefaultValue, + DefaultValueLiteral: p.HasExplicitDefaultValue ? FormatDefaultValue(p.ExplicitDefaultValue, p.Type) : null, + CallerArgumentExpressionTarget: caeTarget)); + if (caeTarget is null) ctorCandidates.Add(p); + } + + if (!HasMatchingConstructor(returnType, assertionTypeArg, assertionContext, ctorCandidates)) + { + return false; + } + + data = new WrapperMethodData( + SourceMethodName: method.Name, + Parameters: new EquatableArray(paramData), + ReturnTypeFullName: returnType.ConstructedFrom.ToDisplayString(NameWithoutTypeArgsFormat), + ReturnTypeGenericArgs: new EquatableArray(returnType.TypeArguments.Select(a => a.ToDisplayString(NoGlobalFormat)).ToList()), + RequiresUnreferencedCodeMessage: TryGetRucMessage(method.GetAttributes()) + ?? TryGetRucMessage(returnType.GetAttributes()) + ?? TryGetRucMessageFromConstructors(returnType)); + return true; + } + + /// + /// Pre-filter: returns true only when the reference (or one of its direct module references) + /// is itself. The check is one-level deep, NOT transitive — a + /// transitive reference is treated as "doesn't contain Should-relevant types" and skipped. + /// In practice this is safe because IAssertionSource<T> lives in TUnit.Assertions, + /// so any assembly declaring extension methods on it must have a direct reference. Skipping + /// transitively-only-referenced assemblies is the single biggest perf win for the generator + /// (otherwise it'd walk the entire BCL). + /// + private static bool ReferencesAssertionsAssembly(IAssemblySymbol reference, IAssemblySymbol assertionsAssembly) + { + if (SymbolEqualityComparer.Default.Equals(reference, assertionsAssembly)) + { + return true; + } + + foreach (var module in reference.Modules) + { + foreach (var refed in module.ReferencedAssemblySymbols) + { + if (SymbolEqualityComparer.Default.Equals(refed, assertionsAssembly)) + { + return true; + } + } + } + return false; + } + + private static INamespaceSymbol? LookupNamespace(INamespaceSymbol root, string dottedName) + { + var current = root; + foreach (var segment in dottedName.Split('.')) + { + current = current.GetNamespaceMembers().FirstOrDefault(n => n.Name == segment); + if (current is null) return null; + } + return current; + } + + private static void WalkNamespace(INamespaceSymbol ns, CollectionContext ctx) + { + foreach (var type in ns.GetTypeMembers()) + { + CollectFromContainer(type, ctx); + } + foreach (var nested in ns.GetNamespaceMembers()) + { + WalkNamespace(nested, ctx); + } + } + + private static void CollectFromContainer(INamedTypeSymbol type, CollectionContext ctx) + { + foreach (var nested in type.GetTypeMembers()) + { + CollectFromContainer(nested, ctx); + } + + if (type.DeclaredAccessibility != Accessibility.Public + || !type.IsStatic) + { + return; + } + + // Generic static containers are not supported; [AssertionExtension] methods live on + // non-generic static classes so the generated Should wrapper has a concrete owner. + if (type.IsGenericType) + { + return; + } + + if (!SymbolEqualityComparer.Default.Equals(type.ContainingAssembly, ctx.Compilation.Assembly) + && ctx.AlreadyBakedShouldExtensionNames.Contains($"Should{type.Name}")) + { + return; + } + + foreach (var member in type.GetMembers()) + { + if (member is IMethodSymbol method) + { + CollectFromMethod(method, type, ctx); + } + } + } + + private static void CollectFromMethod(IMethodSymbol method, INamedTypeSymbol container, CollectionContext ctx) + { + if (method.DeclaredAccessibility != Accessibility.Public + || !method.IsStatic + || !method.IsExtensionMethod + || method.Parameters.Length == 0) + { + return; + } + + if (method.Parameters[0].Type is not INamedTypeSymbol firstParamType + || !IsAssertionSourceInterface(firstParamType, ctx.AssertionSource)) + { + return; + } + + if (method.ReturnType is not INamedTypeSymbol returnType + || !DerivesFromAssertion(returnType, ctx.AssertionBase, out var assertionTypeArg)) + { + return; + } + + var paramData = ImmutableArray.CreateBuilder(); + var ctorCandidates = new List(); + for (var i = 1; i < method.Parameters.Length; i++) + { + var p = method.Parameters[i]; + var caeTarget = TryGetCallerArgumentExpressionTarget(p); + paramData.Add(new ParameterData( + Name: p.Name, + TypeName: p.Type.ToDisplayString(NoGlobalFormat), + HasDefaultValue: p.HasExplicitDefaultValue, + DefaultValueLiteral: p.HasExplicitDefaultValue ? FormatDefaultValue(p.ExplicitDefaultValue, p.Type) : null, + CallerArgumentExpressionTarget: caeTarget)); + if (caeTarget is null) ctorCandidates.Add(p); + } + + // Skip cross-type extensions where the source's TypeArg differs from the return-type's + // assertion TypeArg (e.g. ImplicitConversionEqualityExtensions.IsEqualTo). + // These need a Context.Map call we can't synthesize without inspecting the body. + if (!SymbolEqualityComparer.Default.Equals(firstParamType.TypeArguments[0], assertionTypeArg)) + { + return; + } + + // Find a public ctor on the return type whose param list (after the leading + // AssertionContext) matches our ctor candidates by type. + if (!HasMatchingConstructor(returnType, assertionTypeArg, ctx.AssertionContext, ctorCandidates)) + { + return; + } + + var classGenericParams = ImmutableArray.CreateBuilder(); + foreach (var tp in method.TypeParameters) + { + classGenericParams.Add(GenericParamData.From(tp, NoGlobalFormat)); + } + + var rucMessage = TryGetRucMessage(method.GetAttributes()) + ?? TryGetRucMessage(returnType.GetAttributes()) + ?? TryGetRucMessageFromConstructors(returnType); + + var suppressedTrimWarnings = CollectSuppressedTrimWarnings(method.GetAttributes()); + + var forwardedAttributes = CollectForwardedAttributes(method.GetAttributes()); + + var overrideName = TryGetShouldNameOverride(returnType, ctx.ShouldNameAttribute); + + ctx.Builder.Add(new MethodData( + ContainerName: container.Name, + MethodName: method.Name, + MethodGenericParams: new EquatableArray(classGenericParams), + SourceTypeArgDisplay: firstParamType.TypeArguments[0].ToDisplayString(NoGlobalFormat), + AssertionTypeArgDisplay: assertionTypeArg.ToDisplayString(NoGlobalFormat), + ReturnTypeFullName: returnType.ConstructedFrom.ToDisplayString(NameWithoutTypeArgsFormat), + ReturnTypeGenericArgs: new EquatableArray(returnType.TypeArguments.Select(a => a.ToDisplayString(NoGlobalFormat)).ToList()), + Parameters: new EquatableArray(paramData), + ShouldNameOverride: overrideName, + RequiresUnreferencedCodeMessage: rucMessage, + SuppressedTrimWarnings: new EquatableArray(suppressedTrimWarnings), + ForwardedAttributes: new EquatableArray(forwardedAttributes))); + } + + /// + /// Captures and + /// on the source extension method + /// so they propagate to the Should-flavored counterpart. Without forwarding, deprecating an + /// underlying assertion (IsAll) would leave the Should counterpart (BeAll) + /// undeprecated — users would see the warning on one entry but not the other. + /// + private static List CollectForwardedAttributes(ImmutableArray attrs) + { + var result = new List(); + foreach (var a in attrs) + { + var ns = a.AttributeClass?.ContainingNamespace?.ToDisplayString(); + if (a.AttributeClass?.Name == "ObsoleteAttribute" && ns == "System") + { + result.Add(FormatObsolete(a)); + } + else if (a.AttributeClass?.Name == "EditorBrowsableAttribute" && ns == "System.ComponentModel") + { + result.Add(FormatEditorBrowsable(a)); + } + } + return result; + } + + private static string FormatObsolete(AttributeData attr) + => TUnit.SourceGen.Shared.AttributeForwardingFormatters.FormatObsolete(attr, globalQualifier: "global::"); + + private static string FormatEditorBrowsable(AttributeData attr) + => TUnit.SourceGen.Shared.AttributeForwardingFormatters.FormatEditorBrowsable(attr, globalQualifier: "global::"); + + private static List CollectSuppressedTrimWarnings(ImmutableArray attrs) + { + var result = new List(); + foreach (var a in attrs) + { + if (a.AttributeClass?.Name != UnconditionalSuppressMessageAttributeName + || a.ConstructorArguments.Length < 2) + { + continue; + } + if (a.ConstructorArguments[0].Value is string category && category == "Trimming" + && a.ConstructorArguments[1].Value is string code) + { + result.Add(code); + } + } + return result; + } + + /// + /// Returns true when has a public ctor whose parameters, + /// after a leading AssertionContext<assertionTypeArg>, match + /// by type. This guards the "simple factory" template + /// against extension methods that map context or otherwise transform before construction. + /// + private static bool HasMatchingConstructor( + INamedTypeSymbol returnType, + ITypeSymbol assertionTypeArg, + INamedTypeSymbol assertionContextSymbol, + List ctorCandidates) + { + foreach (var ctor in returnType.Constructors) + { + if (ctor.DeclaredAccessibility != Accessibility.Public + || ctor.IsStatic + || ctor.Parameters.Length != ctorCandidates.Count + 1) + { + continue; + } + + var firstCtorParam = ctor.Parameters[0].Type as INamedTypeSymbol; + if (firstCtorParam is null + || !SymbolEqualityComparer.Default.Equals(firstCtorParam.OriginalDefinition, assertionContextSymbol) + || firstCtorParam.TypeArguments.Length != 1 + || !SymbolEqualityComparer.Default.Equals(firstCtorParam.TypeArguments[0], assertionTypeArg)) + { + continue; + } + + var allMatch = true; + for (var i = 0; i < ctorCandidates.Count; i++) + { + if (!SymbolEqualityComparer.Default.Equals(ctor.Parameters[i + 1].Type, ctorCandidates[i].Type)) + { + allMatch = false; + break; + } + } + if (allMatch) return true; + } + return false; + } + + private static string? TryGetShouldNameOverride(INamedTypeSymbol returnType, INamedTypeSymbol? shouldNameAttr) + { + if (shouldNameAttr is null) return null; + foreach (var attr in returnType.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, shouldNameAttr) + && attr.ConstructorArguments.Length > 0) + { + return attr.ConstructorArguments[0].Value as string; + } + } + return null; + } + + private static bool IsAssertionSourceInterface(INamedTypeSymbol type, INamedTypeSymbol assertionSource) + => type.OriginalDefinition is { } def + && SymbolEqualityComparer.Default.Equals(def, assertionSource) + && type.TypeArguments.Length == 1; + + private static bool DerivesFromAssertion(INamedTypeSymbol type, INamedTypeSymbol assertionBase, out ITypeSymbol assertionTypeArg) + { + for (var current = type; current is not null; current = current.BaseType) + { + if (current.OriginalDefinition is { } def + && SymbolEqualityComparer.Default.Equals(def, assertionBase)) + { + if (current.TypeArguments.Length != 1) + { + break; + } + assertionTypeArg = current.TypeArguments[0]; + return true; + } + } + assertionTypeArg = null!; + return false; + } + + private static string? TryGetCallerArgumentExpressionTarget(IParameterSymbol parameter) + { + foreach (var attr in parameter.GetAttributes()) + { + if (attr.AttributeClass?.Name == CallerArgumentExpressionAttributeName + && attr.ConstructorArguments.Length > 0 + && attr.ConstructorArguments[0].Value is string target) + { + return target; + } + } + return null; + } + + private static string? TryGetRucMessage(ImmutableArray attrs) + { + foreach (var a in attrs) + { + if (a.AttributeClass?.Name == RequiresUnreferencedCodeAttributeName + && a.ConstructorArguments.Length > 0) + { + return a.ConstructorArguments[0].Value as string; + } + } + return null; + } + + private static string? TryGetRucMessageFromConstructors(INamedTypeSymbol type) + { + foreach (var ctor in type.Constructors) + { + var msg = TryGetRucMessage(ctor.GetAttributes()); + if (msg is not null) return msg; + } + return null; + } + + private static string FormatDefaultValue(object? defaultValue, ITypeSymbol type) + { + if (defaultValue is null) + { + return type.IsReferenceType && type.NullableAnnotation != NullableAnnotation.Annotated + ? "default!" + : "default"; + } + + if (type.TypeKind == TypeKind.Enum && type is INamedTypeSymbol enumType) + { + foreach (var member in enumType.GetMembers()) + { + if (member is IFieldSymbol { HasConstantValue: true } field + && field.ConstantValue is not null + && field.ConstantValue.Equals(defaultValue)) + { + return $"{enumType.ToDisplayString(NoGlobalFormat)}.{field.Name}"; + } + } + return $"({enumType.ToDisplayString(NoGlobalFormat)})({defaultValue})"; + } + + // Numeric literals need their C# type suffix or they'd default-bind to int/double: + // a `float` parameter with default 1.5f would otherwise emit `= 1.5` (a double literal) + // and fail to compile. Cast through invariant culture so locales using comma decimal + // separators don't produce malformed literals like `1,5F`. + return defaultValue switch + { + string s => "\"" + s.Replace("\"", "\\\"") + "\"", + bool b => b ? "true" : "false", + char c => $"'{c}'", + float f => System.FormattableString.Invariant($"{f}F"), + double d => System.FormattableString.Invariant($"{d}D"), + decimal m => System.FormattableString.Invariant($"{m}M"), + long l => System.FormattableString.Invariant($"{l}L"), + ulong ul => System.FormattableString.Invariant($"{ul}UL"), + uint u => System.FormattableString.Invariant($"{u}U"), + _ => System.Convert.ToString(defaultValue, System.Globalization.CultureInfo.InvariantCulture) ?? "default", + }; + } + + private static void EmitWrapperPartial(SourceProductionContext ctx, WrapperData wrapper, HashSet emittedHints) + { + if (!wrapper.IsCurrentAssembly || wrapper.Methods.Length == 0) + { + return; + } + + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine(); + sb.AppendLine("using System;"); + sb.AppendLine("using System.Runtime.CompilerServices;"); + sb.AppendLine("using TUnit.Assertions.Core;"); + sb.AppendLine(); + if (!string.IsNullOrEmpty(wrapper.ContainingNamespace)) + { + sb.AppendLine($"namespace {wrapper.ContainingNamespace};"); + sb.AppendLine(); + } + + var classGenericList = wrapper.ClassGenericParams.Length > 0 + ? "<" + string.Join(", ", wrapper.ClassGenericParams.Select(p => p.Name)) + ">" + : string.Empty; + + sb.AppendLine($"partial class {wrapper.ClassName}{classGenericList}"); + sb.AppendLine("{"); + + foreach (var m in wrapper.Methods) + { + EmitWrapperMethod(sb, wrapper, m); + } + + sb.AppendLine("}"); + + var hint = $"{wrapper.ClassName}.Generated.g.cs"; + var suffix = 0; + while (!emittedHints.Add(hint)) + { + hint = $"{wrapper.ClassName}_{++suffix}.Generated.g.cs"; + } + ctx.AddSource(hint, sb.ToString()); + } + + private static void EmitWrapperMethod(StringBuilder sb, WrapperData wrapper, WrapperMethodData m) + { + var positiveName = NameConjugator.Conjugate(m.SourceMethodName); + var returnType = $"global::TUnit.Assertions.Should.Core.ShouldAssertion<{wrapper.AssertionTypeArgDisplay}>"; + + sb.AppendLine(); + if (!string.IsNullOrEmpty(m.RequiresUnreferencedCodeMessage)) + { + var escaped = m.RequiresUnreferencedCodeMessage!.Replace("\"", "\\\""); + sb.AppendLine($" [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode(\"{escaped}\")]"); + } + + sb.Append($" public {returnType} {positiveName}("); + var first = true; + foreach (var p in m.Parameters) + { + if (p.CallerArgumentExpressionTarget is not null) continue; + if (!first) sb.Append(", "); + sb.Append($"{p.TypeName} {p.Name}"); + if (p.HasDefaultValue) + { + sb.Append(" = ").Append(p.DefaultValueLiteral); + } + first = false; + } + foreach (var p in m.Parameters) + { + if (p.CallerArgumentExpressionTarget is null) continue; + if (!first) sb.Append(", "); + sb.Append($"[global::System.Runtime.CompilerServices.CallerArgumentExpression(\"{p.CallerArgumentExpressionTarget}\")] string? {p.Name} = null"); + first = false; + } + sb.AppendLine(")"); + sb.AppendLine(" {"); + sb.AppendLine($" Context.ExpressionBuilder.Append(\".{positiveName}(\");"); + + var caeParams = m.Parameters.Where(p => p.CallerArgumentExpressionTarget is not null).ToArray(); + if (caeParams.Length == 1) + { + sb.AppendLine($" Context.ExpressionBuilder.Append({caeParams[0].Name});"); + } + else if (caeParams.Length > 1) + { + sb.AppendLine(" var __added = false;"); + foreach (var p in caeParams) + { + sb.AppendLine($" if ({p.Name} is not null)"); + sb.AppendLine(" {"); + sb.AppendLine(" if (__added) Context.ExpressionBuilder.Append(\", \");"); + sb.AppendLine($" Context.ExpressionBuilder.Append({p.Name});"); + sb.AppendLine(" __added = true;"); + sb.AppendLine(" }"); + } + } + sb.AppendLine(" Context.ExpressionBuilder.Append(\")\");"); + + var ctorArgs = new List { "Context" }; + ctorArgs.AddRange(m.Parameters.Where(p => p.CallerArgumentExpressionTarget is null).Select(p => p.Name)); + + sb.AppendLine($" var inner = new global::{m.ReturnTypeFullName}{FormatGenericArgs(m.ReturnTypeGenericArgs)}({string.Join(", ", ctorArgs)});"); + sb.AppendLine($" var __tunit_should_because = ((global::TUnit.Assertions.Should.Core.IShouldSource<{wrapper.AssertionTypeArgDisplay}>)this).ConsumeBecauseMessage();"); + sb.AppendLine(" if (__tunit_should_because is not null)"); + sb.AppendLine(" {"); + sb.AppendLine(" inner.Because(__tunit_should_because);"); + sb.AppendLine(" }"); + sb.AppendLine($" return new global::TUnit.Assertions.Should.Core.ShouldAssertion<{wrapper.AssertionTypeArgDisplay}>(Context, inner);"); + sb.AppendLine(" }"); + } + + private static void EmitContainer(SourceProductionContext ctx, string containerName, MethodData[] methods, HashSet emittedHints) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine(); + sb.AppendLine("using System;"); + sb.AppendLine("using System.Runtime.CompilerServices;"); + sb.AppendLine("using TUnit.Assertions.Core;"); + sb.AppendLine("using TUnit.Assertions.Should.Core;"); + sb.AppendLine(); + sb.AppendLine("namespace TUnit.Assertions.Should.Extensions;"); + sb.AppendLine(); + + var className = "Should" + containerName; + sb.AppendLine($"public static partial class {className}"); + sb.AppendLine("{"); + + foreach (var m in methods) + { + EmitMethod(sb, m); + } + + sb.AppendLine("}"); + + var hint = className + ".g.cs"; + var suffix = 0; + while (!emittedHints.Add(hint)) + { + hint = $"{className}_{++suffix}.g.cs"; + } + ctx.AddSource(hint, sb.ToString()); + } + + private static void EmitMethod(StringBuilder sb, MethodData m) + { + var positiveName = m.ShouldNameOverride ?? NameConjugator.Conjugate(m.MethodName); + + var genericList = m.MethodGenericParams.Length > 0 + ? "<" + string.Join(", ", m.MethodGenericParams.Select(p => + p.DynamicallyAccessedMembersAttribute is null + ? p.Name + : $"{p.DynamicallyAccessedMembersAttribute} {p.Name}")) + ">" + : string.Empty; + + var constraints = string.Join(" ", m.MethodGenericParams + .Select(p => p.ConstraintClause) + .Where(c => c is not null)); + + var sourceType = $"global::TUnit.Assertions.Should.Core.IShouldSource<{m.SourceTypeArgDisplay}>"; + var returnType = $"global::TUnit.Assertions.Should.Core.ShouldAssertion<{m.AssertionTypeArgDisplay}>"; + + sb.AppendLine(); + if (!string.IsNullOrEmpty(m.RequiresUnreferencedCodeMessage)) + { + var escaped = m.RequiresUnreferencedCodeMessage!.Replace("\"", "\\\""); + sb.AppendLine($" [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode(\"{escaped}\")]"); + } + foreach (var code in m.SuppressedTrimWarnings) + { + sb.AppendLine($" [global::System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage(\"Trimming\", \"{code}\", Justification = \"Forwarded from source method\")]"); + } + foreach (var attr in m.ForwardedAttributes) + { + sb.AppendLine($" {attr}"); + } + + sb.Append($" public static {returnType} {positiveName}{genericList}(this {sourceType} source"); + foreach (var p in m.Parameters) + { + if (p.CallerArgumentExpressionTarget is not null) continue; + sb.Append($", {p.TypeName} {p.Name}"); + if (p.HasDefaultValue) + { + sb.Append(" = ").Append(p.DefaultValueLiteral); + } + } + foreach (var p in m.Parameters) + { + if (p.CallerArgumentExpressionTarget is null) continue; + sb.Append($", [global::System.Runtime.CompilerServices.CallerArgumentExpression(\"{p.CallerArgumentExpressionTarget}\")] string? {p.Name} = null"); + } + sb.Append(')'); + + if (!string.IsNullOrEmpty(constraints)) + { + sb.AppendLine(); + sb.Append(" ").Append(constraints); + } + + sb.AppendLine(); + sb.AppendLine(" {"); + sb.AppendLine(" var innerContext = source.Context;"); + sb.AppendLine($" innerContext.ExpressionBuilder.Append(\".{positiveName}(\");"); + + var caeParams = m.Parameters.Where(p => p.CallerArgumentExpressionTarget is not null).ToArray(); + if (caeParams.Length == 1) + { + sb.AppendLine($" innerContext.ExpressionBuilder.Append({caeParams[0].Name});"); + } + else if (caeParams.Length > 1) + { + sb.AppendLine(" var __added = false;"); + foreach (var p in caeParams) + { + sb.AppendLine($" if ({p.Name} is not null)"); + sb.AppendLine(" {"); + sb.AppendLine(" if (__added) innerContext.ExpressionBuilder.Append(\", \");"); + sb.AppendLine($" innerContext.ExpressionBuilder.Append({p.Name});"); + sb.AppendLine(" __added = true;"); + sb.AppendLine(" }"); + } + } + sb.AppendLine(" innerContext.ExpressionBuilder.Append(\")\");"); + + var ctorArgs = new List { "innerContext" }; + ctorArgs.AddRange(m.Parameters.Where(p => p.CallerArgumentExpressionTarget is null).Select(p => p.Name)); + + sb.AppendLine($" var inner = new global::{m.ReturnTypeFullName}{FormatGenericArgs(m.ReturnTypeGenericArgs)}({string.Join(", ", ctorArgs)});"); + sb.AppendLine(" var __tunit_should_because = source.ConsumeBecauseMessage();"); + sb.AppendLine(" if (__tunit_should_because is not null)"); + sb.AppendLine(" {"); + sb.AppendLine(" inner.Because(__tunit_should_because);"); + sb.AppendLine(" }"); + sb.AppendLine($" return new global::TUnit.Assertions.Should.Core.ShouldAssertion<{m.AssertionTypeArgDisplay}>(innerContext, inner);"); + sb.AppendLine(" }"); + } + + private static string FormatGenericArgs(EquatableArray args) + => args.Length == 0 ? string.Empty : "<" + string.Join(", ", args) + ">"; + + private sealed record GeneratorPayload( + EquatableArray Methods, + EquatableArray Wrappers); + + private sealed record WrapperData( + string ContainingNamespace, + string ClassName, + EquatableArray ClassGenericParams, + string ClassGenericSuffix, + string AssertionTypeArgDisplay, + EquatableArray Methods, + bool IsCurrentAssembly); + + private sealed record WrapperMethodData( + string SourceMethodName, + EquatableArray Parameters, + string ReturnTypeFullName, + EquatableArray ReturnTypeGenericArgs, + string? RequiresUnreferencedCodeMessage); + + /// + /// Mutable bag of pre-resolved Roslyn symbols and the in-flight + /// builder, threaded through the namespace walk. Not a record — it doesn't flow through + /// the incremental pipeline as a cache key, and embeds a mutable builder. + /// + private sealed class CollectionContext + { + public CollectionContext( + Compilation compilation, + INamedTypeSymbol assertionSource, + INamedTypeSymbol assertionBase, + INamedTypeSymbol assertionContext, + INamedTypeSymbol? shouldNameAttribute, + HashSet alreadyBakedShouldExtensionNames, + ImmutableArray.Builder builder) + { + Compilation = compilation; + AssertionSource = assertionSource; + AssertionBase = assertionBase; + AssertionContext = assertionContext; + ShouldNameAttribute = shouldNameAttribute; + AlreadyBakedShouldExtensionNames = alreadyBakedShouldExtensionNames; + Builder = builder; + } + + public Compilation Compilation { get; } + public INamedTypeSymbol AssertionSource { get; } + public INamedTypeSymbol AssertionBase { get; } + public INamedTypeSymbol AssertionContext { get; } + public INamedTypeSymbol? ShouldNameAttribute { get; } + public HashSet AlreadyBakedShouldExtensionNames { get; } + public ImmutableArray.Builder Builder { get; } + } + + private sealed record MethodData( + string ContainerName, + string MethodName, + EquatableArray MethodGenericParams, + string SourceTypeArgDisplay, + string AssertionTypeArgDisplay, + string ReturnTypeFullName, + EquatableArray ReturnTypeGenericArgs, + EquatableArray Parameters, + string? ShouldNameOverride, + string? RequiresUnreferencedCodeMessage, + EquatableArray SuppressedTrimWarnings, + EquatableArray ForwardedAttributes); + + private sealed record ParameterData( + string Name, + string TypeName, + bool HasDefaultValue, + string? DefaultValueLiteral, + string? CallerArgumentExpressionTarget); + + private sealed record GenericParamData(string Name, string? ConstraintClause, string? DynamicallyAccessedMembersAttribute) + { + public static GenericParamData From(ITypeParameterSymbol tp, SymbolDisplayFormat format) + { + var constraints = new List(); + if (tp.HasReferenceTypeConstraint) constraints.Add("class"); + if (tp.HasValueTypeConstraint) constraints.Add("struct"); + if (tp.HasNotNullConstraint) constraints.Add("notnull"); + foreach (var ct in tp.ConstraintTypes) + { + constraints.Add(ct.ToDisplayString(format)); + } + if (tp.HasConstructorConstraint) constraints.Add("new()"); + + string? damAttr = null; + foreach (var attr in tp.GetAttributes()) + { + if (attr.AttributeClass?.Name != DynamicallyAccessedMembersAttributeName + || attr.ConstructorArguments.Length == 0) + { + continue; + } + var ctorArg = attr.ConstructorArguments[0]; + if (ctorArg.Type is INamedTypeSymbol enumType && ctorArg.Value is int intValue) + { + damAttr = $"[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(({enumType.ToDisplayString(format)}){intValue})]"; + } + break; + } + + return new GenericParamData( + tp.Name, + constraints.Count > 0 ? $"where {tp.Name} : {string.Join(", ", constraints)}" : null, + damAttr); + } + } +} diff --git a/TUnit.Assertions.Should.SourceGenerator/TUnit.Assertions.Should.SourceGenerator.csproj b/TUnit.Assertions.Should.SourceGenerator/TUnit.Assertions.Should.SourceGenerator.csproj new file mode 100644 index 0000000000..3c06e84a78 --- /dev/null +++ b/TUnit.Assertions.Should.SourceGenerator/TUnit.Assertions.Should.SourceGenerator.csproj @@ -0,0 +1,39 @@ + + + + + + netstandard2.0 + enable + latest + true + true + TUnit.Assertions.Should.SourceGenerator + TUnit.Assertions.Should.SourceGenerator + false + false + + + + + + + + + + <_Parameter1>TUnit.Assertions.Should.SourceGenerator.Tests + + + + + + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/TUnit.Assertions.Should.Tests/AssertMultipleTests.cs b/TUnit.Assertions.Should.Tests/AssertMultipleTests.cs new file mode 100644 index 0000000000..5cb64ecb05 --- /dev/null +++ b/TUnit.Assertions.Should.Tests/AssertMultipleTests.cs @@ -0,0 +1,48 @@ +using TUnit.Assertions.Exceptions; +using TUnit.Assertions.Should; +using TUnit.Assertions.Should.Extensions; + +namespace TUnit.Assertions.Should.Tests; + +public class AssertMultipleTests +{ + [Test] + public async Task All_pass_inside_Multiple() + { + using (Assert.Multiple()) + { + await 5.Should().BeEqualTo(5); + await "hello".Should().BeEqualTo("hello"); + await new[] { 1, 2 }.Should().Contain(1); + } + } + + [Test] + public async Task Multiple_failures_aggregate() + { + var ex = await Assert.That(async () => + { + using (Assert.Multiple()) + { + await 5.Should().BeEqualTo(99); + await "hello".Should().BeEqualTo("world"); + } + }).Throws(); + + // Both failures should appear in the aggregated message. + await Assert.That(ex.Message).Contains("BeEqualTo"); + } + + [Test] + public async Task Single_failure_in_Multiple_still_throws() + { + await Assert.That(async () => + { + using (Assert.Multiple()) + { + await 5.Should().BeEqualTo(5); + await 5.Should().BeEqualTo(99); + } + }).Throws(); + } +} diff --git a/TUnit.Assertions.Should.Tests/BasicShouldTests.cs b/TUnit.Assertions.Should.Tests/BasicShouldTests.cs new file mode 100644 index 0000000000..f229d546be --- /dev/null +++ b/TUnit.Assertions.Should.Tests/BasicShouldTests.cs @@ -0,0 +1,127 @@ +using TUnit.Assertions.Should; +using TUnit.Assertions.Should.Extensions; + +namespace TUnit.Assertions.Should.Tests; + +public class BasicShouldTests +{ + [Test] + public async Task Value_BeEqualTo_passes() + { + await 42.Should().BeEqualTo(42); + } + + [Test] + public async Task Value_NotBeEqualTo_passes() + { + await 42.Should().NotBeEqualTo(7); + } + + [Test] + public async Task Value_BeEqualTo_fails_with_message() + { + var ex = await Assert.That(async () => await 42.Should().BeEqualTo(7)) + .Throws(); + await Assert.That(ex.Message).Contains("BeEqualTo"); + } + + [Test] + public async Task String_BeEqualTo_works() + { + await "hello".Should().BeEqualTo("hello"); + } + + [Test] + public async Task String_Contain_works() + { + await "hello world".Should().Contain("world"); + } + + [Test] + public async Task String_StartWith_works() + { + await "hello world".Should().StartWith("hello"); + } + + [Test] + public async Task Collection_Contain_works() + { + var list = new List { 1, 2, 3 }; + await list.Should().Contain(2); + } + + [Test] + public async Task Chain_And_works() + { + await 42.Should().BeEqualTo(42).And.NotBeEqualTo(7); + } + + [Test] + public async Task Chain_Or_works() + { + await 42.Should().BeEqualTo(7).Or.BeEqualTo(42); + } + + [Test] + public async Task Default_BeDefault_works() + { + int value = 0; + await value.Should().BeDefault(); + } + + [Test] + public async Task NotDefault_NotBeDefault_works() + { + int value = 42; + await value.Should().NotBeDefault(); + } + + [Test] + public async Task Action_Throw_works() + { + Action act = () => throw new InvalidOperationException("boom"); + await act.Should().Throw(); + } + + [Test] + public async Task Action_ThrowExactly_works() + { + Action act = () => throw new InvalidOperationException("boom"); + await act.Should().ThrowExactly(); + } + + [Test] + public async Task FuncTask_Throw_works() + { + Func act = () => throw new InvalidOperationException("boom"); + await act.Should().Throw(); + } + + [Test] + public async Task FuncResult_does_not_throw() + { + Func f = () => 42; + await f.Should().NotBeEqualTo(7); + } + + [Test] + public async Task List_Contain_infers_element_type_without_cast() + { + var list = new List { 1, 2, 3 }; + await list.Should().Contain(2); + } + + [Test] + public async Task ReadOnlyList_Contain_infers_element_type_without_cast() + { + IReadOnlyList list = new[] { "a", "b", "c" }; + await list.Should().Contain("b"); + } + + [Test] + public async Task Array_Contain_infers_element_type_without_cast() + { + var arr = new[] { 1, 2, 3 }; + await arr.Should().Contain(2); + } +} diff --git a/TUnit.Assertions.Should.Tests/ChainingTests.cs b/TUnit.Assertions.Should.Tests/ChainingTests.cs new file mode 100644 index 0000000000..777c2e005f --- /dev/null +++ b/TUnit.Assertions.Should.Tests/ChainingTests.cs @@ -0,0 +1,96 @@ +using TUnit.Assertions.Exceptions; +using TUnit.Assertions.Should; +using TUnit.Assertions.Should.Extensions; + +namespace TUnit.Assertions.Should.Tests; + +public class ChainingTests +{ + [Test] + public async Task And_two_passes() + { + await 5.Should().BeEqualTo(5).And.NotBeEqualTo(7); + } + + [Test] + public async Task And_three_passes() + { + await 5.Should().BeEqualTo(5).And.NotBeEqualTo(7).And.BeBetween(1, 10); + } + + [Test] + public async Task And_first_fails_throws() + { + await Assert.That(async () => await 5.Should().BeEqualTo(99).And.NotBeEqualTo(7)) + .Throws(); + } + + [Test] + public async Task And_second_fails_throws() + { + await Assert.That(async () => await 5.Should().BeEqualTo(5).And.BeEqualTo(99)) + .Throws(); + } + + [Test] + public async Task Or_first_passes_short_circuits() + { + await 5.Should().BeEqualTo(5).Or.BeEqualTo(99); + } + + [Test] + public async Task Or_second_passes() + { + await 5.Should().BeEqualTo(99).Or.BeEqualTo(5); + } + + [Test] + public async Task Or_both_fail_throws() + { + await Assert.That(async () => await 5.Should().BeEqualTo(99).Or.BeEqualTo(100)) + .Throws(); + } + + [Test] + public async Task Mixed_And_Or_throws() + { + await Assert.That(async () => await 5.Should().BeEqualTo(5).And.BeEqualTo(5).Or.BeEqualTo(7)) + .Throws(); + } + + [Test] + public async Task Chain_keeps_Should_naming_throughout() + { + // After .And the source is ShouldContinuation: IShouldSource; only Should-flavored + // extensions should resolve. + var list = new List { 1, 2, 3 }; + await list.Should().Contain(1).And.Contain(2).And.NotContain(99); + } + + [Test] + public async Task Because_propagates_to_failure_message() + { + var ex = await Assert.That(async () => + await 5.Should().BeEqualTo(99).Because("business rule X")) + .Throws(); + await Assert.That(ex.Message).Contains("business rule X"); + } + + [Test] + public async Task Pre_chain_Because_propagates_to_failure_message() + { + var ex = await Assert.That(async () => + await 5.Should().Because("business rule Y").BeEqualTo(99)) + .Throws(); + await Assert.That(ex.Message).Contains("business rule Y"); + } + + [Test] + public async Task Continuation_Because_propagates_to_next_assertion() + { + var ex = await Assert.That(async () => + await 5.Should().BeEqualTo(5).And.Because("business rule Z").BeEqualTo(99)) + .Throws(); + await Assert.That(ex.Message).Contains("business rule Z"); + } +} diff --git a/TUnit.Assertions.Should.Tests/CollectionTests.cs b/TUnit.Assertions.Should.Tests/CollectionTests.cs new file mode 100644 index 0000000000..8c1e215950 --- /dev/null +++ b/TUnit.Assertions.Should.Tests/CollectionTests.cs @@ -0,0 +1,132 @@ +using TUnit.Assertions.Should; +using TUnit.Assertions.Should.Extensions; + +namespace TUnit.Assertions.Should.Tests; + +public class CollectionTests +{ + [Test] + public async Task List_Contain() + { + var list = new List { 1, 2, 3 }; + await list.Should().Contain(2); + } + + [Test] + public async Task List_NotContain() + { + var list = new List { 1, 2, 3 }; + await list.Should().NotContain(99); + } + + [Test] + public async Task IReadOnlyList_Contain_infers_element_type() + { + IReadOnlyList list = new[] { "a", "b", "c" }; + await list.Should().Contain("b"); + } + + [Test] + public async Task Array_Contain() + { + var arr = new[] { 1, 2, 3 }; + await arr.Should().Contain(2); + } + + [Test] + public async Task IEnumerable_Contain() + { + IEnumerable seq = Enumerable.Range(1, 5); + await seq.Should().Contain(3); + } + + [Test] + public async Task BeInOrder() + { + var list = new List { 1, 2, 3, 4, 5 }; + await list.Should().BeInOrder(); + } + + [Test] + public async Task BeInDescendingOrder() + { + var list = new List { 5, 4, 3, 2, 1 }; + await list.Should().BeInDescendingOrder(); + } + + [Test] + public async Task All_predicate() + { + var list = new List { 2, 4, 6 }; + await list.Should().All(x => x % 2 == 0); + } + + [Test] + public async Task Any_predicate() + { + var list = new List { 1, 2, 3 }; + await list.Should().Any(x => x > 2); + } + + [Test] + public async Task HaveSingleItem() + { + var list = new List { 42 }; + await list.Should().HaveSingleItem(); + } + + [Test] + public async Task HaveSingleItem_with_predicate() + { + var list = new List { 1, 2, 3 }; + await list.Should().HaveSingleItem(x => x == 2); + } + + [Test] + public async Task HaveDistinctItems() + { + var list = new List { 1, 2, 3 }; + await list.Should().HaveDistinctItems(); + } + + [Test] + public async Task HaveCount() + { + var list = new List { 1, 2, 3 }; + await list.Should().HaveCount(3); + } + + [Test] + public async Task Contain_predicate() + { + var list = new List { 1, 2, 3 }; + await list.Should().Contain(x => x > 2); + } + + [Test] + public async Task Empty_list_NotContain() + { + var list = new List(); + await list.Should().NotContain(1); + } + + [Test] + public async Task Failure_message_contains_collection_method() + { + var list = new List { 1, 2, 3 }; + var ex = await Assert.That(async () => await list.Should().Contain(99)) + .Throws(); + await Assert.That(ex.Message).Contains("Contain"); + } + + [Test] + public async Task Failure_message_uses_Should_flavored_expression() + { + var list = new List { 1, 2, 3 }; + var ex = await Assert.That(async () => await list.Should().Contain(99)) + .Throws(); + // Collection entry must mirror the value entry's expression style — ".Should().Method(...)" + // rather than the underlying CollectionAssertion's "Assert.That(...)" form. + await Assert.That(ex.Message).Contains(".Should().Contain("); + } +} diff --git a/TUnit.Assertions.Should.Tests/DateTimeTests.cs b/TUnit.Assertions.Should.Tests/DateTimeTests.cs new file mode 100644 index 0000000000..88e944208c --- /dev/null +++ b/TUnit.Assertions.Should.Tests/DateTimeTests.cs @@ -0,0 +1,49 @@ +using TUnit.Assertions.Should; +using TUnit.Assertions.Should.Extensions; + +namespace TUnit.Assertions.Should.Tests; + +public class DateTimeTests +{ + [Test] + public async Task DateTime_BeEqualTo() + { + var dt = new DateTime(2026, 4, 28, 12, 0, 0); + await dt.Should().BeEqualTo(dt); + } + + [Test] + public async Task DateTime_EqualExact() + { + var dt = new DateTime(2026, 4, 28, 12, 0, 0); + await dt.Should().EqualExact(dt); + } + + [Test] + public async Task DateTimeOffset_BeEqualTo() + { + var dto = DateTimeOffset.Parse("2026-04-28T12:00:00+00:00"); + await dto.Should().BeEqualTo(dto); + } + + [Test] + public async Task DateOnly_BeEqualTo() + { + var date = new DateOnly(2026, 4, 28); + await date.Should().BeEqualTo(date); + } + + [Test] + public async Task TimeOnly_BeEqualTo() + { + var time = new TimeOnly(12, 0, 0); + await time.Should().BeEqualTo(time); + } + + [Test] + public async Task TimeSpan_BeEqualTo() + { + var span = TimeSpan.FromMinutes(5); + await span.Should().BeEqualTo(span); + } +} diff --git a/TUnit.Assertions.Should.Tests/DelegateTests.cs b/TUnit.Assertions.Should.Tests/DelegateTests.cs new file mode 100644 index 0000000000..54632b2188 --- /dev/null +++ b/TUnit.Assertions.Should.Tests/DelegateTests.cs @@ -0,0 +1,88 @@ +using TUnit.Assertions.Exceptions; +using TUnit.Assertions.Should; +using TUnit.Assertions.Should.Extensions; + +namespace TUnit.Assertions.Should.Tests; + +public class DelegateTests +{ + [Test] + public async Task Action_Throw_subclass_passes() + { + Action act = () => throw new ArgumentNullException(); + await act.Should().Throw(); + } + + [Test] + public async Task Action_ThrowExactly_subclass_fails() + { + Action act = () => throw new ArgumentNullException(); + await Assert.That(async () => await act.Should().ThrowExactly()) + .Throws(); + } + + [Test] + public async Task Action_ThrowExactly_exact_match_passes() + { + Action act = () => throw new InvalidOperationException(); + await act.Should().ThrowExactly(); + } + + [Test] + public async Task Action_no_exception_Throw_fails() + { + Action act = () => { }; + await Assert.That(async () => await act.Should().Throw()) + .Throws(); + } + + [Test] + public async Task FuncTask_Throw() + { + Func act = () => throw new InvalidOperationException("boom"); + await act.Should().Throw(); + } + + [Test] + public async Task FuncTaskT_NotThrow_returns_value() + { + Func> f = () => Task.FromResult(42); + await f.Should().BeEqualTo(42); + } + + [Test] + public async Task FuncT_evaluates_only_once() + { + var count = 0; + Func f = () => { count++; return 42; }; + await f.Should().BeEqualTo(42).And.NotBeEqualTo(99); + await Assert.That(count).IsEqualTo(1); + } + + [Test] + public async Task Throw_failure_message_contains_method_name() + { + Action act = () => { }; + var ex = await Assert.That(async () => await act.Should().Throw()) + .Throws(); + await Assert.That(ex.Message).Contains(".Should().Throw<"); + } + + [Test] + public async Task Throw_call_site_renders_generic_exception_type_in_expression() + { + Action act = () => { }; + var ex = await Assert.That(async () => await act.Should().Throw>()) + .Throws(); + // Type.Name on a generic returns "MyGenericException`1". The Should layer's expression + // formatter strips the backtick-arity suffix and recurses into generic arguments so + // the call-site portion of the failure message reads "Throw>()" + // rather than the mangled "Throw()". + await Assert.That(ex.Message).Contains(".Throw>()"); + } + + private sealed class MyGenericException : Exception + { + public MyGenericException() : base("test") { } + } +} diff --git a/TUnit.Assertions.Should.Tests/FailureTests.cs b/TUnit.Assertions.Should.Tests/FailureTests.cs new file mode 100644 index 0000000000..f7ea83389a --- /dev/null +++ b/TUnit.Assertions.Should.Tests/FailureTests.cs @@ -0,0 +1,148 @@ +using TUnit.Assertions.Exceptions; +using TUnit.Assertions.Should; +using TUnit.Assertions.Should.Extensions; + +namespace TUnit.Assertions.Should.Tests; + +/// +/// Focused failure-path coverage for the Should surface. These tests intentionally overlap +/// the passing-path feature tests so a broken Should adapter cannot accidentally keep only +/// happy-path coverage green. +/// +public class FailureTests +{ + [Test] + public async Task Scalar_generated_extension_failure_throws_assertion_exception() + { + await Assert.That(async () => await 42.Should().BeEqualTo(7)) + .Throws(); + } + + [Test] + public async Task Boolean_generated_extension_failure_throws_assertion_exception() + { + await Assert.That(async () => await false.Should().BeTrue()) + .Throws(); + } + + [Test] + public async Task DateTime_specialized_extension_failure_throws_assertion_exception() + { + var value = new DateTime(2026, 4, 29, 12, 0, 0); + var expected = value.AddDays(1); + + await Assert.That(async () => await value.Should().BeEqualTo(expected)) + .Throws(); + } + + [Test] + public async Task String_generated_extension_failure_throws_assertion_exception() + { + await Assert.That(async () => await "hello".Should().Contain("missing")) + .Throws(); + } + + [Test] + public async Task Collection_generated_extension_failure_throws_assertion_exception() + { + var values = new[] { 1, 2, 3 }; + await Assert.That(async () => await values.Should().Contain(99)) + .Throws(); + } + + [Test] + public async Task Collection_instance_method_failure_throws_assertion_exception() + { + var values = new[] { 3, 2, 1 }; + await Assert.That(async () => await values.Should().BeInOrder()) + .Throws(); + } + + [Test] + public async Task Collection_predicate_instance_method_failure_throws_assertion_exception() + { + var values = new[] { 1, 2, 3 }; + await Assert.That(async () => await values.Should().All(x => x > 1)) + .Throws(); + } + + [Test] + public async Task Task_void_returning_value_failure_throws_assertion_exception() + { + await Assert.That(async () => await Task.CompletedTask.Should().BeNull()) + .Throws(); + } + + [Test] + public async Task Task_value_returning_value_failure_throws_assertion_exception() + { + var task = Task.FromResult(42); + + await Assert.That(async () => await task.Should().BeNull()) + .Throws(); + } + + [Test] + public async Task ValueTask_void_returning_value_failure_throws_assertion_exception() + { + await Assert.That(async () => await ValueTask.CompletedTask.Should().BeNull()) + .Throws(); + } + + [Test] + public async Task Action_void_returning_delegate_failure_throws_assertion_exception() + { + Action action = () => { }; + + await Assert.That(async () => await action.Should().Throw()) + .Throws(); + } + + [Test] + public async Task Func_value_returning_delegate_failure_throws_assertion_exception() + { + Func func = () => 42; + + await Assert.That(async () => await func.Should().BeEqualTo(7)) + .Throws(); + } + + [Test] + public async Task Async_void_returning_delegate_failure_throws_assertion_exception() + { + Func action = () => Task.CompletedTask; + + await Assert.That(async () => await action.Should().Throw()) + .Throws(); + } + + [Test] + public async Task Async_value_returning_delegate_failure_throws_assertion_exception() + { + Func> func = () => Task.FromResult(42); + + await Assert.That(async () => await func.Should().BeEqualTo(7)) + .Throws(); + } + + [Test] + public async Task GenerateAssertion_backed_failure_throws_assertion_exception() + { + await Assert.That(async () => await 5.Should().BeZero()) + .Throws(); + } + + [Test] + public async Task User_defined_assertion_failure_throws_assertion_exception() + { + await Assert.That(async () => await 2.Should().BeOdd()) + .Throws(); + } + + [Test] + public async Task Chained_assertion_failure_throws_assertion_exception() + { + await Assert.That(async () => await 5.Should().BeEqualTo(5).And.BeEqualTo(99)) + .Throws(); + } +} diff --git a/TUnit.Assertions.Should.Tests/GenerateAssertionTests.cs b/TUnit.Assertions.Should.Tests/GenerateAssertionTests.cs new file mode 100644 index 0000000000..fee83032eb --- /dev/null +++ b/TUnit.Assertions.Should.Tests/GenerateAssertionTests.cs @@ -0,0 +1,70 @@ +using TUnit.Assertions.Should; +using TUnit.Assertions.Should.Extensions; + +namespace TUnit.Assertions.Should.Tests; + +/// +/// Verifies the generator picks up assertions produced by [GenerateAssertion] +/// (e.g., IsZero, IsEven, IsOdd on int) — these don't carry an +/// [AssertionExtension] attribute on the underlying assertion class, so they need +/// the extension-method-scanning code path to be discovered. +/// +public class GenerateAssertionTests +{ + [Test] + public async Task Int_BeZero() + { + await 0.Should().BeZero(); + } + + [Test] + public async Task Int_NotBeZero() + { + await 5.Should().NotBeZero(); + } + + [Test] + public async Task Int_BeEven() + { + await 4.Should().BeEven(); + } + + [Test] + public async Task Int_BeOdd() + { + await 3.Should().BeOdd(); + } + + [Test] + public async Task Double_BeZero() + { + await 0.0.Should().BeZero(); + } + + [Test] + public async Task Bool_BeTrue_via_GenerateAssertion() + { + // BoolAssertions exposes IsTrue/IsFalse via [GenerateAssertion(InlineMethodBody = true)]. + await true.Should().BeTrue(); + } + + [Test] + public async Task Bool_BeFalse_via_GenerateAssertion() + { + await false.Should().BeFalse(); + } + + [Test] + public async Task Range_BeFullRange() + { + await Range.All.Should().BeFullRange(); + } + + [Test] + public async Task BeZero_failure_reports_method_name() + { + var ex = await Assert.That(async () => await 5.Should().BeZero()) + .Throws(); + await Assert.That(ex.Message).Contains(".BeZero()"); + } +} diff --git a/TUnit.Assertions.Should.Tests/InferenceTests.cs b/TUnit.Assertions.Should.Tests/InferenceTests.cs new file mode 100644 index 0000000000..2db49dba16 --- /dev/null +++ b/TUnit.Assertions.Should.Tests/InferenceTests.cs @@ -0,0 +1,58 @@ +using TUnit.Assertions.Should; +using TUnit.Assertions.Should.Extensions; + +namespace TUnit.Assertions.Should.Tests; + +/// +/// Verifies that element-type inference works without any explicit generic type arguments. +/// If any of these tests require a manual cast or explicit .Should<T>(), the generator +/// is missing covariance handling for that source type. +/// +public class InferenceTests +{ + [Test] + public async Task List_int_Contain_no_explicit_generic() + { + var list = new List { 1, 2, 3 }; + await list.Should().Contain(2); + } + + [Test] + public async Task ReadOnlyCollection_string_Contain_no_explicit_generic() + { + IReadOnlyCollection coll = new[] { "a", "b", "c" }; + await coll.Should().Contain("b"); + } + + [Test] + public async Task HashSet_Contain_no_explicit_generic() + { + var set = new HashSet { 1, 2, 3 }; + await set.Should().Contain(2); + } + + [Test] + public async Task Stack_Contain_no_explicit_generic() + { + var stack = new Stack(); + stack.Push(1); + stack.Push(2); + await stack.Should().Contain(1); + } + + [Test] + public async Task Queue_Contain_no_explicit_generic() + { + var queue = new Queue(); + queue.Enqueue("a"); + queue.Enqueue("b"); + await queue.Should().Contain("a"); + } + + [Test] + public async Task Custom_collection_Contain_no_explicit_generic() + { + IEnumerable dates = new[] { DateTime.MinValue, DateTime.MaxValue }; + await dates.Should().Contain(DateTime.MinValue); + } +} diff --git a/TUnit.Assertions.Should.Tests/NullSourceTests.cs b/TUnit.Assertions.Should.Tests/NullSourceTests.cs new file mode 100644 index 0000000000..7fd793e419 --- /dev/null +++ b/TUnit.Assertions.Should.Tests/NullSourceTests.cs @@ -0,0 +1,59 @@ +using TUnit.Assertions.Exceptions; +using TUnit.Assertions.Should; +using TUnit.Assertions.Should.Extensions; + +namespace TUnit.Assertions.Should.Tests; + +/// +/// Verifies null sources produce a meaningful assertion failure rather than an NRE. +/// +public class NullSourceTests +{ + [Test] + public async Task Null_string_BeEqualTo_fails_with_assertion_exception() + { + string? value = null; + await Assert.That(async () => await value.Should().BeEqualTo("expected")) + .Throws(); + } + + [Test] + public async Task Null_collection_Contain_fails_with_assertion_exception() + { + List? list = null; + await Assert.That(async () => await list.Should().Contain(1)) + .Throws(); + } + + [Test] + public async Task Null_collection_BeInOrder_fails_with_assertion_exception() + { + List? list = null; + await Assert.That(async () => await list.Should().BeInOrder()) + .Throws(); + } + + [Test] + public async Task Null_collection_All_predicate_fails_with_assertion_exception() + { + List? list = null; + await Assert.That(async () => await list.Should().All(x => x > 0)) + .Throws(); + } + + [Test] + public async Task Null_collection_Any_predicate_fails_with_assertion_exception() + { + List? list = null; + await Assert.That(async () => await list.Should().Any(x => x > 0)) + .Throws(); + } + + [Test] + public async Task Null_collection_HaveSingleItem_predicate_fails_with_assertion_exception() + { + List? list = null; + await Assert.That(async () => await list.Should().HaveSingleItem(x => x > 0)) + .Throws(); + } +} diff --git a/TUnit.Assertions.Should.Tests/NumericTests.cs b/TUnit.Assertions.Should.Tests/NumericTests.cs new file mode 100644 index 0000000000..33a5334759 --- /dev/null +++ b/TUnit.Assertions.Should.Tests/NumericTests.cs @@ -0,0 +1,75 @@ +using TUnit.Assertions.Should; +using TUnit.Assertions.Should.Extensions; + +namespace TUnit.Assertions.Should.Tests; + +public class NumericTests +{ + [Test] + public async Task Int_BeBetween() + { + await 5.Should().BeBetween(1, 10); + } + + [Test] + public async Task Int_BeCloseTo_within_tolerance() + { + await 100.Should().BeCloseTo(102, 5); + } + + [Test] + public async Task Int_BeWithinPercentOf() + { + await 105.Should().BeWithinPercentOf(100, 10); + } + + [Test] + public async Task Long_BeEqualTo() + { + await 12345678901234L.Should().BeEqualTo(12345678901234L); + } + + [Test] + public async Task Decimal_BeEqualTo() + { + await 3.14m.Should().BeEqualTo(3.14m); + } + + [Test] + public async Task Decimal_BeCloseTo() + { + await 3.14m.Should().BeCloseTo(3.15m, 0.02m); + } + + [Test] + public async Task Double_BeEqualTo() + { + await 1.5.Should().BeEqualTo(1.5); + } + + [Test] + public async Task Double_BeCloseTo() + { + await 0.1.Should().BeCloseTo(0.10001, 0.001); + } + + [Test] + public async Task Float_BeEqualTo() + { + await 2.5f.Should().BeEqualTo(2.5f); + } + + [Test] + public async Task Int_BeBetween_failure_throws() + { + var ex = await Assert.That(async () => await 50.Should().BeBetween(1, 10)) + .Throws(); + await Assert.That(ex.Message).Contains("BeBetween"); + } + + [Test] + public async Task Int_NotBeEqualTo() + { + await 5.Should().NotBeEqualTo(7); + } +} diff --git a/TUnit.Assertions.Should.Tests/StringTests.cs b/TUnit.Assertions.Should.Tests/StringTests.cs new file mode 100644 index 0000000000..da202f73c8 --- /dev/null +++ b/TUnit.Assertions.Should.Tests/StringTests.cs @@ -0,0 +1,69 @@ +using TUnit.Assertions.Should; +using TUnit.Assertions.Should.Extensions; + +namespace TUnit.Assertions.Should.Tests; + +public class StringTests +{ + [Test] + public async Task BeEqualTo() + { + await "hello".Should().BeEqualTo("hello"); + } + + [Test] + public async Task NotBeEqualTo() + { + await "hello".Should().NotBeEqualTo("world"); + } + + [Test] + public async Task Contain() + { + await "hello world".Should().Contain("world"); + } + + [Test] + public async Task NotContain() + { + await "hello world".Should().NotContain("xyz"); + } + + [Test] + public async Task StartWith() + { + await "hello world".Should().StartWith("hello"); + } + + [Test] + public async Task NotStartWith() + { + await "hello world".Should().NotStartWith("xyz"); + } + + [Test] + public async Task EndWith() + { + await "hello world".Should().EndWith("world"); + } + + [Test] + public async Task NotEndWith() + { + await "hello world".Should().NotEndWith("xyz"); + } + + [Test] + public async Task NotMatch() + { + await "hello".Should().NotMatch(@"\d+"); + } + + [Test] + public async Task Failure_includes_method_name_in_expression() + { + var ex = await Assert.That(async () => await "hello".Should().Contain("xyz")) + .Throws(); + await Assert.That(ex.Message).Contains(".Should().Contain("); + } +} diff --git a/TUnit.Assertions.Should.Tests/TUnit.Assertions.Should.Tests.csproj b/TUnit.Assertions.Should.Tests/TUnit.Assertions.Should.Tests.csproj new file mode 100644 index 0000000000..ab3c18b754 --- /dev/null +++ b/TUnit.Assertions.Should.Tests/TUnit.Assertions.Should.Tests.csproj @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/TUnit.Assertions.Should.Tests/UserDefinedAssertionTests.cs b/TUnit.Assertions.Should.Tests/UserDefinedAssertionTests.cs new file mode 100644 index 0000000000..6062cd8631 --- /dev/null +++ b/TUnit.Assertions.Should.Tests/UserDefinedAssertionTests.cs @@ -0,0 +1,57 @@ +using TUnit.Assertions.Attributes; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should; +using TUnit.Assertions.Should.Attributes; +using TUnit.Assertions.Should.Extensions; + +namespace TUnit.Assertions.Should.Tests; + +[AssertionExtension("IsOdd")] +public sealed class OddAssertion : Assertion +{ + public OddAssertion(AssertionContext context) : base(context) { } + + protected override Task CheckAsync(EvaluationMetadata metadata) + => Task.FromResult(metadata.Value % 2 != 0 + ? AssertionResult.Passed + : AssertionResult.Failed($"{metadata.Value} is even")); + + protected override string GetExpectation() => "to be odd"; +} + +[ShouldName("BeOddByCustomName")] +public sealed class OddCustomNameAssertion : Assertion +{ + public OddCustomNameAssertion(AssertionContext context) : base(context) { } + + protected override Task CheckAsync(EvaluationMetadata metadata) + => Task.FromResult(metadata.Value % 2 != 0 + ? AssertionResult.Passed + : AssertionResult.Failed($"{metadata.Value} is even")); + + protected override string GetExpectation() => "to be odd"; +} + +public static class OddCustomNameExtensions +{ + public static OddCustomNameAssertion ChecksOddByCustomName(this IAssertionSource source) + => new(source.Context); +} + +public class UserDefinedAssertionTests +{ + [Test] + public async Task User_assertion_gets_Should_flavored_counterpart() + { + // OddAssertion is declared in this test assembly — the generator running in + // user compilation must emit ShouldOddAssertionExtensions.BeOdd, even though + // the baked-into-Should.dll extensions for built-in assertions are skipped. + await 3.Should().BeOdd(); + } + + [Test] + public async Task User_assertion_can_override_Should_name() + { + await 3.Should().BeOddByCustomName(); + } +} diff --git a/TUnit.Assertions.Should/Attributes/ShouldGeneratePartialAttribute.cs b/TUnit.Assertions.Should/Attributes/ShouldGeneratePartialAttribute.cs new file mode 100644 index 0000000000..bac5510379 --- /dev/null +++ b/TUnit.Assertions.Should/Attributes/ShouldGeneratePartialAttribute.cs @@ -0,0 +1,42 @@ +using System; +using TUnit.Assertions.Core; + +namespace TUnit.Assertions.Should.Attributes; + +/// +/// Marks a partial Should-flavored source wrapper class for which the source generator +/// should emit instance-method partials covering each public instance method on +/// . The wrapper class is responsible for constructing +/// its own rather than borrowing one from the wrapped +/// type's constructor. +/// +/// +/// +/// Used by ShouldCollectionSource<T> to expose BeInOrder/All/ +/// Any/HaveSingleItem/etc. as instance methods so callers don't need +/// explicit type arguments — the generated extension form +/// Method<TCollection, TItem>(IShouldSource<TCollection>) can't infer +/// TItem from a constraint alone. +/// +/// +/// Pass the open generic form (typeof(CollectionAssertion<>)) when both wrapper +/// and wrapped are 1-arity generics: the source generator substitutes the wrapper's type +/// parameter into the wrapped definition. C# 11 generic attributes were the natural fit but +/// can't take a type parameter from the enclosing context as a type argument +/// (CS8968); the open-generic Type form is the workaround. +/// +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] +public sealed class ShouldGeneratePartialAttribute : Attribute +{ + public ShouldGeneratePartialAttribute(Type wrappedTypeDefinition) + { + WrappedTypeDefinition = wrappedTypeDefinition; + } + + /// + /// The wrapped TUnit assertion type. Pass the open generic (typeof(Foo<>)) + /// when the wrapper and wrapped have the same generic arity. + /// + public Type WrappedTypeDefinition { get; } +} diff --git a/TUnit.Assertions.Should/Attributes/ShouldNameAttribute.cs b/TUnit.Assertions.Should/Attributes/ShouldNameAttribute.cs new file mode 100644 index 0000000000..ff57a03477 --- /dev/null +++ b/TUnit.Assertions.Should/Attributes/ShouldNameAttribute.cs @@ -0,0 +1,35 @@ +using System; + +namespace TUnit.Assertions.Should.Attributes; + +/// +/// Overrides the auto-conjugated Should name produced by the source generator for a class +/// decorated with [AssertionExtension]. Use when the conjugation rules don't produce +/// the desired Should-flavored name. +/// +/// +/// The override applies to the extension method whose name matches the class's +/// [AssertionExtension(MethodName)]. Negated forms (declared via +/// [AssertionExtension(NegatedMethodName = "...")]) are emitted as separate methods by +/// TUnit.Assertions.SourceGenerator and get their own auto-conjugation pass — to override +/// a negated name, place a separate [ShouldName] on the negated assertion class (TUnit's +/// own naming convention typically gives positive and negated assertions distinct classes). +/// This attribute applies only to assertion classes. Methods decorated with +/// [GenerateAssertion] use generated, file-scoped assertion classes that cannot be annotated; +/// those Should names are always produced by automatic conjugation. +/// +/// +/// +/// [AssertionExtension("IsOdd")] +/// [ShouldName("BeOdd")] +/// public class IsOddAssertion : Assertion<int> { ... } +/// +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] +public sealed class ShouldNameAttribute : Attribute +{ + public ShouldNameAttribute(string name) => Name = name; + + /// The Should-flavored method name to emit (overrides conjugation). + public string Name { get; } +} diff --git a/TUnit.Assertions.Should/Core/IShouldSource.cs b/TUnit.Assertions.Should/Core/IShouldSource.cs new file mode 100644 index 0000000000..7ff12e5056 --- /dev/null +++ b/TUnit.Assertions.Should/Core/IShouldSource.cs @@ -0,0 +1,29 @@ +using System.ComponentModel; +using TUnit.Assertions.Core; + +namespace TUnit.Assertions.Should.Core; + +/// +/// Non-generic marker for all Should-flavored assertion sources. +/// +public interface IShouldSource; + +/// +/// Marker interface for Should-flavored assertion sources. Generated extension +/// methods target this type and read the assertion context via . +/// Element-type inference for collection-style assertions is achieved through +/// method-level generic constraints (where TActual : IEnumerable<TItem>), +/// not interface variance. +/// +/// The type of value being asserted. +public interface IShouldSource : IShouldSource +{ + AssertionContext Context { get; } + + /// + /// Consumes a pending Because message set before the next assertion is created. + /// Used by generated extensions to apply pre-chain reasons to the assertion instance. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + string? ConsumeBecauseMessage(); +} diff --git a/TUnit.Assertions.Should/Core/ShouldAssertion.cs b/TUnit.Assertions.Should/Core/ShouldAssertion.cs new file mode 100644 index 0000000000..13e4dd5004 --- /dev/null +++ b/TUnit.Assertions.Should/Core/ShouldAssertion.cs @@ -0,0 +1,48 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; + +namespace TUnit.Assertions.Should.Core; + +/// +/// Per-call result of a generated Should extension method. Wraps the underlying +/// produced by the original TUnit.Assertions extension +/// and forwards await to it. Exposes .And/.Or properties that +/// return Should-flavored continuations to keep the chain in Should naming. +/// +/// +/// Class rather than readonly struct: forwards to the wrapped +/// 's state-machine awaiter, which mutates instance state during +/// evaluation. A struct ShouldAssertion would either copy that mutation away from the captured +/// receiver or have to box-and-shelve through , neither of which +/// is desirable. The single per-call allocation is the conscious cost; the entry types +/// (ShouldSource, ShouldContinuation) stay structs because they're pure routers. +/// +public sealed class ShouldAssertion : IShouldSource +{ + private readonly Assertion _inner; + + public AssertionContext Context { get; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public ShouldAssertion(AssertionContext context, Assertion inner) + { + Context = context; + _inner = inner; + } + + public TaskAwaiter GetAwaiter() => _inner.GetAwaiter(); + + public ShouldAssertion Because(string message) + { + _inner.Because(message); + return this; + } + + public ShouldContinuation And => new(_inner.And.Context); + + public ShouldContinuation Or => new(_inner.Or.Context); + + string? IShouldSource.ConsumeBecauseMessage() + => null; +} diff --git a/TUnit.Assertions.Should/Core/ShouldCollectionSource.cs b/TUnit.Assertions.Should/Core/ShouldCollectionSource.cs new file mode 100644 index 0000000000..717c1e41e9 --- /dev/null +++ b/TUnit.Assertions.Should/Core/ShouldCollectionSource.cs @@ -0,0 +1,97 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Text; +using TUnit.Assertions.Conditions; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Attributes; +using TUnit.Assertions.Sources; + +namespace TUnit.Assertions.Should.Core; + +/// +/// Should-flavored entry wrapper for collections. Exposes element-typed instance methods +/// (BeInOrder/All/HaveSingleItem) so callers don't need explicit type +/// arguments — the generated extension form +/// Method<TCollection, TItem>(IShouldSource<TCollection>) can't infer +/// TItem from a constraint alone. +/// +/// The Should-flavored instance methods are emitted by the source generator from +/// 's public instance methods — see +/// . +/// +/// +[ShouldGeneratePartial(typeof(CollectionAssertion<>))] +public sealed partial class ShouldCollectionSource : IShouldSource> +{ + private string? _becauseMessage; + + public AssertionContext> Context { get; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public ShouldCollectionSource(IEnumerable? value, string? expression) + { + var sb = new StringBuilder((expression?.Length ?? 1) + 16); + sb.Append(expression ?? "?").Append(".Should()"); + Context = new AssertionContext>(value, sb); + } + + public ShouldCollectionSource Because(string message) + { + _becauseMessage = message.Trim(); + return this; + } + + string? IShouldSource>.ConsumeBecauseMessage() + => ConsumeBecauseMessage(); + + private string? ConsumeBecauseMessage() + { + var message = _becauseMessage; + _becauseMessage = null; + return message; + } + + private TAssertion ApplyBecause(TAssertion assertion) + where TAssertion : Assertion> + { + var because = ConsumeBecauseMessage(); + if (because is not null) + { + assertion.Because(because); + } + return assertion; + } + + // The next three methods can't be source-generated by the simple-factory rule: their target + // assertion ctors take a separate `predicateDescription` string (e.g. + // CollectionAllAssertion(ctx, predicate, predicateDescription)) which the source-side method + // fills from the CAE expression value with a literal fallback — a one-method-param-to-two- + // ctor-params shape the generator's filter rejects. + + public ShouldAssertion> All( + Func predicate, + [CallerArgumentExpression(nameof(predicate))] string? expression = null) + { + Context.ExpressionBuilder.Append(".All(").Append(expression).Append(')'); + var inner = ApplyBecause(new CollectionAllAssertion, TItem>(Context, predicate, expression ?? "predicate")); + return new ShouldAssertion>(Context, inner); + } + + public ShouldAssertion> Any( + Func predicate, + [CallerArgumentExpression(nameof(predicate))] string? expression = null) + { + Context.ExpressionBuilder.Append(".Any(").Append(expression).Append(')'); + var inner = ApplyBecause(new CollectionAnyAssertion, TItem>(Context, predicate, expression ?? "predicate")); + return new ShouldAssertion>(Context, inner); + } + + public ShouldAssertion> HaveSingleItem( + Func predicate, + [CallerArgumentExpression(nameof(predicate))] string? expression = null) + { + Context.ExpressionBuilder.Append(".HaveSingleItem(").Append(expression).Append(')'); + var inner = ApplyBecause(new HasSingleItemPredicateAssertion, TItem>(Context, predicate, expression ?? "predicate")); + return new ShouldAssertion>(Context, inner); + } +} diff --git a/TUnit.Assertions.Should/Core/ShouldContinuation.cs b/TUnit.Assertions.Should/Core/ShouldContinuation.cs new file mode 100644 index 0000000000..684be9c595 --- /dev/null +++ b/TUnit.Assertions.Should/Core/ShouldContinuation.cs @@ -0,0 +1,31 @@ +using TUnit.Assertions.Core; + +namespace TUnit.Assertions.Should.Core; + +/// +/// Should-flavored chain link returned by / +/// . Generated extensions resolve only against +/// , so the chain stays in Should naming throughout. +/// +public readonly struct ShouldContinuation : IShouldSource +{ + private readonly string? _becauseMessage; + + public AssertionContext Context { get; } + + public ShouldContinuation(AssertionContext context) : this(context, becauseMessage: null) + { + } + + private ShouldContinuation(AssertionContext context, string? becauseMessage) + { + Context = context; + _becauseMessage = becauseMessage; + } + + public ShouldContinuation Because(string message) + => new(Context, message.Trim()); + + string? IShouldSource.ConsumeBecauseMessage() + => _becauseMessage; +} diff --git a/TUnit.Assertions.Should/Core/ShouldDelegateSource.cs b/TUnit.Assertions.Should/Core/ShouldDelegateSource.cs new file mode 100644 index 0000000000..abe2e652a5 --- /dev/null +++ b/TUnit.Assertions.Should/Core/ShouldDelegateSource.cs @@ -0,0 +1,94 @@ +using System.ComponentModel; +using System.Linq; +using TUnit.Assertions.Conditions; +using TUnit.Assertions.Core; + +namespace TUnit.Assertions.Should.Core; + +/// +/// Should-flavored entry wrapper for delegates and async functions. Surfaces +/// Throw<TException> / ThrowExactly<TException> instance methods +/// that mirror FuncAssertion/DelegateAssertion behavior in TUnit.Assertions. +/// +public readonly struct ShouldDelegateSource : IShouldSource +{ + private readonly string? _becauseMessage; + + public AssertionContext Context { get; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public ShouldDelegateSource(AssertionContext context) : this(context, becauseMessage: null) + { + } + + private ShouldDelegateSource(AssertionContext context, string? becauseMessage) + { + Context = context; + _becauseMessage = becauseMessage; + } + + public ShouldDelegateSource Because(string message) + => new(Context, message.Trim()); + + string? IShouldSource.ConsumeBecauseMessage() + => _becauseMessage; + + /// + /// Asserts the delegate throws an exception of or a subclass. + /// + public ShouldAssertion Throw() where TException : Exception + { + Context.ExpressionBuilder.Append($".Throw<{FormatTypeName(typeof(TException))}>()"); + var mapped = Context.MapException(); + var inner = new ThrowsAssertion(mapped); + ApplyBecause(inner); + return new ShouldAssertion(mapped, inner); + } + + /// + /// Asserts the delegate throws an exception of exactly (no subclasses). + /// + public ShouldAssertion ThrowExactly() where TException : Exception + { + Context.ExpressionBuilder.Append($".ThrowExactly<{FormatTypeName(typeof(TException))}>()"); + var mapped = Context.MapException(); + var inner = new ThrowsExactlyAssertion(mapped); + ApplyBecause(inner); + return new ShouldAssertion(mapped, inner); + } + + private void ApplyBecause(Assertion assertion) + { + if (_becauseMessage is not null) + { + assertion.Because(_becauseMessage); + } + } + + /// + /// Renders a type's display name for the assertion's expression-builder string. Strips the + /// backtick-arity suffix and recurses into generic arguments so that + /// typeof(MyException<int>) appears as MyException<Int32> in + /// failure messages rather than the raw MyException`1 that + /// returns. Note: this runs at runtime (no Roslyn) so primitive aliases come through as their + /// CLR names (Int32, not int); the source-generator's emit path uses Roslyn's + /// display format and produces int. The asymmetry is acceptable for failure messages + /// — exception types are rarely generic and almost never primitive — but is worth noting. + /// + private static string FormatTypeName(System.Type t) + { + if (!t.IsGenericType) + { + return t.Name; + } + + var name = t.Name; + var tickIndex = name.IndexOf('`'); + if (tickIndex > 0) + { + name = name.Substring(0, tickIndex); + } + + return $"{name}<{string.Join(", ", t.GenericTypeArguments.Select(FormatTypeName))}>"; + } +} diff --git a/TUnit.Assertions.Should/Core/ShouldSource.cs b/TUnit.Assertions.Should/Core/ShouldSource.cs new file mode 100644 index 0000000000..27dfc1da01 --- /dev/null +++ b/TUnit.Assertions.Should/Core/ShouldSource.cs @@ -0,0 +1,30 @@ +using TUnit.Assertions.Core; + +namespace TUnit.Assertions.Should.Core; + +/// +/// Entry wrapper produced by value.Should(). Holds the assertion context +/// and provides the starting point for chained Should-flavored assertions. +/// +public readonly struct ShouldSource : IShouldSource +{ + private readonly string? _becauseMessage; + + public AssertionContext Context { get; } + + public ShouldSource(AssertionContext context) : this(context, becauseMessage: null) + { + } + + private ShouldSource(AssertionContext context, string? becauseMessage) + { + Context = context; + _becauseMessage = becauseMessage; + } + + public ShouldSource Because(string message) + => new(Context, message.Trim()); + + string? IShouldSource.ConsumeBecauseMessage() + => _becauseMessage; +} diff --git a/TUnit.Assertions.Should/ShouldExtensions.cs b/TUnit.Assertions.Should/ShouldExtensions.cs new file mode 100644 index 0000000000..d821ef5b1f --- /dev/null +++ b/TUnit.Assertions.Should/ShouldExtensions.cs @@ -0,0 +1,114 @@ +using System.Runtime.CompilerServices; +using System.Text; +using TUnit.Assertions.Core; +using TUnit.Assertions.Should.Core; + +namespace TUnit.Assertions.Should; + +/// +/// Entry-point extensions for Should-style assertions. Mirrors +/// TUnit.Assertions.Assert.That(...) as a fluent extension on the value. +/// +public static class ShouldExtensions +{ + /// + /// Begins a Should-flavored assertion chain on the supplied value. + /// + public static ShouldSource Should( + this T? value, + [CallerArgumentExpression(nameof(value))] string? expression = null) + { + var sb = BuildExpression(expression); + return new ShouldSource(new AssertionContext(value, sb)); + } + + /// + /// Strings — explicit overload so they don't bind to the overload + /// (which would lose string-specific assertions like StartWith/Contain). + /// is honored by SDK 9+ compilers; older + /// compilers ignore it and fall back to normal overload resolution. + /// + [OverloadResolutionPriority(2)] + public static ShouldSource Should( + this string? value, + [CallerArgumentExpression(nameof(value))] string? expression = null) + { + var sb = BuildExpression(expression); + return new ShouldSource(new AssertionContext(value, sb)); + } + + /// + /// Collections enter as so element-typed + /// instance methods (BeInOrder, HaveSingleItem, All, Any) infer + /// the element type without an explicit type argument. The wrapper also implements + /// for so generated extensions + /// that DO have item-typed parameters (Contain, NotContain) resolve as well. + /// is honored by SDK 9+ compilers; older + /// compilers ignore it and fall back to normal overload resolution. + /// + [OverloadResolutionPriority(1)] + public static ShouldCollectionSource Should( + this IEnumerable? value, + [CallerArgumentExpression(nameof(value))] string? expression = null) + => new(value, expression); + + /// + /// Begins a Should-flavored assertion chain on a synchronous action. + /// Used primarily for exception assertions: (() => action()).Should().Throw<E>(). + /// + public static ShouldDelegateSource Should( + this Action action, + [CallerArgumentExpression(nameof(action))] string? expression = null) + => CreateDelegateSource(expression, () => + { + try { action(); return Task.FromResult<(object?, Exception?)>((null, null)); } + catch (Exception ex) { return Task.FromResult<(object?, Exception?)>((null, ex)); } + }); + + /// + /// Begins a Should-flavored assertion chain on a synchronous function. + /// + public static ShouldDelegateSource Should( + this Func func, + [CallerArgumentExpression(nameof(func))] string? expression = null) + => CreateDelegateSource(expression, () => + { + try { return Task.FromResult<(T?, Exception?)>((func(), null)); } + catch (Exception ex) { return Task.FromResult<(T?, Exception?)>((default, ex)); } + }); + + /// + /// Begins a Should-flavored assertion chain on an asynchronous action. + /// + public static ShouldDelegateSource Should( + this Func action, + [CallerArgumentExpression(nameof(action))] string? expression = null) + => CreateDelegateSource(expression, async () => + { + try { await action().ConfigureAwait(false); return (null, null); } + catch (Exception ex) { return (null, ex); } + }); + + /// + /// Begins a Should-flavored assertion chain on an asynchronous function. + /// + public static ShouldDelegateSource Should( + this Func> func, + [CallerArgumentExpression(nameof(func))] string? expression = null) + => CreateDelegateSource(expression, async () => + { + try { return (await func().ConfigureAwait(false), null); } + catch (Exception ex) { return (default, ex); } + }); + + private static ShouldDelegateSource CreateDelegateSource( + string? expression, Func> evaluator) + => new(new AssertionContext(new EvaluationContext(evaluator), BuildExpression(expression))); + + private static StringBuilder BuildExpression(string? expression) + { + var sb = new StringBuilder((expression?.Length ?? 1) + 16); + sb.Append(expression ?? "?").Append(".Should()"); + return sb; + } +} diff --git a/TUnit.Assertions.Should/TUnit.Assertions.Should.csproj b/TUnit.Assertions.Should/TUnit.Assertions.Should.csproj new file mode 100644 index 0000000000..2ad1c02caa --- /dev/null +++ b/TUnit.Assertions.Should/TUnit.Assertions.Should.csproj @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs b/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs index f66b278773..ce6c7dda6a 100644 --- a/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs +++ b/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs @@ -173,7 +173,28 @@ private static (AssertionMethodData? Data, Diagnostic? Diagnostic) GetAssertionM foreach (var attr in methodSymbol.GetAttributes()) { var attributeClass = attr.AttributeClass; - if (attributeClass == null || attributeClass.ContainingNamespace?.ToDisplayString() != "System.Diagnostics.CodeAnalysis") + if (attributeClass == null) + continue; + + var attrNamespace = attributeClass.ContainingNamespace?.ToDisplayString(); + + // Forward [Obsolete] / [EditorBrowsable] to the generated extension method so the + // user-visible API surface carries the same deprecation/visibility decisions as the + // source-declared method. Without this, [Obsolete] on a [GenerateAssertion] method is + // a no-op — the source method is internal (file-scoped or hidden inside the assertion + // class), and only the generated extension is callable by users. + if (attributeClass.Name == "ObsoleteAttribute" && attrNamespace == "System") + { + diagnosticAttributesForExtensionMethod.Add(FormatObsoleteAttribute(attr)); + continue; + } + if (attributeClass.Name == "EditorBrowsableAttribute" && attrNamespace == "System.ComponentModel") + { + diagnosticAttributesForExtensionMethod.Add(FormatEditorBrowsableAttribute(attr)); + continue; + } + + if (attrNamespace != "System.Diagnostics.CodeAnalysis") continue; // Handle UnconditionalSuppressMessage - goes to CheckAsync @@ -344,6 +365,12 @@ private static bool IsExtensionMethod(IMethodSymbol methodSymbol) => methodSymbol.IsExtensionMethod || (methodSymbol.Parameters.Length > 0 && methodSymbol.Parameters[0].IsThis); + private static string FormatObsoleteAttribute(AttributeData attr) + => TUnit.SourceGen.Shared.AttributeForwardingFormatters.FormatObsolete(attr); + + private static string FormatEditorBrowsableAttribute(AttributeData attr) + => TUnit.SourceGen.Shared.AttributeForwardingFormatters.FormatEditorBrowsable(attr); + /// /// Checks if a type is file-scoped (has 'file' accessibility) /// File-scoped types have specific metadata that we can check. diff --git a/TUnit.Assertions.SourceGenerator/Shared/AttributeForwardingFormatters.cs b/TUnit.Assertions.SourceGenerator/Shared/AttributeForwardingFormatters.cs new file mode 100644 index 0000000000..784d82d2d9 --- /dev/null +++ b/TUnit.Assertions.SourceGenerator/Shared/AttributeForwardingFormatters.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace TUnit.SourceGen.Shared; + +/// +/// Shared formatters that turn and +/// instances back into the C# source +/// fragments needed to forward them onto generator-emitted methods. +/// +/// +/// Linked into both TUnit.Assertions.SourceGenerator and +/// TUnit.Assertions.Should.SourceGenerator via <Compile Include="...">. The +/// generators use these to propagate user deprecation/visibility decisions through the source +/// surface they emit; without forwarding, an [Obsolete] on a [GenerateAssertion] +/// or [AssertionExtension] source method would be dead because users only ever call the +/// generated extension/wrapper methods. +/// +internal static class AttributeForwardingFormatters +{ + /// Formats an for the source generator's emitted method. + /// The fully-qualified prefix to use, e.g. "global::" or "". + public static string FormatObsolete(AttributeData attr, string globalQualifier = "") + { + var args = new List(); + if (attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is string message) + { + args.Add($"\"{message.Replace("\"", "\\\"")}\""); + } + if (attr.ConstructorArguments.Length > 1 && attr.ConstructorArguments[1].Value is bool isError && isError) + { + args.Add("true"); + } + + var diagnosticIdPart = ""; + var urlFormatPart = ""; + foreach (var named in attr.NamedArguments) + { + if (named.Key == "DiagnosticId" && named.Value.Value is string id) + { + diagnosticIdPart = $", DiagnosticId = \"{id}\""; + } + else if (named.Key == "UrlFormat" && named.Value.Value is string url) + { + urlFormatPart = $", UrlFormat = \"{url}\""; + } + } + return $"[{globalQualifier}System.Obsolete({string.Join(", ", args)}{diagnosticIdPart}{urlFormatPart})]"; + } + + /// Formats an for the emitted method. + public static string FormatEditorBrowsable(AttributeData attr, string globalQualifier = "") + { + var state = attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is int s ? s : 0; + return $"[{globalQualifier}System.ComponentModel.EditorBrowsable(({globalQualifier}System.ComponentModel.EditorBrowsableState){state})]"; + } +} diff --git a/TUnit.Assertions/Conditions/RangeAssertionExtensions.cs b/TUnit.Assertions/Conditions/RangeAssertionExtensions.cs index 9ee33ceff2..1851d1ca37 100644 --- a/TUnit.Assertions/Conditions/RangeAssertionExtensions.cs +++ b/TUnit.Assertions/Conditions/RangeAssertionExtensions.cs @@ -1,4 +1,6 @@ #if NET8_0_OR_GREATER +using System; +using System.ComponentModel; using TUnit.Assertions.Attributes; namespace TUnit.Assertions.Conditions; @@ -15,6 +17,17 @@ file static partial class RangeAssertionExtensions public static bool HasStartFromBeginning(this Range value) => !value.Start.IsFromEnd; [GenerateAssertion(ExpectationMessage = "to have end index from beginning", InlineMethodBody = true)] public static bool HasEndFromBeginning(this Range value) => !value.End.IsFromEnd; + + [GenerateAssertion(ExpectationMessage = "to be the full range", InlineMethodBody = true)] + public static bool IsFullRange(this Range value) => value.Equals(Range.All); + + // Conjugates to "BeAll" via the Should generator's Is* -> Be* rule, which reads as + // "should be all of what?" — the "all-range" context is lost. Replaced by IsFullRange + // (-> BeFullRange). Kept around for backwards compatibility; both Obsolete and the hidden + // visibility propagate through MethodAssertionGenerator/ShouldExtensionGenerator to the + // user-callable extension surface. + [Obsolete("Use IsFullRange instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] [GenerateAssertion(ExpectationMessage = "to be the all range", InlineMethodBody = true)] public static bool IsAll(this Range value) => value.Equals(Range.All); } diff --git a/TUnit.CI.slnx b/TUnit.CI.slnx index b643d853ea..61523334a9 100644 --- a/TUnit.CI.slnx +++ b/TUnit.CI.slnx @@ -11,6 +11,7 @@ + @@ -49,6 +50,7 @@ + @@ -66,6 +68,8 @@ + + diff --git a/TUnit.Pipeline/Modules/GetPackageProjectsModule.cs b/TUnit.Pipeline/Modules/GetPackageProjectsModule.cs index af7cd67bb2..e1cdaa13cf 100644 --- a/TUnit.Pipeline/Modules/GetPackageProjectsModule.cs +++ b/TUnit.Pipeline/Modules/GetPackageProjectsModule.cs @@ -13,6 +13,7 @@ public class GetPackageProjectsModule : Module> return [ Sourcy.DotNet.Projects.TUnit_Assertions, + Sourcy.DotNet.Projects.TUnit_Assertions_Should, Sourcy.DotNet.Projects.TUnit_Assertions_FSharp, Sourcy.DotNet.Projects.TUnit_Core, Sourcy.DotNet.Projects.TUnit_Engine, diff --git a/TUnit.Pipeline/Modules/PackTUnitFilesModule.cs b/TUnit.Pipeline/Modules/PackTUnitFilesModule.cs index 0a51b910af..223746e881 100644 --- a/TUnit.Pipeline/Modules/PackTUnitFilesModule.cs +++ b/TUnit.Pipeline/Modules/PackTUnitFilesModule.cs @@ -25,7 +25,8 @@ public class PackTUnitFilesModule : Module> "TUnit.Mocks", "TUnit.Mocks.Assertions", "TUnit.Mocks.Http", - "TUnit.Mocks.Logging" + "TUnit.Mocks.Logging", + "TUnit.Assertions.Should" ]; protected override async Task?> ExecuteAsync(IModuleContext context, diff --git a/TUnit.Pipeline/Modules/RunAssertionsShouldSourceGeneratorTestsModule.cs b/TUnit.Pipeline/Modules/RunAssertionsShouldSourceGeneratorTestsModule.cs new file mode 100644 index 0000000000..4c9b785947 --- /dev/null +++ b/TUnit.Pipeline/Modules/RunAssertionsShouldSourceGeneratorTestsModule.cs @@ -0,0 +1,49 @@ +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 RunAssertionsShouldSourceGeneratorTestsModule : TestBaseModule +{ + // Generator output snapshots verify behaviour across consumer TFMs — keep aligned with + // 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 Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken) + { + var project = context.Git().RootDirectory.FindFile(x => x.Name == "TUnit.Assertions.Should.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/TUnit.Pipeline/Modules/RunAssertionsShouldTestsModule.cs b/TUnit.Pipeline/Modules/RunAssertionsShouldTestsModule.cs new file mode 100644 index 0000000000..0db83a5015 --- /dev/null +++ b/TUnit.Pipeline/Modules/RunAssertionsShouldTestsModule.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 RunAssertionsShouldTestsModule : 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.Assertions.Should.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 + { + ["TUNIT_DISABLE_GITHUB_REPORTER"] = "true", + } + } + )); + } +} diff --git a/TUnit.Pipeline/Modules/RunEngineTestsModule.cs b/TUnit.Pipeline/Modules/RunEngineTestsModule.cs index b3c9b4a2ac..b3ac5eb776 100644 --- a/TUnit.Pipeline/Modules/RunEngineTestsModule.cs +++ b/TUnit.Pipeline/Modules/RunEngineTestsModule.cs @@ -16,6 +16,8 @@ namespace TUnit.Pipeline.Modules; [DependsOn] [DependsOn(Optional = true)] [DependsOn] +[DependsOn] +[DependsOn] [DependsOn(Optional = true)] [DependsOn(Optional = true)] [DependsOn] diff --git a/TUnit.PublicAPI/TUnit.PublicAPI.csproj b/TUnit.PublicAPI/TUnit.PublicAPI.csproj index 93fea6c669..3ef18a8c4e 100644 --- a/TUnit.PublicAPI/TUnit.PublicAPI.csproj +++ b/TUnit.PublicAPI/TUnit.PublicAPI.csproj @@ -24,6 +24,7 @@ + diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt index fc2e65071d..82b841f008 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -4883,7 +4883,9 @@ namespace .Extensions public static ._HasBothIndicesFromEnd_Assertion HasBothIndicesFromEnd(this .<> source) { } public static ._HasEndFromBeginning_Assertion HasEndFromBeginning(this .<> source) { } public static ._HasStartFromBeginning_Assertion HasStartFromBeginning(this .<> source) { } + [("Use IsFullRange instead.")] public static ._IsAll_Assertion IsAll(this .<> source) { } + public static ._IsFullRange_Assertion IsFullRange(this .<> source) { } } public sealed class Range_HasBothIndicesFromEnd_Assertion : .<> { @@ -4909,6 +4911,12 @@ namespace .Extensions protected override .<.> CheckAsync(.<> metadata) { } protected override string GetExpectation() { } } + public sealed class Range_IsFullRange_Assertion : .<> + { + public Range_IsFullRange_Assertion(.<> context) { } + protected override .<.> CheckAsync(.<> metadata) { } + protected override string GetExpectation() { } + } public static class ReferenceAssertionExtensions { [.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")] diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt index 011dd68971..1c52015ff5 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -4808,7 +4808,9 @@ namespace .Extensions public static ._HasBothIndicesFromEnd_Assertion HasBothIndicesFromEnd(this .<> source) { } public static ._HasEndFromBeginning_Assertion HasEndFromBeginning(this .<> source) { } public static ._HasStartFromBeginning_Assertion HasStartFromBeginning(this .<> source) { } + [("Use IsFullRange instead.")] public static ._IsAll_Assertion IsAll(this .<> source) { } + public static ._IsFullRange_Assertion IsFullRange(this .<> source) { } } public sealed class Range_HasBothIndicesFromEnd_Assertion : .<> { @@ -4834,6 +4836,12 @@ namespace .Extensions protected override .<.> CheckAsync(.<> metadata) { } protected override string GetExpectation() { } } + public sealed class Range_IsFullRange_Assertion : .<> + { + public Range_IsFullRange_Assertion(.<> context) { } + protected override .<.> CheckAsync(.<> metadata) { } + protected override string GetExpectation() { } + } public static class ReferenceAssertionExtensions { [.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")] diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt index 54d39689d4..53f5d2824a 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -4883,7 +4883,9 @@ namespace .Extensions public static ._HasBothIndicesFromEnd_Assertion HasBothIndicesFromEnd(this .<> source) { } public static ._HasEndFromBeginning_Assertion HasEndFromBeginning(this .<> source) { } public static ._HasStartFromBeginning_Assertion HasStartFromBeginning(this .<> source) { } + [("Use IsFullRange instead.")] public static ._IsAll_Assertion IsAll(this .<> source) { } + public static ._IsFullRange_Assertion IsFullRange(this .<> source) { } } public sealed class Range_HasBothIndicesFromEnd_Assertion : .<> { @@ -4909,6 +4911,12 @@ namespace .Extensions protected override .<.> CheckAsync(.<> metadata) { } protected override string GetExpectation() { } } + public sealed class Range_IsFullRange_Assertion : .<> + { + public Range_IsFullRange_Assertion(.<> context) { } + protected override .<.> CheckAsync(.<> metadata) { } + protected override string GetExpectation() { } + } public static class ReferenceAssertionExtensions { [.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")] diff --git a/TUnit.PublicAPI/Tests.Should_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Should_Library_Has_No_API_Changes.DotNet10_0.verified.txt new file mode 100644 index 0000000000..e737479324 --- /dev/null +++ b/TUnit.PublicAPI/Tests.Should_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -0,0 +1,763 @@ +[assembly: .(".NETCoreApp,Version=v10.0", FrameworkDisplayName=".NET 10.0")] +namespace . +{ + [(.Class, AllowMultiple=false)] + public sealed class ShouldGeneratePartialAttribute : + { + public ShouldGeneratePartialAttribute( wrappedTypeDefinition) { } + public WrappedTypeDefinition { get; } + } + [(.Class, AllowMultiple=false)] + public sealed class ShouldNameAttribute : + { + public ShouldNameAttribute(string name) { } + public string Name { get; } + } +} +namespace . +{ + public interface IShouldSource { } + public interface IShouldSource : ..IShouldSource + { + . Context { get; } + string? ConsumeBecauseMessage(); + } + public sealed class ShouldAssertion : ..IShouldSource, ..IShouldSource + { + public ShouldAssertion(. context, . inner) { } + public ..ShouldContinuation And { get; } + public . Context { get; } + public ..ShouldContinuation Or { get; } + public ..ShouldAssertion Because(string message) { } + public . GetAwaiter() { } + } + [..ShouldGeneratePartial(typeof(.))] + public sealed class ShouldCollectionSource : ..IShouldSource, ..IShouldSource<.> + { + public ShouldCollectionSource(.? value, string? expression) { } + public .<.> Context { get; } + public ..ShouldAssertion<.> All( predicate, [.("predicate")] string? expression = null) { } + public ..ShouldAssertion<.> Any( predicate, [.("predicate")] string? expression = null) { } + public ..ShouldAssertion<.> BeEmpty() { } + public ..ShouldAssertion<.> BeInDescendingOrder() { } + public ..ShouldAssertion<.> BeInOrder() { } + public ..ShouldCollectionSource Because(string message) { } + public ..ShouldAssertion<.> Contain( predicate, [.("predicate")] string? expression = null) { } + public ..ShouldAssertion<.> Contain(TItem expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + public ..ShouldAssertion<.> HaveAtLeast(int minCount, [.("minCount")] string? expression = null) { } + public ..ShouldAssertion<.> HaveAtMost(int maxCount, [.("maxCount")] string? expression = null) { } + public ..ShouldAssertion<.> HaveCount(int expectedCount, [.("expectedCount")] string? expression = null) { } + public ..ShouldAssertion<.> HaveCountBetween(int min, int max, [.("min")] string? minExpression = null, [.("max")] string? maxExpression = null) { } + public ..ShouldAssertion<.> HaveDistinctItems() { } + public ..ShouldAssertion<.> HaveDistinctItems(. comparer, [.("comparer")] string? comparerExpression = null) { } + public ..ShouldAssertion<.> HaveSingleItem() { } + public ..ShouldAssertion<.> HaveSingleItem( predicate, [.("predicate")] string? expression = null) { } + public ..ShouldAssertion<.> NotBeEmpty() { } + public ..ShouldAssertion<.> NotContain(TItem expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + } + public readonly struct ShouldContinuation : ..IShouldSource, ..IShouldSource + { + public ShouldContinuation(. context) { } + public . Context { get; } + public ..ShouldContinuation Because(string message) { } + } + public readonly struct ShouldDelegateSource : ..IShouldSource, ..IShouldSource + { + public ShouldDelegateSource(. context) { } + public . Context { get; } + public ..ShouldDelegateSource Because(string message) { } + public ..ShouldAssertion Throw() + where TException : { } + public ..ShouldAssertion ThrowExactly() + where TException : { } + } + public readonly struct ShouldSource : ..IShouldSource, ..IShouldSource + { + public ShouldSource(. context) { } + public . Context { get; } + public ..ShouldSource Because(string message) { } + } +} +namespace . +{ + public static class ShouldArrayAssertionExtensions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeEmpty(this ..IShouldSource source) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion<.> BeSingleElement(this ..IShouldSource<.> source) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeSingleElement(this ..IShouldSource source) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotBeEmpty(this ..IShouldSource source) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion<.> NotBeSingleElement(this ..IShouldSource<.> source) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotBeSingleElement(this ..IShouldSource source) { } + } + public static class ShouldAssertionExtensions + { + public static ..ShouldAssertion<> BeAfter(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfter(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfter(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfter(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfterOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfterOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfterOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfterOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBefore(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBefore(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBefore(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBefore(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBeforeOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBeforeOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBeforeOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBeforeOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion BeDefined(this ..IShouldSource source) + where TEnum : struct, { } + public static ..ShouldAssertion BeOfType(this ..IShouldSource source, expectedType, [.("expectedType")] string? expression = null) { } + public static ..ShouldAssertion BeParsableInto<[.(..None | ..PublicMethods | ..Interfaces)] T>(this ..IShouldSource source) { } + public static ..ShouldAssertion EqualTo(this ..IShouldSource source, TValue? expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion Eventually(this ..IShouldSource source, <., .> assertionBuilder, timeout, ? pollingInterval = default, [.("timeout")] string? timeoutExpression = null, [.("pollingInterval")] string? pollingIntervalExpression = null) { } + [("Use Length().IsEqualTo(expectedLength) instead.")] + public static ..ShouldAssertion HaveLength(this ..IShouldSource source, int expectedLength, [.("expectedLength")] string? expression = null) { } + public static ..ShouldAssertion HaveMessage(this ..IShouldSource source, string expectedMessage, comparison, [.("expectedMessage")] string? expression = null) + where TException : { } + public static ..ShouldAssertion HaveMessageContaining(this ..IShouldSource source, string expectedSubstring, comparison, [.("expectedSubstring")] string? expression = null) + where TException : { } + public static ..ShouldAssertion HaveMessageEqualTo(this ..IShouldSource source, string expectedMessage, comparison) { } + public static ..ShouldAssertion HaveMessageStartingWith(this ..IShouldSource source, string expectedPrefix, comparison) { } + public static ..ShouldAssertion NotBeDefined(this ..IShouldSource source) + where TEnum : struct, { } + public static ..ShouldAssertion NotBeNull(this ..IShouldSource source) + where TValue : class { } + public static ..ShouldAssertion NotBeParsableInto<[.(..None | ..PublicMethods | ..Interfaces)] T>(this ..IShouldSource source) { } + public static ..ShouldAssertion WaitFor(this ..IShouldSource source, <., .> assertionBuilder, timeout, ? pollingInterval = default, [.("timeout")] string? timeoutExpression = null, [.("pollingInterval")] string? pollingIntervalExpression = null) { } + public static ..ShouldAssertion WithInnerException(this ..IShouldSource source) + where TException : + where TInnerException : { } + public static ..ShouldAssertion WithMessage(this ..IShouldSource source, string expectedMessage, comparison, [.("expectedMessage")] string? expression = null) + where TException : { } + public static ..ShouldAssertion WithMessageContaining(this ..IShouldSource source, string expectedSubstring, comparison, [.("expectedSubstring")] string? expression = null) + where TException : { } + public static ..ShouldAssertion WithMessageMatching(this ..IShouldSource source, .StringMatcher matcher, [.("matcher")] string? expression = null) + where TException : { } + public static ..ShouldAssertion WithMessageMatching(this ..IShouldSource source, string pattern, [.("pattern")] string? expression = null) + where TException : { } + public static ..ShouldAssertion WithMessageNotContaining(this ..IShouldSource source, string notExpectedSubstring, comparison, [.("notExpectedSubstring")] string? expression = null) + where TException : { } + public static ..ShouldAssertion WithStackTraceContaining(this ..IShouldSource source, string expectedSubstring, comparison, [.("expectedSubstring")] string? expression = null) + where TException : { } + } + public static class ShouldBetweenAssertionExtensions + { + public static ..ShouldAssertion BeBetween(this ..IShouldSource source, TValue minimum, TValue maximum, [.("minimum")] string? minimumExpression = null, [.("maximum")] string? maximumExpression = null) + where TValue : { } + } + public static class ShouldBooleanAssertionExtensions + { + public static ..ShouldAssertion BeFalse(this ..IShouldSource source) { } + public static ..ShouldAssertion BeFalse(this ..IShouldSource source) { } + public static ..ShouldAssertion BeTrue(this ..IShouldSource source) { } + public static ..ShouldAssertion BeTrue(this ..IShouldSource source) { } + } + public static class ShouldByteAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldCancellationTokenAssertionExtensions + { + public static ..ShouldAssertion<.CancellationToken> BeNone(this ..IShouldSource<.CancellationToken> source) { } + public static ..ShouldAssertion<.CancellationToken> NotBeNone(this ..IShouldSource<.CancellationToken> source) { } + } + public static class ShouldCollectionAllAssertionExtensions + { + public static ..ShouldAssertion All(this ..IShouldSource source, predicate, string predicateDescription, [.("predicate")] string? predicateExpression = null, [.("predicateDescription")] string? predicateDescriptionExpression = null) + where TCollection : . { } + } + public static class ShouldCollectionAnyAssertionExtensions + { + public static ..ShouldAssertion Any(this ..IShouldSource source, predicate, string predicateDescription, [.("predicate")] string? predicateExpression = null, [.("predicateDescription")] string? predicateDescriptionExpression = null) + where TCollection : . { } + } + public static class ShouldCollectionContainsAssertionExtensions + { + public static ..ShouldAssertion Contain(this ..IShouldSource source, TItem expected, .? comparer = null, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) + where TCollection : . { } + } + public static class ShouldCollectionContainsPredicateAssertionExtensions + { + public static ..ShouldAssertion Contain(this ..IShouldSource source, predicate, [.("predicate")] string? predicateExpression = null) + where TCollection : . { } + } + public static class ShouldCollectionDoesNotContainAssertionExtensions + { + public static ..ShouldAssertion NotContain(this ..IShouldSource source, TItem expected, .? comparer = null, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) + where TCollection : . { } + } + public static class ShouldCollectionDoesNotContainPredicateAssertionExtensions + { + public static ..ShouldAssertion NotContain(this ..IShouldSource source, predicate, string predicateDescription, [.("predicate")] string? predicateExpression = null, [.("predicateDescription")] string? predicateDescriptionExpression = null) + where TCollection : . { } + } + public static class ShouldCollectionIsInDescendingOrderAssertionExtensions + { + public static ..ShouldAssertion BeInDescendingOrder(this ..IShouldSource source) + where TCollection : . { } + } + public static class ShouldCollectionIsInOrderAssertionExtensions + { + public static ..ShouldAssertion BeInOrder(this ..IShouldSource source) + where TCollection : . { } + } + public static class ShouldComparisonAssertionExtensions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeGreaterThan(this ..IShouldSource source, TValue minimum, [.("minimum")] string? minimumExpression = null) + where TValue : { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeGreaterThanOrEqualTo(this ..IShouldSource source, TValue minimum, [.("minimum")] string? minimumExpression = null) + where TValue : { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeLessThan(this ..IShouldSource source, TValue maximum, [.("maximum")] string? maximumExpression = null) + where TValue : { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeLessThanOrEqualTo(this ..IShouldSource source, TValue maximum, [.("maximum")] string? maximumExpression = null) + where TValue : { } + } + public static class ShouldDateOnlyAssertionExtensions + { + public static ..ShouldAssertion<> BeFirstDayOfMonth(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInFuture(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInPast(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeLastDayOfMonth(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeLeapYear(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeOnWeekday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeOnWeekend(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeToday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeLeapYear(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeToday(this ..IShouldSource<> source) { } + } + public static class ShouldDateOnlyEqualsAssertionExtensions + { + public static ..ShouldAssertion<> BeEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldDateTimeAssertionExtensions + { + public static ..ShouldAssertion<> BeInFuture(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInFutureUtc(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInPast(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInPastUtc(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeLeapYear(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeOnWeekday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeOnWeekend(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeToday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeUtc(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeLeapYear(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeToday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeUtc(this ..IShouldSource<> source) { } + } + public static class ShouldDateTimeEqualsAssertionExtensions + { + public static ..ShouldAssertion<> BeEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldDateTimeEqualsExactAssertionExtensions + { + public static ..ShouldAssertion<> EqualExact(this ..IShouldSource<> source, expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldDateTimeOffsetAssertionExtensions + { + public static ..ShouldAssertion<> BeInFuture(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInFutureUtc(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInPast(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInPastUtc(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeLeapYear(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeOnWeekday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeOnWeekend(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeToday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeUtc(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeLeapYear(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeToday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeUtc(this ..IShouldSource<> source) { } + } + public static class ShouldDateTimeOffsetEqualsAssertionExtensions + { + public static ..ShouldAssertion<> BeEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldDayOfWeekAssertionExtensions + { + public static ..ShouldAssertion<> BeFriday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeMonday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeWeekday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeWeekend(this ..IShouldSource<> source) { } + } + public static class ShouldDecimalAssertions + { + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldDecimalEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, decimal expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldDecimalIsCloseToAssertionExtensions + { + public static ..ShouldAssertion BeCloseTo(this ..IShouldSource source, decimal expected, decimal tolerance, [.("expected")] string? expectedExpression = null, [.("tolerance")] string? toleranceExpression = null) { } + } + public static class ShouldDecimalIsWithinPercentOfAssertionExtensions + { + public static ..ShouldAssertion BeWithinPercentOf(this ..IShouldSource source, decimal expected, decimal percent, [.("expected")] string? expectedExpression = null, [.("percent")] string? percentExpression = null) { } + } + public static class ShouldDirectoryHasFilesAssertionExtensions + { + public static ..ShouldAssertion<.DirectoryInfo> HaveFiles(this ..IShouldSource<.DirectoryInfo> source) { } + } + public static class ShouldDirectoryHasNoSubdirectoriesAssertionExtensions + { + public static ..ShouldAssertion<.DirectoryInfo> HaveNoSubdirectories(this ..IShouldSource<.DirectoryInfo> source) { } + } + public static class ShouldDirectoryInfoAssertionExtensions + { + public static ..ShouldAssertion<.DirectoryInfo> BeEmpty(this ..IShouldSource<.DirectoryInfo> source) { } + public static ..ShouldAssertion<.DirectoryInfo> BeHidden(this ..IShouldSource<.DirectoryInfo> source) { } + public static ..ShouldAssertion<.DirectoryInfo> BeRoot(this ..IShouldSource<.DirectoryInfo> source) { } + public static ..ShouldAssertion<.DirectoryInfo> BeSystemDirectory(this ..IShouldSource<.DirectoryInfo> source) { } + public static ..ShouldAssertion<.DirectoryInfo> NotBeEmpty(this ..IShouldSource<.DirectoryInfo> source) { } + public static ..ShouldAssertion<.DirectoryInfo> NotBeHidden(this ..IShouldSource<.DirectoryInfo> source) { } + public static ..ShouldAssertion<.DirectoryInfo> NotBeRoot(this ..IShouldSource<.DirectoryInfo> source) { } + } + public static class ShouldDoubleAssertions + { + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldDoubleEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, double expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldDoubleIsCloseToAssertionExtensions + { + public static ..ShouldAssertion BeCloseTo(this ..IShouldSource source, double expected, double tolerance, [.("expected")] string? expectedExpression = null, [.("tolerance")] string? toleranceExpression = null) { } + } + public static class ShouldDoubleIsWithinPercentOfAssertionExtensions + { + public static ..ShouldAssertion BeWithinPercentOf(this ..IShouldSource source, double expected, double percent, [.("expected")] string? expectedExpression = null, [.("percent")] string? percentExpression = null) { } + } + public static class ShouldEnumAssertionExtensions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion HaveFlag(this ..IShouldSource source, TEnum expectedFlag, [.("expectedFlag")] string? expectedFlagExpression = null) + where TEnum : struct, { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion HaveSameNameAs(this ..IShouldSource source, otherEnumValue, [.("otherEnumValue")] string? otherEnumValueExpression = null) + where TEnum : struct, { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion HaveSameValueAs(this ..IShouldSource source, otherEnumValue, [.("otherEnumValue")] string? otherEnumValueExpression = null) + where TEnum : struct, { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotHaveFlag(this ..IShouldSource source, TEnum unexpectedFlag, [.("unexpectedFlag")] string? unexpectedFlagExpression = null) + where TEnum : struct, { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotHaveSameNameAs(this ..IShouldSource source, otherEnumValue, [.("otherEnumValue")] string? otherEnumValueExpression = null) + where TEnum : struct, { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotHaveSameValueAs(this ..IShouldSource source, otherEnumValue, [.("otherEnumValue")] string? otherEnumValueExpression = null) + where TEnum : struct, { } + } + public static class ShouldEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, TValue? expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, TValue? expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + } + public static class ShouldEquatableAssertionExtensions + { + public static ..ShouldAssertion BeEquatableTo(this ..IShouldSource source, TExpected expected, [.("expected")] string? expectedExpression = null) + where TActual : { } + } + public static class ShouldFileInfoAssertionExtensions + { + public static ..ShouldAssertion<.FileInfo> BeArchived(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> BeEmpty(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> BeHidden(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> BeSystemFile(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> HaveExtension(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> HaveNoExtension(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> NotBeEmpty(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> NotBeHidden(this ..IShouldSource<.FileInfo> source) { } + } + public static class ShouldFileIsNotExecutableAssertionExtensions + { + public static ..ShouldAssertion<.FileInfo> NotBeExecutable(this ..IShouldSource<.FileInfo> source) { } + } + public static class ShouldFileIsNotSystemAssertionExtensions + { + public static ..ShouldAssertion<.FileInfo> NotBeSystem(this ..IShouldSource<.FileInfo> source) { } + } + public static class ShouldFileSystemComparisonAssertions + { + public static ..ShouldAssertion<.DirectoryInfo> BeEquivalentTo(this ..IShouldSource<.DirectoryInfo> source, .DirectoryInfo expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion<.FileInfo> HaveSameContentAs(this ..IShouldSource<.FileInfo> source, .FileInfo expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion<.DirectoryInfo> HaveSameStructureAs(this ..IShouldSource<.DirectoryInfo> source, .DirectoryInfo expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion<.DirectoryInfo> NotBeEquivalentTo(this ..IShouldSource<.DirectoryInfo> source, .DirectoryInfo expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion<.FileInfo> NotHaveSameContentAs(this ..IShouldSource<.FileInfo> source, .FileInfo expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldFloatAssertions + { + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldFloatEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, float expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldFloatIsCloseToAssertionExtensions + { + public static ..ShouldAssertion BeCloseTo(this ..IShouldSource source, float expected, float tolerance, [.("expected")] string? expectedExpression = null, [.("tolerance")] string? toleranceExpression = null) { } + } + public static class ShouldFloatIsWithinPercentOfAssertionExtensions + { + public static ..ShouldAssertion BeWithinPercentOf(this ..IShouldSource source, float expected, float percent, [.("expected")] string? expectedExpression = null, [.("percent")] string? percentExpression = null) { } + } + public static class ShouldGenericAssertions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeIn(this ..IShouldSource source, T[] collection) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeIn(this ..IShouldSource source, . collection, [.("collection")] string? collectionExpression = null) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotBeIn(this ..IShouldSource source, T[] collection) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotBeIn(this ..IShouldSource source, . collection, [.("collection")] string? collectionExpression = null) { } + } + public static class ShouldGuidAssertionExtensions + { + public static ..ShouldAssertion<> BeEmptyGuid(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeEmptyGuid(this ..IShouldSource<> source) { } + } + public static class ShouldHasSingleItemAssertionExtensions + { + public static ..ShouldAssertion HaveSingleItem(this ..IShouldSource source) + where TCollection : . { } + } + public static class ShouldHasSingleItemPredicateAssertionExtensions + { + public static ..ShouldAssertion HaveSingleItem(this ..IShouldSource source, predicate, string predicateDescription, [.("predicate")] string? predicateExpression = null, [.("predicateDescription")] string? predicateDescriptionExpression = null) + where TCollection : . { } + } + public static class ShouldHttpStatusCodeAssertionExtensions + { + public static ..ShouldAssertion<.HttpStatusCode> BeClientError(this ..IShouldSource<.HttpStatusCode> source) { } + public static ..ShouldAssertion<.HttpStatusCode> BeError(this ..IShouldSource<.HttpStatusCode> source) { } + public static ..ShouldAssertion<.HttpStatusCode> BeInformational(this ..IShouldSource<.HttpStatusCode> source) { } + public static ..ShouldAssertion<.HttpStatusCode> BeRedirection(this ..IShouldSource<.HttpStatusCode> source) { } + public static ..ShouldAssertion<.HttpStatusCode> BeServerError(this ..IShouldSource<.HttpStatusCode> source) { } + public static ..ShouldAssertion<.HttpStatusCode> BeSuccess(this ..IShouldSource<.HttpStatusCode> source) { } + public static ..ShouldAssertion<.HttpStatusCode> NotBeSuccess(this ..IShouldSource<.HttpStatusCode> source) { } + } + public static class ShouldIntAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldIntEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, int expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldIntIsCloseToAssertionExtensions + { + public static ..ShouldAssertion BeCloseTo(this ..IShouldSource source, int expected, int tolerance, [.("expected")] string? expectedExpression = null, [.("tolerance")] string? toleranceExpression = null) { } + } + public static class ShouldIntIsWithinPercentOfAssertionExtensions + { + public static ..ShouldAssertion BeWithinPercentOf(this ..IShouldSource source, int expected, double percent, [.("expected")] string? expectedExpression = null, [.("percent")] string? percentExpression = null) { } + } + public static class ShouldIsDefaultAssertionExtensions + { + public static ..ShouldAssertion BeDefault(this ..IShouldSource source) + where TValue : struct { } + } + public static class ShouldIsDefaultNullableAssertionExtensions + { + public static ..ShouldAssertion BeDefault(this ..IShouldSource source) + where TValue : struct { } + } + public static class ShouldIsDefaultReferenceAssertionExtensions + { + public static ..ShouldAssertion BeDefault(this ..IShouldSource source) + where TValue : class { } + } + public static class ShouldIsEquatableOrEqualToAssertionExtensions + { + public static ..ShouldAssertion BeEquatableOrEqualTo(this ..IShouldSource source, TValue expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldIsEquivalentToAssertionExtensions + { + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] + public static ..ShouldAssertion BeEquivalentTo(this ..IShouldSource source, . expected, . ordering = 0, [.("expected")] string? expectedExpression = null, [.("ordering")] string? orderingExpression = null) + where TCollection : . { } + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] + public static ..ShouldAssertion BeEquivalentTo(this ..IShouldSource source, . expected, . comparer, . ordering = 0, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null, [.("ordering")] string? orderingExpression = null) + where TCollection : . { } + } + public static class ShouldIsNotDefaultAssertionExtensions + { + public static ..ShouldAssertion NotBeDefault(this ..IShouldSource source) + where TValue : struct { } + } + public static class ShouldIsNotDefaultNullableAssertionExtensions + { + public static ..ShouldAssertion NotBeDefault(this ..IShouldSource source) + where TValue : struct { } + } + public static class ShouldIsNotDefaultReferenceAssertionExtensions + { + public static ..ShouldAssertion NotBeDefault(this ..IShouldSource source) + where TValue : class { } + } + public static class ShouldJsonElementAssertionExtensions + { + public static ..ShouldAssertion<.> BeArray(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> BeBoolean(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> BeDeepEqualTo(this ..IShouldSource<.> source, . expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion<.> BeNull(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> BeNumber(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> BeObject(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> BeString(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> HaveProperty(this ..IShouldSource<.> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { } + public static ..ShouldAssertion<.> NotBeDeepEqualTo(this ..IShouldSource<.> source, . expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion<.> NotBeNull(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> NotHaveProperty(this ..IShouldSource<.> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { } + } + public static class ShouldJsonStringAssertionExtensions + { + public static ..ShouldAssertion BeValidJson(this ..IShouldSource source) { } + public static ..ShouldAssertion BeValidJsonArray(this ..IShouldSource source) { } + public static ..ShouldAssertion BeValidJsonObject(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeValidJson(this ..IShouldSource source) { } + } + public static class ShouldLazyAssertionExtensions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion<> BeValueCreated(this ..IShouldSource<> source) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion<> BeValueNotCreated(this ..IShouldSource<> source) { } + } + public static class ShouldLongAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldLongEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, long expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldLongIsCloseToAssertionExtensions + { + public static ..ShouldAssertion BeCloseTo(this ..IShouldSource source, long expected, long tolerance, [.("expected")] string? expectedExpression = null, [.("tolerance")] string? toleranceExpression = null) { } + } + public static class ShouldLongIsWithinPercentOfAssertionExtensions + { + public static ..ShouldAssertion BeWithinPercentOf(this ..IShouldSource source, long expected, double percent, [.("expected")] string? expectedExpression = null, [.("percent")] string? percentExpression = null) { } + } + public static class ShouldNotEqualsAssertionExtensions + { + public static ..ShouldAssertion NotBeEqualTo(this ..IShouldSource source, TValue notExpected, .? comparer = null, [.("notExpected")] string? notExpectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + } + public static class ShouldNotEquivalentToAssertionExtensions + { + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] + public static ..ShouldAssertion NotBeEquivalentTo(this ..IShouldSource source, . notExpected, . ordering = 0, [.("notExpected")] string? notExpectedExpression = null, [.("ordering")] string? orderingExpression = null) + where TCollection : . { } + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] + public static ..ShouldAssertion NotBeEquivalentTo(this ..IShouldSource source, . notExpected, . comparer, . ordering = 0, [.("notExpected")] string? notExpectedExpression = null, [.("comparer")] string? comparerExpression = null, [.("ordering")] string? orderingExpression = null) + where TCollection : . { } + } + public static class ShouldNullAssertionExtension + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeNull(this ..IShouldSource source) { } + } + public static class ShouldNullableEquatableAssertionExtensions + { + public static ..ShouldAssertion BeEquatableTo(this ..IShouldSource source, TExpected expected, [.("expected")] string? expectedExpression = null) + where TActual : struct, { } + } + public static class ShouldRangeAssertionExtensions + { + [("Use IsFullRange instead.")] + public static ..ShouldAssertion<> BeAll(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeFullRange(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveBothIndicesFromEnd(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveEndFromBeginning(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveStartFromBeginning(this ..IShouldSource<> source) { } + } + public static class ShouldReferenceAssertionExtensions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeSameReferenceAs(this ..IShouldSource source, object? expected, [.("expected")] string? expectedExpression = null) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotBeSameReferenceAs(this ..IShouldSource source, object? expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldSbyteAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldShortAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldStrictEqualityAssertions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeStrictlyEqualTo(this ..IShouldSource source, T expected, [.("expected")] string? expectedExpression = null) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotBeStrictlyEqualTo(this ..IShouldSource source, T expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldStringBuilderAssertionExtensions + { + public static ..ShouldAssertion<.StringBuilder> BeEmpty(this ..IShouldSource<.StringBuilder> source) { } + public static ..ShouldAssertion<.StringBuilder> HaveExcessCapacity(this ..IShouldSource<.StringBuilder> source) { } + public static ..ShouldAssertion<.StringBuilder> NotBeEmpty(this ..IShouldSource<.StringBuilder> source) { } + } + public static class ShouldStringContainsAssertionExtensions + { + public static ..ShouldAssertion Contain(this ..IShouldSource source, string expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion Contain(this ..IShouldSource source, string expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldStringDoesNotContainAssertionExtensions + { + public static ..ShouldAssertion NotContain(this ..IShouldSource source, string expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion NotContain(this ..IShouldSource source, string expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldStringDoesNotEndWithAssertionExtensions + { + public static ..ShouldAssertion NotEndWith(this ..IShouldSource source, string expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion NotEndWith(this ..IShouldSource source, string expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldStringDoesNotMatchAssertionExtensions + { + public static ..ShouldAssertion NotMatch(this ..IShouldSource source, . regex, [.("regex")] string? regexExpression = null) { } + public static ..ShouldAssertion NotMatch(this ..IShouldSource source, string pattern, [.("pattern")] string? patternExpression = null) { } + } + public static class ShouldStringDoesNotStartWithAssertionExtensions + { + public static ..ShouldAssertion NotStartWith(this ..IShouldSource source, string expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion NotStartWith(this ..IShouldSource source, string expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldStringEmptyAssertionExtensions + { + public static ..ShouldAssertion BeEmpty(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeEmpty(this ..IShouldSource source) { } + } + public static class ShouldStringEndsWithAssertionExtensions + { + public static ..ShouldAssertion EndWith(this ..IShouldSource source, string expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion EndWith(this ..IShouldSource source, string expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldStringEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, string? expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, string? expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldStringLengthRangeAssertionExtensions + { + public static ..ShouldAssertion HaveLengthBetween(this ..IShouldSource source, int minLength, int maxLength, [.("minLength")] string? minLengthExpression = null, [.("maxLength")] string? maxLengthExpression = null) { } + public static ..ShouldAssertion HaveMaxLength(this ..IShouldSource source, int maxLength, [.("maxLength")] string? maxLengthExpression = null) { } + public static ..ShouldAssertion HaveMinLength(this ..IShouldSource source, int minLength, [.("minLength")] string? minLengthExpression = null) { } + } + public static class ShouldStringStartsWithAssertionExtensions + { + public static ..ShouldAssertion StartWith(this ..IShouldSource source, string expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion StartWith(this ..IShouldSource source, string expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldTimeOnlyAssertionExtensions + { + public static ..ShouldAssertion<> BeAM(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeEndOfHour(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeMidnight(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeNoon(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BePM(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeStartOfHour(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeMidnight(this ..IShouldSource<> source) { } + } + public static class ShouldTimeOnlyEqualsAssertionExtensions + { + public static ..ShouldAssertion<> BeEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldTimeSpanAssertionExtensions + { + public static ..ShouldAssertion<> BeNegative(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeNonNegative(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeNonPositive(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BePositive(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeZero(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeZero(this ..IShouldSource<> source) { } + } + public static class ShouldTimeSpanEqualsAssertionExtensions + { + public static ..ShouldAssertion<> BeEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldUintAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldUlongAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldUshortAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldVersionAssertionExtensions + { + public static ..ShouldAssertion<> BeMajorVersion(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveBuildNumber(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveNoBuildNumber(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveNoRevisionNumber(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveRevisionNumber(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeMajorVersion(this ..IShouldSource<> source) { } + } +} +namespace .Should +{ + public static class ShouldExtensions + { + public static ..ShouldDelegateSource Should(this action, [.("action")] string? expression = null) { } + public static ..ShouldDelegateSource Should(this <.> action, [.("action")] string? expression = null) { } + [.(2)] + public static ..ShouldSource Should(this string? value, [.("value")] string? expression = null) { } + [.(1)] + public static ..ShouldCollectionSource Should(this .? value, [.("value")] string? expression = null) { } + public static ..ShouldDelegateSource Should(this <.> func, [.("func")] string? expression = null) { } + public static ..ShouldDelegateSource Should(this func, [.("func")] string? expression = null) { } + public static ..ShouldSource Should(this T? value, [.("value")] string? expression = null) { } + } +} \ No newline at end of file diff --git a/TUnit.PublicAPI/Tests.Should_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Should_Library_Has_No_API_Changes.DotNet8_0.verified.txt new file mode 100644 index 0000000000..2eb9939894 --- /dev/null +++ b/TUnit.PublicAPI/Tests.Should_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -0,0 +1,759 @@ +[assembly: .(".NETCoreApp,Version=v8.0", FrameworkDisplayName=".NET 8.0")] +namespace . +{ + [(.Class, AllowMultiple=false)] + public sealed class ShouldGeneratePartialAttribute : + { + public ShouldGeneratePartialAttribute( wrappedTypeDefinition) { } + public WrappedTypeDefinition { get; } + } + [(.Class, AllowMultiple=false)] + public sealed class ShouldNameAttribute : + { + public ShouldNameAttribute(string name) { } + public string Name { get; } + } +} +namespace . +{ + public interface IShouldSource { } + public interface IShouldSource : ..IShouldSource + { + . Context { get; } + string? ConsumeBecauseMessage(); + } + public sealed class ShouldAssertion : ..IShouldSource, ..IShouldSource + { + public ShouldAssertion(. context, . inner) { } + public ..ShouldContinuation And { get; } + public . Context { get; } + public ..ShouldContinuation Or { get; } + public ..ShouldAssertion Because(string message) { } + public . GetAwaiter() { } + } + [..ShouldGeneratePartial(typeof(.))] + public sealed class ShouldCollectionSource : ..IShouldSource, ..IShouldSource<.> + { + public ShouldCollectionSource(.? value, string? expression) { } + public .<.> Context { get; } + public ..ShouldAssertion<.> All( predicate, [.("predicate")] string? expression = null) { } + public ..ShouldAssertion<.> Any( predicate, [.("predicate")] string? expression = null) { } + public ..ShouldAssertion<.> BeEmpty() { } + public ..ShouldAssertion<.> BeInDescendingOrder() { } + public ..ShouldAssertion<.> BeInOrder() { } + public ..ShouldCollectionSource Because(string message) { } + public ..ShouldAssertion<.> Contain( predicate, [.("predicate")] string? expression = null) { } + public ..ShouldAssertion<.> Contain(TItem expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + public ..ShouldAssertion<.> HaveAtLeast(int minCount, [.("minCount")] string? expression = null) { } + public ..ShouldAssertion<.> HaveAtMost(int maxCount, [.("maxCount")] string? expression = null) { } + public ..ShouldAssertion<.> HaveCount(int expectedCount, [.("expectedCount")] string? expression = null) { } + public ..ShouldAssertion<.> HaveCountBetween(int min, int max, [.("min")] string? minExpression = null, [.("max")] string? maxExpression = null) { } + public ..ShouldAssertion<.> HaveDistinctItems() { } + public ..ShouldAssertion<.> HaveDistinctItems(. comparer, [.("comparer")] string? comparerExpression = null) { } + public ..ShouldAssertion<.> HaveSingleItem() { } + public ..ShouldAssertion<.> HaveSingleItem( predicate, [.("predicate")] string? expression = null) { } + public ..ShouldAssertion<.> NotBeEmpty() { } + public ..ShouldAssertion<.> NotContain(TItem expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + } + public readonly struct ShouldContinuation : ..IShouldSource, ..IShouldSource + { + public ShouldContinuation(. context) { } + public . Context { get; } + public ..ShouldContinuation Because(string message) { } + } + public readonly struct ShouldDelegateSource : ..IShouldSource, ..IShouldSource + { + public ShouldDelegateSource(. context) { } + public . Context { get; } + public ..ShouldDelegateSource Because(string message) { } + public ..ShouldAssertion Throw() + where TException : { } + public ..ShouldAssertion ThrowExactly() + where TException : { } + } + public readonly struct ShouldSource : ..IShouldSource, ..IShouldSource + { + public ShouldSource(. context) { } + public . Context { get; } + public ..ShouldSource Because(string message) { } + } +} +namespace . +{ + public static class ShouldArrayAssertionExtensions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeEmpty(this ..IShouldSource source) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion<.> BeSingleElement(this ..IShouldSource<.> source) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeSingleElement(this ..IShouldSource source) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotBeEmpty(this ..IShouldSource source) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion<.> NotBeSingleElement(this ..IShouldSource<.> source) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotBeSingleElement(this ..IShouldSource source) { } + } + public static class ShouldAssertionExtensions + { + public static ..ShouldAssertion<> BeAfter(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfter(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfter(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfter(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfterOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfterOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfterOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfterOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBefore(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBefore(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBefore(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBefore(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBeforeOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBeforeOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBeforeOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBeforeOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion BeDefined(this ..IShouldSource source) + where TEnum : struct, { } + public static ..ShouldAssertion BeOfType(this ..IShouldSource source, expectedType, [.("expectedType")] string? expression = null) { } + public static ..ShouldAssertion BeParsableInto<[.(..None | ..PublicMethods | ..Interfaces)] T>(this ..IShouldSource source) { } + public static ..ShouldAssertion EqualTo(this ..IShouldSource source, TValue? expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion Eventually(this ..IShouldSource source, <., .> assertionBuilder, timeout, ? pollingInterval = default, [.("timeout")] string? timeoutExpression = null, [.("pollingInterval")] string? pollingIntervalExpression = null) { } + [("Use Length().IsEqualTo(expectedLength) instead.")] + public static ..ShouldAssertion HaveLength(this ..IShouldSource source, int expectedLength, [.("expectedLength")] string? expression = null) { } + public static ..ShouldAssertion HaveMessage(this ..IShouldSource source, string expectedMessage, comparison, [.("expectedMessage")] string? expression = null) + where TException : { } + public static ..ShouldAssertion HaveMessageContaining(this ..IShouldSource source, string expectedSubstring, comparison, [.("expectedSubstring")] string? expression = null) + where TException : { } + public static ..ShouldAssertion HaveMessageEqualTo(this ..IShouldSource source, string expectedMessage, comparison) { } + public static ..ShouldAssertion HaveMessageStartingWith(this ..IShouldSource source, string expectedPrefix, comparison) { } + public static ..ShouldAssertion NotBeDefined(this ..IShouldSource source) + where TEnum : struct, { } + public static ..ShouldAssertion NotBeNull(this ..IShouldSource source) + where TValue : class { } + public static ..ShouldAssertion NotBeParsableInto<[.(..None | ..PublicMethods | ..Interfaces)] T>(this ..IShouldSource source) { } + public static ..ShouldAssertion WaitFor(this ..IShouldSource source, <., .> assertionBuilder, timeout, ? pollingInterval = default, [.("timeout")] string? timeoutExpression = null, [.("pollingInterval")] string? pollingIntervalExpression = null) { } + public static ..ShouldAssertion WithInnerException(this ..IShouldSource source) + where TException : + where TInnerException : { } + public static ..ShouldAssertion WithMessage(this ..IShouldSource source, string expectedMessage, comparison, [.("expectedMessage")] string? expression = null) + where TException : { } + public static ..ShouldAssertion WithMessageContaining(this ..IShouldSource source, string expectedSubstring, comparison, [.("expectedSubstring")] string? expression = null) + where TException : { } + public static ..ShouldAssertion WithMessageMatching(this ..IShouldSource source, .StringMatcher matcher, [.("matcher")] string? expression = null) + where TException : { } + public static ..ShouldAssertion WithMessageMatching(this ..IShouldSource source, string pattern, [.("pattern")] string? expression = null) + where TException : { } + public static ..ShouldAssertion WithMessageNotContaining(this ..IShouldSource source, string notExpectedSubstring, comparison, [.("notExpectedSubstring")] string? expression = null) + where TException : { } + public static ..ShouldAssertion WithStackTraceContaining(this ..IShouldSource source, string expectedSubstring, comparison, [.("expectedSubstring")] string? expression = null) + where TException : { } + } + public static class ShouldBetweenAssertionExtensions + { + public static ..ShouldAssertion BeBetween(this ..IShouldSource source, TValue minimum, TValue maximum, [.("minimum")] string? minimumExpression = null, [.("maximum")] string? maximumExpression = null) + where TValue : { } + } + public static class ShouldBooleanAssertionExtensions + { + public static ..ShouldAssertion BeFalse(this ..IShouldSource source) { } + public static ..ShouldAssertion BeFalse(this ..IShouldSource source) { } + public static ..ShouldAssertion BeTrue(this ..IShouldSource source) { } + public static ..ShouldAssertion BeTrue(this ..IShouldSource source) { } + } + public static class ShouldByteAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldCancellationTokenAssertionExtensions + { + public static ..ShouldAssertion<.CancellationToken> BeNone(this ..IShouldSource<.CancellationToken> source) { } + public static ..ShouldAssertion<.CancellationToken> NotBeNone(this ..IShouldSource<.CancellationToken> source) { } + } + public static class ShouldCollectionAllAssertionExtensions + { + public static ..ShouldAssertion All(this ..IShouldSource source, predicate, string predicateDescription, [.("predicate")] string? predicateExpression = null, [.("predicateDescription")] string? predicateDescriptionExpression = null) + where TCollection : . { } + } + public static class ShouldCollectionAnyAssertionExtensions + { + public static ..ShouldAssertion Any(this ..IShouldSource source, predicate, string predicateDescription, [.("predicate")] string? predicateExpression = null, [.("predicateDescription")] string? predicateDescriptionExpression = null) + where TCollection : . { } + } + public static class ShouldCollectionContainsAssertionExtensions + { + public static ..ShouldAssertion Contain(this ..IShouldSource source, TItem expected, .? comparer = null, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) + where TCollection : . { } + } + public static class ShouldCollectionContainsPredicateAssertionExtensions + { + public static ..ShouldAssertion Contain(this ..IShouldSource source, predicate, [.("predicate")] string? predicateExpression = null) + where TCollection : . { } + } + public static class ShouldCollectionDoesNotContainAssertionExtensions + { + public static ..ShouldAssertion NotContain(this ..IShouldSource source, TItem expected, .? comparer = null, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) + where TCollection : . { } + } + public static class ShouldCollectionDoesNotContainPredicateAssertionExtensions + { + public static ..ShouldAssertion NotContain(this ..IShouldSource source, predicate, string predicateDescription, [.("predicate")] string? predicateExpression = null, [.("predicateDescription")] string? predicateDescriptionExpression = null) + where TCollection : . { } + } + public static class ShouldCollectionIsInDescendingOrderAssertionExtensions + { + public static ..ShouldAssertion BeInDescendingOrder(this ..IShouldSource source) + where TCollection : . { } + } + public static class ShouldCollectionIsInOrderAssertionExtensions + { + public static ..ShouldAssertion BeInOrder(this ..IShouldSource source) + where TCollection : . { } + } + public static class ShouldComparisonAssertionExtensions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeGreaterThan(this ..IShouldSource source, TValue minimum, [.("minimum")] string? minimumExpression = null) + where TValue : { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeGreaterThanOrEqualTo(this ..IShouldSource source, TValue minimum, [.("minimum")] string? minimumExpression = null) + where TValue : { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeLessThan(this ..IShouldSource source, TValue maximum, [.("maximum")] string? maximumExpression = null) + where TValue : { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeLessThanOrEqualTo(this ..IShouldSource source, TValue maximum, [.("maximum")] string? maximumExpression = null) + where TValue : { } + } + public static class ShouldDateOnlyAssertionExtensions + { + public static ..ShouldAssertion<> BeFirstDayOfMonth(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInFuture(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInPast(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeLastDayOfMonth(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeLeapYear(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeOnWeekday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeOnWeekend(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeToday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeLeapYear(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeToday(this ..IShouldSource<> source) { } + } + public static class ShouldDateOnlyEqualsAssertionExtensions + { + public static ..ShouldAssertion<> BeEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldDateTimeAssertionExtensions + { + public static ..ShouldAssertion<> BeInFuture(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInFutureUtc(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInPast(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInPastUtc(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeLeapYear(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeOnWeekday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeOnWeekend(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeToday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeUtc(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeLeapYear(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeToday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeUtc(this ..IShouldSource<> source) { } + } + public static class ShouldDateTimeEqualsAssertionExtensions + { + public static ..ShouldAssertion<> BeEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldDateTimeEqualsExactAssertionExtensions + { + public static ..ShouldAssertion<> EqualExact(this ..IShouldSource<> source, expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldDateTimeOffsetAssertionExtensions + { + public static ..ShouldAssertion<> BeInFuture(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInFutureUtc(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInPast(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInPastUtc(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeLeapYear(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeOnWeekday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeOnWeekend(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeToday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeUtc(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeLeapYear(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeToday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeUtc(this ..IShouldSource<> source) { } + } + public static class ShouldDateTimeOffsetEqualsAssertionExtensions + { + public static ..ShouldAssertion<> BeEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldDayOfWeekAssertionExtensions + { + public static ..ShouldAssertion<> BeFriday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeMonday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeWeekday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeWeekend(this ..IShouldSource<> source) { } + } + public static class ShouldDecimalAssertions + { + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldDecimalEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, decimal expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldDecimalIsCloseToAssertionExtensions + { + public static ..ShouldAssertion BeCloseTo(this ..IShouldSource source, decimal expected, decimal tolerance, [.("expected")] string? expectedExpression = null, [.("tolerance")] string? toleranceExpression = null) { } + } + public static class ShouldDecimalIsWithinPercentOfAssertionExtensions + { + public static ..ShouldAssertion BeWithinPercentOf(this ..IShouldSource source, decimal expected, decimal percent, [.("expected")] string? expectedExpression = null, [.("percent")] string? percentExpression = null) { } + } + public static class ShouldDirectoryHasFilesAssertionExtensions + { + public static ..ShouldAssertion<.DirectoryInfo> HaveFiles(this ..IShouldSource<.DirectoryInfo> source) { } + } + public static class ShouldDirectoryHasNoSubdirectoriesAssertionExtensions + { + public static ..ShouldAssertion<.DirectoryInfo> HaveNoSubdirectories(this ..IShouldSource<.DirectoryInfo> source) { } + } + public static class ShouldDirectoryInfoAssertionExtensions + { + public static ..ShouldAssertion<.DirectoryInfo> BeEmpty(this ..IShouldSource<.DirectoryInfo> source) { } + public static ..ShouldAssertion<.DirectoryInfo> BeHidden(this ..IShouldSource<.DirectoryInfo> source) { } + public static ..ShouldAssertion<.DirectoryInfo> BeRoot(this ..IShouldSource<.DirectoryInfo> source) { } + public static ..ShouldAssertion<.DirectoryInfo> BeSystemDirectory(this ..IShouldSource<.DirectoryInfo> source) { } + public static ..ShouldAssertion<.DirectoryInfo> NotBeEmpty(this ..IShouldSource<.DirectoryInfo> source) { } + public static ..ShouldAssertion<.DirectoryInfo> NotBeHidden(this ..IShouldSource<.DirectoryInfo> source) { } + public static ..ShouldAssertion<.DirectoryInfo> NotBeRoot(this ..IShouldSource<.DirectoryInfo> source) { } + } + public static class ShouldDoubleAssertions + { + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldDoubleEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, double expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldDoubleIsCloseToAssertionExtensions + { + public static ..ShouldAssertion BeCloseTo(this ..IShouldSource source, double expected, double tolerance, [.("expected")] string? expectedExpression = null, [.("tolerance")] string? toleranceExpression = null) { } + } + public static class ShouldDoubleIsWithinPercentOfAssertionExtensions + { + public static ..ShouldAssertion BeWithinPercentOf(this ..IShouldSource source, double expected, double percent, [.("expected")] string? expectedExpression = null, [.("percent")] string? percentExpression = null) { } + } + public static class ShouldEnumAssertionExtensions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion HaveFlag(this ..IShouldSource source, TEnum expectedFlag, [.("expectedFlag")] string? expectedFlagExpression = null) + where TEnum : struct, { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion HaveSameNameAs(this ..IShouldSource source, otherEnumValue, [.("otherEnumValue")] string? otherEnumValueExpression = null) + where TEnum : struct, { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion HaveSameValueAs(this ..IShouldSource source, otherEnumValue, [.("otherEnumValue")] string? otherEnumValueExpression = null) + where TEnum : struct, { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotHaveFlag(this ..IShouldSource source, TEnum unexpectedFlag, [.("unexpectedFlag")] string? unexpectedFlagExpression = null) + where TEnum : struct, { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotHaveSameNameAs(this ..IShouldSource source, otherEnumValue, [.("otherEnumValue")] string? otherEnumValueExpression = null) + where TEnum : struct, { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotHaveSameValueAs(this ..IShouldSource source, otherEnumValue, [.("otherEnumValue")] string? otherEnumValueExpression = null) + where TEnum : struct, { } + } + public static class ShouldEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, TValue? expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, TValue? expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + } + public static class ShouldEquatableAssertionExtensions + { + public static ..ShouldAssertion BeEquatableTo(this ..IShouldSource source, TExpected expected, [.("expected")] string? expectedExpression = null) + where TActual : { } + } + public static class ShouldFileInfoAssertionExtensions + { + public static ..ShouldAssertion<.FileInfo> BeArchived(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> BeEmpty(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> BeHidden(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> BeSystemFile(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> HaveExtension(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> HaveNoExtension(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> NotBeEmpty(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> NotBeHidden(this ..IShouldSource<.FileInfo> source) { } + } + public static class ShouldFileIsNotExecutableAssertionExtensions + { + public static ..ShouldAssertion<.FileInfo> NotBeExecutable(this ..IShouldSource<.FileInfo> source) { } + } + public static class ShouldFileIsNotSystemAssertionExtensions + { + public static ..ShouldAssertion<.FileInfo> NotBeSystem(this ..IShouldSource<.FileInfo> source) { } + } + public static class ShouldFileSystemComparisonAssertions + { + public static ..ShouldAssertion<.DirectoryInfo> BeEquivalentTo(this ..IShouldSource<.DirectoryInfo> source, .DirectoryInfo expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion<.FileInfo> HaveSameContentAs(this ..IShouldSource<.FileInfo> source, .FileInfo expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion<.DirectoryInfo> HaveSameStructureAs(this ..IShouldSource<.DirectoryInfo> source, .DirectoryInfo expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion<.DirectoryInfo> NotBeEquivalentTo(this ..IShouldSource<.DirectoryInfo> source, .DirectoryInfo expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion<.FileInfo> NotHaveSameContentAs(this ..IShouldSource<.FileInfo> source, .FileInfo expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldFloatAssertions + { + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldFloatEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, float expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldFloatIsCloseToAssertionExtensions + { + public static ..ShouldAssertion BeCloseTo(this ..IShouldSource source, float expected, float tolerance, [.("expected")] string? expectedExpression = null, [.("tolerance")] string? toleranceExpression = null) { } + } + public static class ShouldFloatIsWithinPercentOfAssertionExtensions + { + public static ..ShouldAssertion BeWithinPercentOf(this ..IShouldSource source, float expected, float percent, [.("expected")] string? expectedExpression = null, [.("percent")] string? percentExpression = null) { } + } + public static class ShouldGenericAssertions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeIn(this ..IShouldSource source, T[] collection) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeIn(this ..IShouldSource source, . collection, [.("collection")] string? collectionExpression = null) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotBeIn(this ..IShouldSource source, T[] collection) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotBeIn(this ..IShouldSource source, . collection, [.("collection")] string? collectionExpression = null) { } + } + public static class ShouldGuidAssertionExtensions + { + public static ..ShouldAssertion<> BeEmptyGuid(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeEmptyGuid(this ..IShouldSource<> source) { } + } + public static class ShouldHasSingleItemAssertionExtensions + { + public static ..ShouldAssertion HaveSingleItem(this ..IShouldSource source) + where TCollection : . { } + } + public static class ShouldHasSingleItemPredicateAssertionExtensions + { + public static ..ShouldAssertion HaveSingleItem(this ..IShouldSource source, predicate, string predicateDescription, [.("predicate")] string? predicateExpression = null, [.("predicateDescription")] string? predicateDescriptionExpression = null) + where TCollection : . { } + } + public static class ShouldHttpStatusCodeAssertionExtensions + { + public static ..ShouldAssertion<.HttpStatusCode> BeClientError(this ..IShouldSource<.HttpStatusCode> source) { } + public static ..ShouldAssertion<.HttpStatusCode> BeError(this ..IShouldSource<.HttpStatusCode> source) { } + public static ..ShouldAssertion<.HttpStatusCode> BeInformational(this ..IShouldSource<.HttpStatusCode> source) { } + public static ..ShouldAssertion<.HttpStatusCode> BeRedirection(this ..IShouldSource<.HttpStatusCode> source) { } + public static ..ShouldAssertion<.HttpStatusCode> BeServerError(this ..IShouldSource<.HttpStatusCode> source) { } + public static ..ShouldAssertion<.HttpStatusCode> BeSuccess(this ..IShouldSource<.HttpStatusCode> source) { } + public static ..ShouldAssertion<.HttpStatusCode> NotBeSuccess(this ..IShouldSource<.HttpStatusCode> source) { } + } + public static class ShouldIntAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldIntEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, int expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldIntIsCloseToAssertionExtensions + { + public static ..ShouldAssertion BeCloseTo(this ..IShouldSource source, int expected, int tolerance, [.("expected")] string? expectedExpression = null, [.("tolerance")] string? toleranceExpression = null) { } + } + public static class ShouldIntIsWithinPercentOfAssertionExtensions + { + public static ..ShouldAssertion BeWithinPercentOf(this ..IShouldSource source, int expected, double percent, [.("expected")] string? expectedExpression = null, [.("percent")] string? percentExpression = null) { } + } + public static class ShouldIsDefaultAssertionExtensions + { + public static ..ShouldAssertion BeDefault(this ..IShouldSource source) + where TValue : struct { } + } + public static class ShouldIsDefaultNullableAssertionExtensions + { + public static ..ShouldAssertion BeDefault(this ..IShouldSource source) + where TValue : struct { } + } + public static class ShouldIsDefaultReferenceAssertionExtensions + { + public static ..ShouldAssertion BeDefault(this ..IShouldSource source) + where TValue : class { } + } + public static class ShouldIsEquatableOrEqualToAssertionExtensions + { + public static ..ShouldAssertion BeEquatableOrEqualTo(this ..IShouldSource source, TValue expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldIsEquivalentToAssertionExtensions + { + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] + public static ..ShouldAssertion BeEquivalentTo(this ..IShouldSource source, . expected, . ordering = 0, [.("expected")] string? expectedExpression = null, [.("ordering")] string? orderingExpression = null) + where TCollection : . { } + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] + public static ..ShouldAssertion BeEquivalentTo(this ..IShouldSource source, . expected, . comparer, . ordering = 0, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null, [.("ordering")] string? orderingExpression = null) + where TCollection : . { } + } + public static class ShouldIsNotDefaultAssertionExtensions + { + public static ..ShouldAssertion NotBeDefault(this ..IShouldSource source) + where TValue : struct { } + } + public static class ShouldIsNotDefaultNullableAssertionExtensions + { + public static ..ShouldAssertion NotBeDefault(this ..IShouldSource source) + where TValue : struct { } + } + public static class ShouldIsNotDefaultReferenceAssertionExtensions + { + public static ..ShouldAssertion NotBeDefault(this ..IShouldSource source) + where TValue : class { } + } + public static class ShouldJsonElementAssertionExtensions + { + public static ..ShouldAssertion<.> BeArray(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> BeBoolean(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> BeNull(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> BeNumber(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> BeObject(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> BeString(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> HaveProperty(this ..IShouldSource<.> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { } + public static ..ShouldAssertion<.> NotBeNull(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> NotHaveProperty(this ..IShouldSource<.> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { } + } + public static class ShouldJsonStringAssertionExtensions + { + public static ..ShouldAssertion BeValidJson(this ..IShouldSource source) { } + public static ..ShouldAssertion BeValidJsonArray(this ..IShouldSource source) { } + public static ..ShouldAssertion BeValidJsonObject(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeValidJson(this ..IShouldSource source) { } + } + public static class ShouldLazyAssertionExtensions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion<> BeValueCreated(this ..IShouldSource<> source) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion<> BeValueNotCreated(this ..IShouldSource<> source) { } + } + public static class ShouldLongAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldLongEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, long expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldLongIsCloseToAssertionExtensions + { + public static ..ShouldAssertion BeCloseTo(this ..IShouldSource source, long expected, long tolerance, [.("expected")] string? expectedExpression = null, [.("tolerance")] string? toleranceExpression = null) { } + } + public static class ShouldLongIsWithinPercentOfAssertionExtensions + { + public static ..ShouldAssertion BeWithinPercentOf(this ..IShouldSource source, long expected, double percent, [.("expected")] string? expectedExpression = null, [.("percent")] string? percentExpression = null) { } + } + public static class ShouldNotEqualsAssertionExtensions + { + public static ..ShouldAssertion NotBeEqualTo(this ..IShouldSource source, TValue notExpected, .? comparer = null, [.("notExpected")] string? notExpectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + } + public static class ShouldNotEquivalentToAssertionExtensions + { + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] + public static ..ShouldAssertion NotBeEquivalentTo(this ..IShouldSource source, . notExpected, . ordering = 0, [.("notExpected")] string? notExpectedExpression = null, [.("ordering")] string? orderingExpression = null) + where TCollection : . { } + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] + public static ..ShouldAssertion NotBeEquivalentTo(this ..IShouldSource source, . notExpected, . comparer, . ordering = 0, [.("notExpected")] string? notExpectedExpression = null, [.("comparer")] string? comparerExpression = null, [.("ordering")] string? orderingExpression = null) + where TCollection : . { } + } + public static class ShouldNullAssertionExtension + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeNull(this ..IShouldSource source) { } + } + public static class ShouldNullableEquatableAssertionExtensions + { + public static ..ShouldAssertion BeEquatableTo(this ..IShouldSource source, TExpected expected, [.("expected")] string? expectedExpression = null) + where TActual : struct, { } + } + public static class ShouldRangeAssertionExtensions + { + [("Use IsFullRange instead.")] + public static ..ShouldAssertion<> BeAll(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeFullRange(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveBothIndicesFromEnd(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveEndFromBeginning(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveStartFromBeginning(this ..IShouldSource<> source) { } + } + public static class ShouldReferenceAssertionExtensions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeSameReferenceAs(this ..IShouldSource source, object? expected, [.("expected")] string? expectedExpression = null) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotBeSameReferenceAs(this ..IShouldSource source, object? expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldSbyteAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldShortAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldStrictEqualityAssertions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeStrictlyEqualTo(this ..IShouldSource source, T expected, [.("expected")] string? expectedExpression = null) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotBeStrictlyEqualTo(this ..IShouldSource source, T expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldStringBuilderAssertionExtensions + { + public static ..ShouldAssertion<.StringBuilder> BeEmpty(this ..IShouldSource<.StringBuilder> source) { } + public static ..ShouldAssertion<.StringBuilder> HaveExcessCapacity(this ..IShouldSource<.StringBuilder> source) { } + public static ..ShouldAssertion<.StringBuilder> NotBeEmpty(this ..IShouldSource<.StringBuilder> source) { } + } + public static class ShouldStringContainsAssertionExtensions + { + public static ..ShouldAssertion Contain(this ..IShouldSource source, string expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion Contain(this ..IShouldSource source, string expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldStringDoesNotContainAssertionExtensions + { + public static ..ShouldAssertion NotContain(this ..IShouldSource source, string expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion NotContain(this ..IShouldSource source, string expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldStringDoesNotEndWithAssertionExtensions + { + public static ..ShouldAssertion NotEndWith(this ..IShouldSource source, string expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion NotEndWith(this ..IShouldSource source, string expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldStringDoesNotMatchAssertionExtensions + { + public static ..ShouldAssertion NotMatch(this ..IShouldSource source, . regex, [.("regex")] string? regexExpression = null) { } + public static ..ShouldAssertion NotMatch(this ..IShouldSource source, string pattern, [.("pattern")] string? patternExpression = null) { } + } + public static class ShouldStringDoesNotStartWithAssertionExtensions + { + public static ..ShouldAssertion NotStartWith(this ..IShouldSource source, string expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion NotStartWith(this ..IShouldSource source, string expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldStringEmptyAssertionExtensions + { + public static ..ShouldAssertion BeEmpty(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeEmpty(this ..IShouldSource source) { } + } + public static class ShouldStringEndsWithAssertionExtensions + { + public static ..ShouldAssertion EndWith(this ..IShouldSource source, string expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion EndWith(this ..IShouldSource source, string expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldStringEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, string? expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, string? expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldStringLengthRangeAssertionExtensions + { + public static ..ShouldAssertion HaveLengthBetween(this ..IShouldSource source, int minLength, int maxLength, [.("minLength")] string? minLengthExpression = null, [.("maxLength")] string? maxLengthExpression = null) { } + public static ..ShouldAssertion HaveMaxLength(this ..IShouldSource source, int maxLength, [.("maxLength")] string? maxLengthExpression = null) { } + public static ..ShouldAssertion HaveMinLength(this ..IShouldSource source, int minLength, [.("minLength")] string? minLengthExpression = null) { } + } + public static class ShouldStringStartsWithAssertionExtensions + { + public static ..ShouldAssertion StartWith(this ..IShouldSource source, string expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion StartWith(this ..IShouldSource source, string expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldTimeOnlyAssertionExtensions + { + public static ..ShouldAssertion<> BeAM(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeEndOfHour(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeMidnight(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeNoon(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BePM(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeStartOfHour(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeMidnight(this ..IShouldSource<> source) { } + } + public static class ShouldTimeOnlyEqualsAssertionExtensions + { + public static ..ShouldAssertion<> BeEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldTimeSpanAssertionExtensions + { + public static ..ShouldAssertion<> BeNegative(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeNonNegative(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeNonPositive(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BePositive(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeZero(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeZero(this ..IShouldSource<> source) { } + } + public static class ShouldTimeSpanEqualsAssertionExtensions + { + public static ..ShouldAssertion<> BeEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldUintAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldUlongAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldUshortAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldVersionAssertionExtensions + { + public static ..ShouldAssertion<> BeMajorVersion(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveBuildNumber(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveNoBuildNumber(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveNoRevisionNumber(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveRevisionNumber(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeMajorVersion(this ..IShouldSource<> source) { } + } +} +namespace .Should +{ + public static class ShouldExtensions + { + public static ..ShouldDelegateSource Should(this action, [.("action")] string? expression = null) { } + public static ..ShouldDelegateSource Should(this <.> action, [.("action")] string? expression = null) { } + public static ..ShouldSource Should(this string? value, [.("value")] string? expression = null) { } + public static ..ShouldCollectionSource Should(this .? value, [.("value")] string? expression = null) { } + public static ..ShouldDelegateSource Should(this <.> func, [.("func")] string? expression = null) { } + public static ..ShouldDelegateSource Should(this func, [.("func")] string? expression = null) { } + public static ..ShouldSource Should(this T? value, [.("value")] string? expression = null) { } + } +} \ No newline at end of file diff --git a/TUnit.PublicAPI/Tests.Should_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Should_Library_Has_No_API_Changes.DotNet9_0.verified.txt new file mode 100644 index 0000000000..52007898d7 --- /dev/null +++ b/TUnit.PublicAPI/Tests.Should_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -0,0 +1,763 @@ +[assembly: .(".NETCoreApp,Version=v9.0", FrameworkDisplayName=".NET 9.0")] +namespace . +{ + [(.Class, AllowMultiple=false)] + public sealed class ShouldGeneratePartialAttribute : + { + public ShouldGeneratePartialAttribute( wrappedTypeDefinition) { } + public WrappedTypeDefinition { get; } + } + [(.Class, AllowMultiple=false)] + public sealed class ShouldNameAttribute : + { + public ShouldNameAttribute(string name) { } + public string Name { get; } + } +} +namespace . +{ + public interface IShouldSource { } + public interface IShouldSource : ..IShouldSource + { + . Context { get; } + string? ConsumeBecauseMessage(); + } + public sealed class ShouldAssertion : ..IShouldSource, ..IShouldSource + { + public ShouldAssertion(. context, . inner) { } + public ..ShouldContinuation And { get; } + public . Context { get; } + public ..ShouldContinuation Or { get; } + public ..ShouldAssertion Because(string message) { } + public . GetAwaiter() { } + } + [..ShouldGeneratePartial(typeof(.))] + public sealed class ShouldCollectionSource : ..IShouldSource, ..IShouldSource<.> + { + public ShouldCollectionSource(.? value, string? expression) { } + public .<.> Context { get; } + public ..ShouldAssertion<.> All( predicate, [.("predicate")] string? expression = null) { } + public ..ShouldAssertion<.> Any( predicate, [.("predicate")] string? expression = null) { } + public ..ShouldAssertion<.> BeEmpty() { } + public ..ShouldAssertion<.> BeInDescendingOrder() { } + public ..ShouldAssertion<.> BeInOrder() { } + public ..ShouldCollectionSource Because(string message) { } + public ..ShouldAssertion<.> Contain( predicate, [.("predicate")] string? expression = null) { } + public ..ShouldAssertion<.> Contain(TItem expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + public ..ShouldAssertion<.> HaveAtLeast(int minCount, [.("minCount")] string? expression = null) { } + public ..ShouldAssertion<.> HaveAtMost(int maxCount, [.("maxCount")] string? expression = null) { } + public ..ShouldAssertion<.> HaveCount(int expectedCount, [.("expectedCount")] string? expression = null) { } + public ..ShouldAssertion<.> HaveCountBetween(int min, int max, [.("min")] string? minExpression = null, [.("max")] string? maxExpression = null) { } + public ..ShouldAssertion<.> HaveDistinctItems() { } + public ..ShouldAssertion<.> HaveDistinctItems(. comparer, [.("comparer")] string? comparerExpression = null) { } + public ..ShouldAssertion<.> HaveSingleItem() { } + public ..ShouldAssertion<.> HaveSingleItem( predicate, [.("predicate")] string? expression = null) { } + public ..ShouldAssertion<.> NotBeEmpty() { } + public ..ShouldAssertion<.> NotContain(TItem expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + } + public readonly struct ShouldContinuation : ..IShouldSource, ..IShouldSource + { + public ShouldContinuation(. context) { } + public . Context { get; } + public ..ShouldContinuation Because(string message) { } + } + public readonly struct ShouldDelegateSource : ..IShouldSource, ..IShouldSource + { + public ShouldDelegateSource(. context) { } + public . Context { get; } + public ..ShouldDelegateSource Because(string message) { } + public ..ShouldAssertion Throw() + where TException : { } + public ..ShouldAssertion ThrowExactly() + where TException : { } + } + public readonly struct ShouldSource : ..IShouldSource, ..IShouldSource + { + public ShouldSource(. context) { } + public . Context { get; } + public ..ShouldSource Because(string message) { } + } +} +namespace . +{ + public static class ShouldArrayAssertionExtensions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeEmpty(this ..IShouldSource source) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion<.> BeSingleElement(this ..IShouldSource<.> source) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeSingleElement(this ..IShouldSource source) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotBeEmpty(this ..IShouldSource source) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion<.> NotBeSingleElement(this ..IShouldSource<.> source) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotBeSingleElement(this ..IShouldSource source) { } + } + public static class ShouldAssertionExtensions + { + public static ..ShouldAssertion<> BeAfter(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfter(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfter(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfter(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfterOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfterOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfterOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeAfterOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBefore(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBefore(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBefore(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBefore(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBeforeOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBeforeOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBeforeOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion<> BeBeforeOrEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion BeDefined(this ..IShouldSource source) + where TEnum : struct, { } + public static ..ShouldAssertion BeOfType(this ..IShouldSource source, expectedType, [.("expectedType")] string? expression = null) { } + public static ..ShouldAssertion BeParsableInto<[.(..None | ..PublicMethods | ..Interfaces)] T>(this ..IShouldSource source) { } + public static ..ShouldAssertion EqualTo(this ..IShouldSource source, TValue? expected, [.("expected")] string? expression = null) { } + public static ..ShouldAssertion Eventually(this ..IShouldSource source, <., .> assertionBuilder, timeout, ? pollingInterval = default, [.("timeout")] string? timeoutExpression = null, [.("pollingInterval")] string? pollingIntervalExpression = null) { } + [("Use Length().IsEqualTo(expectedLength) instead.")] + public static ..ShouldAssertion HaveLength(this ..IShouldSource source, int expectedLength, [.("expectedLength")] string? expression = null) { } + public static ..ShouldAssertion HaveMessage(this ..IShouldSource source, string expectedMessage, comparison, [.("expectedMessage")] string? expression = null) + where TException : { } + public static ..ShouldAssertion HaveMessageContaining(this ..IShouldSource source, string expectedSubstring, comparison, [.("expectedSubstring")] string? expression = null) + where TException : { } + public static ..ShouldAssertion HaveMessageEqualTo(this ..IShouldSource source, string expectedMessage, comparison) { } + public static ..ShouldAssertion HaveMessageStartingWith(this ..IShouldSource source, string expectedPrefix, comparison) { } + public static ..ShouldAssertion NotBeDefined(this ..IShouldSource source) + where TEnum : struct, { } + public static ..ShouldAssertion NotBeNull(this ..IShouldSource source) + where TValue : class { } + public static ..ShouldAssertion NotBeParsableInto<[.(..None | ..PublicMethods | ..Interfaces)] T>(this ..IShouldSource source) { } + public static ..ShouldAssertion WaitFor(this ..IShouldSource source, <., .> assertionBuilder, timeout, ? pollingInterval = default, [.("timeout")] string? timeoutExpression = null, [.("pollingInterval")] string? pollingIntervalExpression = null) { } + public static ..ShouldAssertion WithInnerException(this ..IShouldSource source) + where TException : + where TInnerException : { } + public static ..ShouldAssertion WithMessage(this ..IShouldSource source, string expectedMessage, comparison, [.("expectedMessage")] string? expression = null) + where TException : { } + public static ..ShouldAssertion WithMessageContaining(this ..IShouldSource source, string expectedSubstring, comparison, [.("expectedSubstring")] string? expression = null) + where TException : { } + public static ..ShouldAssertion WithMessageMatching(this ..IShouldSource source, .StringMatcher matcher, [.("matcher")] string? expression = null) + where TException : { } + public static ..ShouldAssertion WithMessageMatching(this ..IShouldSource source, string pattern, [.("pattern")] string? expression = null) + where TException : { } + public static ..ShouldAssertion WithMessageNotContaining(this ..IShouldSource source, string notExpectedSubstring, comparison, [.("notExpectedSubstring")] string? expression = null) + where TException : { } + public static ..ShouldAssertion WithStackTraceContaining(this ..IShouldSource source, string expectedSubstring, comparison, [.("expectedSubstring")] string? expression = null) + where TException : { } + } + public static class ShouldBetweenAssertionExtensions + { + public static ..ShouldAssertion BeBetween(this ..IShouldSource source, TValue minimum, TValue maximum, [.("minimum")] string? minimumExpression = null, [.("maximum")] string? maximumExpression = null) + where TValue : { } + } + public static class ShouldBooleanAssertionExtensions + { + public static ..ShouldAssertion BeFalse(this ..IShouldSource source) { } + public static ..ShouldAssertion BeFalse(this ..IShouldSource source) { } + public static ..ShouldAssertion BeTrue(this ..IShouldSource source) { } + public static ..ShouldAssertion BeTrue(this ..IShouldSource source) { } + } + public static class ShouldByteAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldCancellationTokenAssertionExtensions + { + public static ..ShouldAssertion<.CancellationToken> BeNone(this ..IShouldSource<.CancellationToken> source) { } + public static ..ShouldAssertion<.CancellationToken> NotBeNone(this ..IShouldSource<.CancellationToken> source) { } + } + public static class ShouldCollectionAllAssertionExtensions + { + public static ..ShouldAssertion All(this ..IShouldSource source, predicate, string predicateDescription, [.("predicate")] string? predicateExpression = null, [.("predicateDescription")] string? predicateDescriptionExpression = null) + where TCollection : . { } + } + public static class ShouldCollectionAnyAssertionExtensions + { + public static ..ShouldAssertion Any(this ..IShouldSource source, predicate, string predicateDescription, [.("predicate")] string? predicateExpression = null, [.("predicateDescription")] string? predicateDescriptionExpression = null) + where TCollection : . { } + } + public static class ShouldCollectionContainsAssertionExtensions + { + public static ..ShouldAssertion Contain(this ..IShouldSource source, TItem expected, .? comparer = null, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) + where TCollection : . { } + } + public static class ShouldCollectionContainsPredicateAssertionExtensions + { + public static ..ShouldAssertion Contain(this ..IShouldSource source, predicate, [.("predicate")] string? predicateExpression = null) + where TCollection : . { } + } + public static class ShouldCollectionDoesNotContainAssertionExtensions + { + public static ..ShouldAssertion NotContain(this ..IShouldSource source, TItem expected, .? comparer = null, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) + where TCollection : . { } + } + public static class ShouldCollectionDoesNotContainPredicateAssertionExtensions + { + public static ..ShouldAssertion NotContain(this ..IShouldSource source, predicate, string predicateDescription, [.("predicate")] string? predicateExpression = null, [.("predicateDescription")] string? predicateDescriptionExpression = null) + where TCollection : . { } + } + public static class ShouldCollectionIsInDescendingOrderAssertionExtensions + { + public static ..ShouldAssertion BeInDescendingOrder(this ..IShouldSource source) + where TCollection : . { } + } + public static class ShouldCollectionIsInOrderAssertionExtensions + { + public static ..ShouldAssertion BeInOrder(this ..IShouldSource source) + where TCollection : . { } + } + public static class ShouldComparisonAssertionExtensions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeGreaterThan(this ..IShouldSource source, TValue minimum, [.("minimum")] string? minimumExpression = null) + where TValue : { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeGreaterThanOrEqualTo(this ..IShouldSource source, TValue minimum, [.("minimum")] string? minimumExpression = null) + where TValue : { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeLessThan(this ..IShouldSource source, TValue maximum, [.("maximum")] string? maximumExpression = null) + where TValue : { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeLessThanOrEqualTo(this ..IShouldSource source, TValue maximum, [.("maximum")] string? maximumExpression = null) + where TValue : { } + } + public static class ShouldDateOnlyAssertionExtensions + { + public static ..ShouldAssertion<> BeFirstDayOfMonth(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInFuture(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInPast(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeLastDayOfMonth(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeLeapYear(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeOnWeekday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeOnWeekend(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeToday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeLeapYear(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeToday(this ..IShouldSource<> source) { } + } + public static class ShouldDateOnlyEqualsAssertionExtensions + { + public static ..ShouldAssertion<> BeEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldDateTimeAssertionExtensions + { + public static ..ShouldAssertion<> BeInFuture(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInFutureUtc(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInPast(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInPastUtc(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeLeapYear(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeOnWeekday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeOnWeekend(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeToday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeUtc(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeLeapYear(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeToday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeUtc(this ..IShouldSource<> source) { } + } + public static class ShouldDateTimeEqualsAssertionExtensions + { + public static ..ShouldAssertion<> BeEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldDateTimeEqualsExactAssertionExtensions + { + public static ..ShouldAssertion<> EqualExact(this ..IShouldSource<> source, expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldDateTimeOffsetAssertionExtensions + { + public static ..ShouldAssertion<> BeInFuture(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInFutureUtc(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInPast(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeInPastUtc(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeLeapYear(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeOnWeekday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeOnWeekend(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeToday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeUtc(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeLeapYear(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeToday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeUtc(this ..IShouldSource<> source) { } + } + public static class ShouldDateTimeOffsetEqualsAssertionExtensions + { + public static ..ShouldAssertion<> BeEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldDayOfWeekAssertionExtensions + { + public static ..ShouldAssertion<> BeFriday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeMonday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeWeekday(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeWeekend(this ..IShouldSource<> source) { } + } + public static class ShouldDecimalAssertions + { + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldDecimalEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, decimal expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldDecimalIsCloseToAssertionExtensions + { + public static ..ShouldAssertion BeCloseTo(this ..IShouldSource source, decimal expected, decimal tolerance, [.("expected")] string? expectedExpression = null, [.("tolerance")] string? toleranceExpression = null) { } + } + public static class ShouldDecimalIsWithinPercentOfAssertionExtensions + { + public static ..ShouldAssertion BeWithinPercentOf(this ..IShouldSource source, decimal expected, decimal percent, [.("expected")] string? expectedExpression = null, [.("percent")] string? percentExpression = null) { } + } + public static class ShouldDirectoryHasFilesAssertionExtensions + { + public static ..ShouldAssertion<.DirectoryInfo> HaveFiles(this ..IShouldSource<.DirectoryInfo> source) { } + } + public static class ShouldDirectoryHasNoSubdirectoriesAssertionExtensions + { + public static ..ShouldAssertion<.DirectoryInfo> HaveNoSubdirectories(this ..IShouldSource<.DirectoryInfo> source) { } + } + public static class ShouldDirectoryInfoAssertionExtensions + { + public static ..ShouldAssertion<.DirectoryInfo> BeEmpty(this ..IShouldSource<.DirectoryInfo> source) { } + public static ..ShouldAssertion<.DirectoryInfo> BeHidden(this ..IShouldSource<.DirectoryInfo> source) { } + public static ..ShouldAssertion<.DirectoryInfo> BeRoot(this ..IShouldSource<.DirectoryInfo> source) { } + public static ..ShouldAssertion<.DirectoryInfo> BeSystemDirectory(this ..IShouldSource<.DirectoryInfo> source) { } + public static ..ShouldAssertion<.DirectoryInfo> NotBeEmpty(this ..IShouldSource<.DirectoryInfo> source) { } + public static ..ShouldAssertion<.DirectoryInfo> NotBeHidden(this ..IShouldSource<.DirectoryInfo> source) { } + public static ..ShouldAssertion<.DirectoryInfo> NotBeRoot(this ..IShouldSource<.DirectoryInfo> source) { } + } + public static class ShouldDoubleAssertions + { + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldDoubleEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, double expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldDoubleIsCloseToAssertionExtensions + { + public static ..ShouldAssertion BeCloseTo(this ..IShouldSource source, double expected, double tolerance, [.("expected")] string? expectedExpression = null, [.("tolerance")] string? toleranceExpression = null) { } + } + public static class ShouldDoubleIsWithinPercentOfAssertionExtensions + { + public static ..ShouldAssertion BeWithinPercentOf(this ..IShouldSource source, double expected, double percent, [.("expected")] string? expectedExpression = null, [.("percent")] string? percentExpression = null) { } + } + public static class ShouldEnumAssertionExtensions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion HaveFlag(this ..IShouldSource source, TEnum expectedFlag, [.("expectedFlag")] string? expectedFlagExpression = null) + where TEnum : struct, { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion HaveSameNameAs(this ..IShouldSource source, otherEnumValue, [.("otherEnumValue")] string? otherEnumValueExpression = null) + where TEnum : struct, { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion HaveSameValueAs(this ..IShouldSource source, otherEnumValue, [.("otherEnumValue")] string? otherEnumValueExpression = null) + where TEnum : struct, { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotHaveFlag(this ..IShouldSource source, TEnum unexpectedFlag, [.("unexpectedFlag")] string? unexpectedFlagExpression = null) + where TEnum : struct, { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotHaveSameNameAs(this ..IShouldSource source, otherEnumValue, [.("otherEnumValue")] string? otherEnumValueExpression = null) + where TEnum : struct, { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotHaveSameValueAs(this ..IShouldSource source, otherEnumValue, [.("otherEnumValue")] string? otherEnumValueExpression = null) + where TEnum : struct, { } + } + public static class ShouldEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, TValue? expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, TValue? expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + } + public static class ShouldEquatableAssertionExtensions + { + public static ..ShouldAssertion BeEquatableTo(this ..IShouldSource source, TExpected expected, [.("expected")] string? expectedExpression = null) + where TActual : { } + } + public static class ShouldFileInfoAssertionExtensions + { + public static ..ShouldAssertion<.FileInfo> BeArchived(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> BeEmpty(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> BeHidden(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> BeSystemFile(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> HaveExtension(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> HaveNoExtension(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> NotBeEmpty(this ..IShouldSource<.FileInfo> source) { } + public static ..ShouldAssertion<.FileInfo> NotBeHidden(this ..IShouldSource<.FileInfo> source) { } + } + public static class ShouldFileIsNotExecutableAssertionExtensions + { + public static ..ShouldAssertion<.FileInfo> NotBeExecutable(this ..IShouldSource<.FileInfo> source) { } + } + public static class ShouldFileIsNotSystemAssertionExtensions + { + public static ..ShouldAssertion<.FileInfo> NotBeSystem(this ..IShouldSource<.FileInfo> source) { } + } + public static class ShouldFileSystemComparisonAssertions + { + public static ..ShouldAssertion<.DirectoryInfo> BeEquivalentTo(this ..IShouldSource<.DirectoryInfo> source, .DirectoryInfo expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion<.FileInfo> HaveSameContentAs(this ..IShouldSource<.FileInfo> source, .FileInfo expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion<.DirectoryInfo> HaveSameStructureAs(this ..IShouldSource<.DirectoryInfo> source, .DirectoryInfo expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion<.DirectoryInfo> NotBeEquivalentTo(this ..IShouldSource<.DirectoryInfo> source, .DirectoryInfo expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion<.FileInfo> NotHaveSameContentAs(this ..IShouldSource<.FileInfo> source, .FileInfo expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldFloatAssertions + { + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldFloatEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, float expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldFloatIsCloseToAssertionExtensions + { + public static ..ShouldAssertion BeCloseTo(this ..IShouldSource source, float expected, float tolerance, [.("expected")] string? expectedExpression = null, [.("tolerance")] string? toleranceExpression = null) { } + } + public static class ShouldFloatIsWithinPercentOfAssertionExtensions + { + public static ..ShouldAssertion BeWithinPercentOf(this ..IShouldSource source, float expected, float percent, [.("expected")] string? expectedExpression = null, [.("percent")] string? percentExpression = null) { } + } + public static class ShouldGenericAssertions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeIn(this ..IShouldSource source, T[] collection) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeIn(this ..IShouldSource source, . collection, [.("collection")] string? collectionExpression = null) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotBeIn(this ..IShouldSource source, T[] collection) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotBeIn(this ..IShouldSource source, . collection, [.("collection")] string? collectionExpression = null) { } + } + public static class ShouldGuidAssertionExtensions + { + public static ..ShouldAssertion<> BeEmptyGuid(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeEmptyGuid(this ..IShouldSource<> source) { } + } + public static class ShouldHasSingleItemAssertionExtensions + { + public static ..ShouldAssertion HaveSingleItem(this ..IShouldSource source) + where TCollection : . { } + } + public static class ShouldHasSingleItemPredicateAssertionExtensions + { + public static ..ShouldAssertion HaveSingleItem(this ..IShouldSource source, predicate, string predicateDescription, [.("predicate")] string? predicateExpression = null, [.("predicateDescription")] string? predicateDescriptionExpression = null) + where TCollection : . { } + } + public static class ShouldHttpStatusCodeAssertionExtensions + { + public static ..ShouldAssertion<.HttpStatusCode> BeClientError(this ..IShouldSource<.HttpStatusCode> source) { } + public static ..ShouldAssertion<.HttpStatusCode> BeError(this ..IShouldSource<.HttpStatusCode> source) { } + public static ..ShouldAssertion<.HttpStatusCode> BeInformational(this ..IShouldSource<.HttpStatusCode> source) { } + public static ..ShouldAssertion<.HttpStatusCode> BeRedirection(this ..IShouldSource<.HttpStatusCode> source) { } + public static ..ShouldAssertion<.HttpStatusCode> BeServerError(this ..IShouldSource<.HttpStatusCode> source) { } + public static ..ShouldAssertion<.HttpStatusCode> BeSuccess(this ..IShouldSource<.HttpStatusCode> source) { } + public static ..ShouldAssertion<.HttpStatusCode> NotBeSuccess(this ..IShouldSource<.HttpStatusCode> source) { } + } + public static class ShouldIntAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldIntEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, int expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldIntIsCloseToAssertionExtensions + { + public static ..ShouldAssertion BeCloseTo(this ..IShouldSource source, int expected, int tolerance, [.("expected")] string? expectedExpression = null, [.("tolerance")] string? toleranceExpression = null) { } + } + public static class ShouldIntIsWithinPercentOfAssertionExtensions + { + public static ..ShouldAssertion BeWithinPercentOf(this ..IShouldSource source, int expected, double percent, [.("expected")] string? expectedExpression = null, [.("percent")] string? percentExpression = null) { } + } + public static class ShouldIsDefaultAssertionExtensions + { + public static ..ShouldAssertion BeDefault(this ..IShouldSource source) + where TValue : struct { } + } + public static class ShouldIsDefaultNullableAssertionExtensions + { + public static ..ShouldAssertion BeDefault(this ..IShouldSource source) + where TValue : struct { } + } + public static class ShouldIsDefaultReferenceAssertionExtensions + { + public static ..ShouldAssertion BeDefault(this ..IShouldSource source) + where TValue : class { } + } + public static class ShouldIsEquatableOrEqualToAssertionExtensions + { + public static ..ShouldAssertion BeEquatableOrEqualTo(this ..IShouldSource source, TValue expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldIsEquivalentToAssertionExtensions + { + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] + public static ..ShouldAssertion BeEquivalentTo(this ..IShouldSource source, . expected, . ordering = 0, [.("expected")] string? expectedExpression = null, [.("ordering")] string? orderingExpression = null) + where TCollection : . { } + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] + public static ..ShouldAssertion BeEquivalentTo(this ..IShouldSource source, . expected, . comparer, . ordering = 0, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null, [.("ordering")] string? orderingExpression = null) + where TCollection : . { } + } + public static class ShouldIsNotDefaultAssertionExtensions + { + public static ..ShouldAssertion NotBeDefault(this ..IShouldSource source) + where TValue : struct { } + } + public static class ShouldIsNotDefaultNullableAssertionExtensions + { + public static ..ShouldAssertion NotBeDefault(this ..IShouldSource source) + where TValue : struct { } + } + public static class ShouldIsNotDefaultReferenceAssertionExtensions + { + public static ..ShouldAssertion NotBeDefault(this ..IShouldSource source) + where TValue : class { } + } + public static class ShouldJsonElementAssertionExtensions + { + public static ..ShouldAssertion<.> BeArray(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> BeBoolean(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> BeDeepEqualTo(this ..IShouldSource<.> source, . expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion<.> BeNull(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> BeNumber(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> BeObject(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> BeString(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> HaveProperty(this ..IShouldSource<.> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { } + public static ..ShouldAssertion<.> NotBeDeepEqualTo(this ..IShouldSource<.> source, . expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion<.> NotBeNull(this ..IShouldSource<.> source) { } + public static ..ShouldAssertion<.> NotHaveProperty(this ..IShouldSource<.> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { } + } + public static class ShouldJsonStringAssertionExtensions + { + public static ..ShouldAssertion BeValidJson(this ..IShouldSource source) { } + public static ..ShouldAssertion BeValidJsonArray(this ..IShouldSource source) { } + public static ..ShouldAssertion BeValidJsonObject(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeValidJson(this ..IShouldSource source) { } + } + public static class ShouldLazyAssertionExtensions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion<> BeValueCreated(this ..IShouldSource<> source) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion<> BeValueNotCreated(this ..IShouldSource<> source) { } + } + public static class ShouldLongAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldLongEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, long expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldLongIsCloseToAssertionExtensions + { + public static ..ShouldAssertion BeCloseTo(this ..IShouldSource source, long expected, long tolerance, [.("expected")] string? expectedExpression = null, [.("tolerance")] string? toleranceExpression = null) { } + } + public static class ShouldLongIsWithinPercentOfAssertionExtensions + { + public static ..ShouldAssertion BeWithinPercentOf(this ..IShouldSource source, long expected, double percent, [.("expected")] string? expectedExpression = null, [.("percent")] string? percentExpression = null) { } + } + public static class ShouldNotEqualsAssertionExtensions + { + public static ..ShouldAssertion NotBeEqualTo(this ..IShouldSource source, TValue notExpected, .? comparer = null, [.("notExpected")] string? notExpectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + } + public static class ShouldNotEquivalentToAssertionExtensions + { + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] + public static ..ShouldAssertion NotBeEquivalentTo(this ..IShouldSource source, . notExpected, . ordering = 0, [.("notExpected")] string? notExpectedExpression = null, [.("ordering")] string? orderingExpression = null) + where TCollection : . { } + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] + public static ..ShouldAssertion NotBeEquivalentTo(this ..IShouldSource source, . notExpected, . comparer, . ordering = 0, [.("notExpected")] string? notExpectedExpression = null, [.("comparer")] string? comparerExpression = null, [.("ordering")] string? orderingExpression = null) + where TCollection : . { } + } + public static class ShouldNullAssertionExtension + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeNull(this ..IShouldSource source) { } + } + public static class ShouldNullableEquatableAssertionExtensions + { + public static ..ShouldAssertion BeEquatableTo(this ..IShouldSource source, TExpected expected, [.("expected")] string? expectedExpression = null) + where TActual : struct, { } + } + public static class ShouldRangeAssertionExtensions + { + [("Use IsFullRange instead.")] + public static ..ShouldAssertion<> BeAll(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeFullRange(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveBothIndicesFromEnd(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveEndFromBeginning(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveStartFromBeginning(this ..IShouldSource<> source) { } + } + public static class ShouldReferenceAssertionExtensions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeSameReferenceAs(this ..IShouldSource source, object? expected, [.("expected")] string? expectedExpression = null) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotBeSameReferenceAs(this ..IShouldSource source, object? expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldSbyteAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldShortAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldStrictEqualityAssertions + { + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion BeStrictlyEqualTo(this ..IShouldSource source, T expected, [.("expected")] string? expectedExpression = null) { } + [.("Trimming", "IL2091", Justification="Forwarded from source method")] + public static ..ShouldAssertion NotBeStrictlyEqualTo(this ..IShouldSource source, T expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldStringBuilderAssertionExtensions + { + public static ..ShouldAssertion<.StringBuilder> BeEmpty(this ..IShouldSource<.StringBuilder> source) { } + public static ..ShouldAssertion<.StringBuilder> HaveExcessCapacity(this ..IShouldSource<.StringBuilder> source) { } + public static ..ShouldAssertion<.StringBuilder> NotBeEmpty(this ..IShouldSource<.StringBuilder> source) { } + } + public static class ShouldStringContainsAssertionExtensions + { + public static ..ShouldAssertion Contain(this ..IShouldSource source, string expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion Contain(this ..IShouldSource source, string expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldStringDoesNotContainAssertionExtensions + { + public static ..ShouldAssertion NotContain(this ..IShouldSource source, string expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion NotContain(this ..IShouldSource source, string expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldStringDoesNotEndWithAssertionExtensions + { + public static ..ShouldAssertion NotEndWith(this ..IShouldSource source, string expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion NotEndWith(this ..IShouldSource source, string expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldStringDoesNotMatchAssertionExtensions + { + public static ..ShouldAssertion NotMatch(this ..IShouldSource source, . regex, [.("regex")] string? regexExpression = null) { } + public static ..ShouldAssertion NotMatch(this ..IShouldSource source, string pattern, [.("pattern")] string? patternExpression = null) { } + } + public static class ShouldStringDoesNotStartWithAssertionExtensions + { + public static ..ShouldAssertion NotStartWith(this ..IShouldSource source, string expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion NotStartWith(this ..IShouldSource source, string expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldStringEmptyAssertionExtensions + { + public static ..ShouldAssertion BeEmpty(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeEmpty(this ..IShouldSource source) { } + } + public static class ShouldStringEndsWithAssertionExtensions + { + public static ..ShouldAssertion EndWith(this ..IShouldSource source, string expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion EndWith(this ..IShouldSource source, string expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldStringEqualsAssertionExtensions + { + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, string? expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, string? expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } + public static ..ShouldAssertion BeEqualTo(this ..IShouldSource source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldStringLengthRangeAssertionExtensions + { + public static ..ShouldAssertion HaveLengthBetween(this ..IShouldSource source, int minLength, int maxLength, [.("minLength")] string? minLengthExpression = null, [.("maxLength")] string? maxLengthExpression = null) { } + public static ..ShouldAssertion HaveMaxLength(this ..IShouldSource source, int maxLength, [.("maxLength")] string? maxLengthExpression = null) { } + public static ..ShouldAssertion HaveMinLength(this ..IShouldSource source, int minLength, [.("minLength")] string? minLengthExpression = null) { } + } + public static class ShouldStringStartsWithAssertionExtensions + { + public static ..ShouldAssertion StartWith(this ..IShouldSource source, string expected, [.("expected")] string? expectedExpression = null) { } + public static ..ShouldAssertion StartWith(this ..IShouldSource source, string expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { } + } + public static class ShouldTimeOnlyAssertionExtensions + { + public static ..ShouldAssertion<> BeAM(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeEndOfHour(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeMidnight(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeNoon(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BePM(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeStartOfHour(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeMidnight(this ..IShouldSource<> source) { } + } + public static class ShouldTimeOnlyEqualsAssertionExtensions + { + public static ..ShouldAssertion<> BeEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldTimeSpanAssertionExtensions + { + public static ..ShouldAssertion<> BeNegative(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeNonNegative(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeNonPositive(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BePositive(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> BeZero(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeZero(this ..IShouldSource<> source) { } + } + public static class ShouldTimeSpanEqualsAssertionExtensions + { + public static ..ShouldAssertion<> BeEqualTo(this ..IShouldSource<> source, expected, [.("expected")] string? expectedExpression = null) { } + } + public static class ShouldUintAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldUlongAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldUshortAssertions + { + public static ..ShouldAssertion BeEven(this ..IShouldSource source) { } + public static ..ShouldAssertion BeOdd(this ..IShouldSource source) { } + public static ..ShouldAssertion BeZero(this ..IShouldSource source) { } + public static ..ShouldAssertion NotBeZero(this ..IShouldSource source) { } + } + public static class ShouldVersionAssertionExtensions + { + public static ..ShouldAssertion<> BeMajorVersion(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveBuildNumber(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveNoBuildNumber(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveNoRevisionNumber(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> HaveRevisionNumber(this ..IShouldSource<> source) { } + public static ..ShouldAssertion<> NotBeMajorVersion(this ..IShouldSource<> source) { } + } +} +namespace .Should +{ + public static class ShouldExtensions + { + public static ..ShouldDelegateSource Should(this action, [.("action")] string? expression = null) { } + public static ..ShouldDelegateSource Should(this <.> action, [.("action")] string? expression = null) { } + [.(2)] + public static ..ShouldSource Should(this string? value, [.("value")] string? expression = null) { } + [.(1)] + public static ..ShouldCollectionSource Should(this .? value, [.("value")] string? expression = null) { } + public static ..ShouldDelegateSource Should(this <.> func, [.("func")] string? expression = null) { } + public static ..ShouldDelegateSource Should(this func, [.("func")] string? expression = null) { } + public static ..ShouldSource Should(this T? value, [.("value")] string? expression = null) { } + } +} \ No newline at end of file diff --git a/TUnit.PublicAPI/Tests.cs b/TUnit.PublicAPI/Tests.cs index 4063dcb3e7..4bef708f00 100644 --- a/TUnit.PublicAPI/Tests.cs +++ b/TUnit.PublicAPI/Tests.cs @@ -20,6 +20,10 @@ public Task Playwright_Library_Has_No_API_Changes() => VerifyPublicApi(typeof(Playwright.PageTest).Assembly); #if NET + [Test] + public Task Should_Library_Has_No_API_Changes() + => VerifyPublicApi(typeof(Assertions.Should.ShouldExtensions).Assembly); + [Test] public Task OpenTelemetry_Library_Has_No_API_Changes() => VerifyPublicApi(typeof(TUnit.OpenTelemetry.TUnitOpenTelemetry).Assembly); diff --git a/TUnit.slnx b/TUnit.slnx index e5eae1e0f4..cd6de0948c 100644 --- a/TUnit.slnx +++ b/TUnit.slnx @@ -11,6 +11,7 @@ + @@ -49,6 +50,7 @@ + @@ -66,6 +68,8 @@ + + diff --git a/docs/docs/assertions/getting-started.md b/docs/docs/assertions/getting-started.md index 678aaf806d..4c97656b4d 100644 --- a/docs/docs/assertions/getting-started.md +++ b/docs/docs/assertions/getting-started.md @@ -33,6 +33,8 @@ Assert.That(result).IsEqualTo(42); A built-in analyzer warns if you forget. See [Awaiting Assertions](awaiting.md) for more examples and design rationale. +> Prefer `value.Should().BeEqualTo(...)`? Install the optional [`TUnit.Assertions.Should`](should-syntax.md) package — it layers a FluentAssertions-style entry surface on top of the same assertion infrastructure. Both syntaxes coexist in the same project. + ## Assertion Categories TUnit provides assertions for all common scenarios: diff --git a/docs/docs/assertions/should-syntax.md b/docs/docs/assertions/should-syntax.md new file mode 100644 index 0000000000..78e54bb395 --- /dev/null +++ b/docs/docs/assertions/should-syntax.md @@ -0,0 +1,186 @@ +--- +sidebar_position: 1.5 +title: Should Syntax (Optional) +description: FluentAssertions-style value.Should().BeEqualTo() syntax via the optional TUnit.Assertions.Should NuGet package. +--- + +# Should Syntax + +`TUnit.Assertions.Should` is an **optional** add-on package that exposes a FluentAssertions-style entry surface — `value.Should().BeEqualTo(...)` — on top of `TUnit.Assertions`. It's a thin layer: every Should-flavored method is generated from an existing `TUnit.Assertions` assertion, so behaviour, error messages, and async semantics are identical. + +This page is for users coming from FluentAssertions / Shouldly who prefer the `Should()` cadence. The default `Assert.That(...)` syntax remains the canonical TUnit style — the two are interchangeable and don't compete. + +## Installation + +```bash +dotnet add package TUnit.Assertions.Should --prerelease +``` + +The package is published in **beta** — versions are stamped `{semver}-beta` until the API stabilises. + +It depends on `TUnit.Assertions` directly. You can keep `Assert.That(...)` and `value.Should()` in the same project; the two share their underlying assertion infrastructure (context, expression builder, async pipeline) so chains and failure messages compose cleanly. + +## Quick Comparison + +| `Assert.That` style | `Should()` style | +|---|---| +| `await Assert.That(value).IsEqualTo(5)` | `await value.Should().BeEqualTo(5)` | +| `await Assert.That(value).IsNotNull()` | `await value.Should().NotBeNull()` | +| `await Assert.That(text).Contains("foo")` | `await text.Should().Contain("foo")` | +| `await Assert.That(text).StartsWith("hi")` | `await text.Should().StartWith("hi")` | +| `await Assert.That(list).Contains(item)` | `await list.Should().Contain(item)` | +| `await Assert.That(list).IsInOrder()` | `await list.Should().BeInOrder()` | +| `await Assert.That(list).All(p)` | `await list.Should().All(p)` | +| `await Assert.That(() => Foo()).Throws()` | `await ((Action)(() => Foo())).Should().Throw()` | + +## Naming Rules + +The package's source generator scans every `[AssertionExtension]`, `[GenerateAssertion]`, and `[AssertionFrom]` declaration in the referenced `TUnit.Assertions` assembly and emits a Should-flavored counterpart with a conjugated method name: + +| Original | Should-flavored | Rule | +|---|---|---| +| `Is*` | `Be*` | `IsEqualTo` → `BeEqualTo`, `IsZero` → `BeZero` | +| `IsNot*` | `NotBe*` | `IsNotNull` → `NotBeNull`, `IsNotEqualTo` → `NotBeEqualTo` | +| `Has*` | `Have*` | `HasCount` → `HaveCount`, `HasFiles` → `HaveFiles` | +| `DoesNot*` | `Not*` | `DoesNotContain` → `NotContain`, `DoesNotMatch` → `NotMatch` | +| `Does*` | (strip prefix) | `DoesMatch` → `Match` | +| 3rd-person singular `*s` | (drop trailing `-s`) | `Contains` → `Contain`, `StartsWith` → `StartWith`, `Throws` → `Throw` | + +Boundary-aware: `Issue` doesn't become `Besue`. The first word of the method name is matched against the rule, not the substring. + +The `Does*` strip rule reads naturally for verbs (`DoesMatch` → `Match`, `DoesContain` → `Contain`) but can produce awkward names where the auxiliary `does` was load-bearing — for example `DoesRun` → `Run` or `DoesLoad` → `Load`. If the conjugated name reads poorly, decorate the assertion class with `[ShouldName("...")]` to override it. The same applies to non-verb `-es` words (the `Match`/`Wash`/`Fix` family is handled by the sibilant-aware `-es` drop, but exotic stems may still need an override). + +### Custom names + +For irregulars or when the conjugation produces an unwanted name, decorate the assertion class with `[ShouldName("...")]`. The override is consulted before the conjugation rules: + +```csharp +[AssertionExtension("IsOdd")] +[ShouldName("BeAnOddNumber")] +public class OddAssertion : Assertion { … } +``` + +`[AssertionExtension(NegatedMethodName = "...")]` produces a second extension method for the negated form, which the Should generator picks up and conjugates independently — `Contains` → `Contain` and `DoesNotContain` → `NotContain` come out automatically without any `[ShouldName]`. When TUnit's pattern uses **separate classes** for positive and negated forms (e.g. `EqualsAssertion` + `NotEqualsAssertion`), place a separate `[ShouldName]` on each: + +```csharp +[AssertionExtension("IsBetween")] +[ShouldName("BeWithinRange")] +public class BetweenAssertion : Assertion { … } + +[AssertionExtension("IsNotBetween")] +[ShouldName("NotBeWithinRange")] +public class NotBetweenAssertion : Assertion { … } +``` + +## Entry Points + +Each entry overload returns a wrapper appropriate to the source type: + +```csharp +// Value entry — returns ShouldSource +await 42.Should().BeEqualTo(42); +await "hello".Should().Contain("ell"); +await someObject.Should().BeOfType(); + +// Collection entry — returns ShouldCollectionSource +// exposes element-typed instance methods (BeInOrder, All, Any, +// HaveSingleItem, HaveDistinctItems) without explicit type arguments +var list = new List { 1, 2, 3 }; +await list.Should().BeInOrder(); +await list.Should().All(x => x > 0); +await list.Should().Contain(2); + +// Delegate entry — returns ShouldDelegateSource +// exposes Throw / ThrowExactly directly +Action act = () => throw new InvalidOperationException(); +await act.Should().Throw(); + +Func> asyncFunc = () => Task.FromResult(42); +await asyncFunc.Should().BeEqualTo(42); +``` + +## Chaining + +`.And` and `.Or` continuations stay Should-flavored end-to-end — the chain types only expose the Should naming, so you can't accidentally drop back to `Is*`/`Has*` mid-chain: + +```csharp +await value + .Should().BeEqualTo(5) + .And.NotBeEqualTo(7) + .And.BeBetween(1, 10); + +await statusCode + .Should().BeEqualTo(200) + .Or.BeEqualTo(201) + .Or.BeEqualTo(204); +``` + +Mixing `.And` and `.Or` without explicit grouping throws `MixedAndOrAssertionsException` at runtime — the analyzer flags it at compile time too. + +## Assert.Multiple + +Works unchanged. Should-flavored assertions share the underlying `AssertionScope`: + +```csharp +using (Assert.Multiple()) +{ + await user.FirstName.Should().BeEqualTo("Alice"); + await user.LastName.Should().BeEqualTo("Smith"); + await user.Age.Should().BeGreaterThan(18); +} +``` + +## Because + +Add a justification message that appears in failure output: + +```csharp +await score.Should().BeGreaterThan(70).Because("passing grade required"); +``` + +## User-defined assertions + +Any assertion you write with `[AssertionExtension]`, `[GenerateAssertion]`, or `[AssertionFrom]` automatically gets a Should counterpart. No additional wiring needed. + +```csharp +[AssertionExtension("IsOdd")] +public sealed class OddAssertion : Assertion +{ + public OddAssertion(AssertionContext context) : base(context) { } + protected override Task CheckAsync(EvaluationMetadata metadata) + => Task.FromResult(metadata.Value % 2 != 0 + ? AssertionResult.Passed + : AssertionResult.Failed($"{metadata.Value} is even")); + protected override string GetExpectation() => "to be odd"; +} + +// Usage: +await 3.Should().BeOdd(); +``` + +## Analyzer support + +The existing TUnit assertion analyzers also recognise the Should syntax: + +- `TUnitAssertions0002` (assertion not awaited) fires for unawaited `value.Should().X()` chains. +- `TUnitAssertions0001` (mixing And/Or) fires for mixed Should chains. +- The nullability suppressor recognises `value.Should().NotBeNull()` and suppresses CS8600/CS8602/CS8604/CS8618/CS8629 on the asserted variable in subsequent statements. + +All checks are scoped to TUnit namespaces — unrelated `Should()` extensions in other libraries (e.g. FluentAssertions, custom user code) don't trigger any of these diagnostics. + +## FluentAssertions coexistence + +Both libraries declare a `Should()` extension on `T` (and string, IEnumerable, etc.). If a project references both: + +- An ambiguity (CS0121) at the `Should()` call site forces you to pick one. +- The conjugated names downstream are different (`BeEqualTo` vs FA's `Be`), so once you've passed the entry, the two surfaces don't collide. + +For migration projects that want to flip incrementally, prefer one-file-at-a-time using directives or a global using alias to disambiguate the entry point. + +## Limitations + +- **Methods with method-level generic parameters** on collection wrappers (e.g. `IsAssignableTo`, `IsTypeOf` reached via `.Should()` on a collection) are not source-generated; use `Assert.That(value).IsAssignableTo()` instead. +- **Cross-type extensions** (assertions whose source type differs from their assertion's value type, like `IsEqualTo` accepting an implicit-conversion target) skip the Should generation — use `Assert.That(...)` for those. +- **Three predicate-overload collection methods** (`All`, `Any`, `HaveSingleItem(predicate)`) are hand-written rather than generated because their target ctors require a literal-fallback string the simple-factory template can't supply. + +These limitations don't apply to the underlying assertions — `Assert.That(...)` covers everything.