From 20bdb042b3dc39fbb092f3f7dfb056e41a1cbd5a Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 26 Apr 2026 22:21:43 +0100 Subject: [PATCH 01/10] fix(assertions): preserve item-at source types Closes #5706 --- TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs | 88 +++++ TUnit.Assertions/Conditions/ListAssertions.cs | 20 +- .../Conditions/ReadOnlyListAssertions.cs | 20 +- .../ListItemAtSatisfiesExtensions.cs | 311 ++++++++++++++++++ ...Has_No_API_Changes.DotNet10_0.verified.txt | 53 +++ ..._Has_No_API_Changes.DotNet8_0.verified.txt | 53 +++ ..._Has_No_API_Changes.DotNet9_0.verified.txt | 53 +++ ...ary_Has_No_API_Changes.Net4_7.verified.txt | 49 +++ 8 files changed, 641 insertions(+), 6 deletions(-) create mode 100644 TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs create mode 100644 TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs diff --git a/TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs b/TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs new file mode 100644 index 0000000000..48c13fb947 --- /dev/null +++ b/TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs @@ -0,0 +1,88 @@ +namespace TUnit.Assertions.Tests.Bugs; + +/// +/// Regression tests for GitHub issue #5706: +/// ItemAt(...).Satisfies(...) should preserve specialised assertion sources for +/// collection-like item values instead of exposing only IAssertionSource<TItem>. +/// +public class Issue5706Tests +{ + [Test] + public async Task List_ItemAt_Satisfies_Preserves_String_Item_Source() + { + IList items = new List { "alpha" }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Contains("pha")); + } + + [Test] + public async Task List_ItemAt_Satisfies_Preserves_Collection_Item_Source() + { + IList> items = new List> { new[] { 1, 2, 3 } }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task List_ItemAt_Satisfies_Preserves_Dictionary_Item_Source() + { + IList> items = new List> + { + new Dictionary { ["answer"] = 42 } + }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.ContainsKey("answer")); + } + + [Test] + public async Task List_ItemAt_Satisfies_Preserves_Set_Item_Source() + { + IList> items = new List> + { + new HashSet { 1, 2, 3 } + }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.IsSupersetOf(new[] { 1, 2 })); + } + + [Test] + public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_String_Item_Source() + { + IReadOnlyList items = new List { "alpha" }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Contains("pha")); + } + + [Test] + public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_Collection_Item_Source() + { + IReadOnlyList> items = new List> + { + new() { 1, 2, 3 } + }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_Dictionary_Item_Source() + { + IReadOnlyList> items = new List> + { + new() { ["answer"] = 42 } + }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.ContainsKey("answer")); + } + + [Test] + public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_Set_Item_Source() + { + IReadOnlyList> items = new List> + { + new() { 1, 2, 3 } + }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.IsSupersetOf(new[] { 1, 2 })); + } +} diff --git a/TUnit.Assertions/Conditions/ListAssertions.cs b/TUnit.Assertions/Conditions/ListAssertions.cs index 904de2ad88..7b534956bd 100644 --- a/TUnit.Assertions/Conditions/ListAssertions.cs +++ b/TUnit.Assertions/Conditions/ListAssertions.cs @@ -67,6 +67,10 @@ public class ListItemAtSource : IAssertionSource private readonly AssertionContext _listContext; private readonly int _index; + internal AssertionContext InternalListContext => _listContext; + + internal int InternalIndex => _index; + public AssertionContext Context { get; } public ListItemAtSource(AssertionContext listContext, int index) @@ -420,12 +424,23 @@ public class ListItemAtSatisfiesAssertion : ListAssertionBase { private readonly int _index; - private readonly Func, Assertion?> _assertion; + private readonly Func _assertion; public ListItemAtSatisfiesAssertion( AssertionContext context, int index, Func, Assertion?> assertion) + : this( + context, + index, + (item, itemIndex) => assertion(new ValueAssertion(item, $"item[{itemIndex}]"))) + { + } + + internal ListItemAtSatisfiesAssertion( + AssertionContext context, + int index, + Func assertion) : base(context) { _index = index; @@ -451,8 +466,7 @@ protected override async Task CheckAsync(EvaluationMetadata(actualItem, $"item[{_index}]"); - var resultingAssertion = _assertion(itemSource); + var resultingAssertion = _assertion(actualItem, _index); if (resultingAssertion != null) { diff --git a/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs b/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs index 5d91d94373..40397828f5 100644 --- a/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs +++ b/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs @@ -69,6 +69,10 @@ public class ReadOnlyListItemAtSource : IAssertionSource private readonly AssertionContext _listContext; private readonly int _index; + internal AssertionContext InternalListContext => _listContext; + + internal int InternalIndex => _index; + public AssertionContext Context { get; } public ReadOnlyListItemAtSource(AssertionContext listContext, int index) @@ -422,12 +426,23 @@ public class ReadOnlyListItemAtSatisfiesAssertion : ReadOnlyListAs where TList : IReadOnlyList { private readonly int _index; - private readonly Func, Assertion?> _assertion; + private readonly Func _assertion; public ReadOnlyListItemAtSatisfiesAssertion( AssertionContext context, int index, Func, Assertion?> assertion) + : this( + context, + index, + (item, itemIndex) => assertion(new ValueAssertion(item, $"item[{itemIndex}]"))) + { + } + + internal ReadOnlyListItemAtSatisfiesAssertion( + AssertionContext context, + int index, + Func assertion) : base(context) { _index = index; @@ -453,8 +468,7 @@ protected override async Task CheckAsync(EvaluationMetadata(actualItem, $"item[{_index}]"); - var resultingAssertion = _assertion(itemSource); + var resultingAssertion = _assertion(actualItem, _index); if (resultingAssertion != null) { diff --git a/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs b/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs new file mode 100644 index 0000000000..177257fc0a --- /dev/null +++ b/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs @@ -0,0 +1,311 @@ +using System.Runtime.CompilerServices; +using TUnit.Assertions.Conditions; +using TUnit.Assertions.Core; +using TUnit.Assertions.Sources; + +namespace TUnit.Assertions.Extensions; + +/// +/// Specialised overloads for ItemAt(...).Satisfies(...) when the item is itself +/// a collection-like value. +/// +public static class ListItemAtSatisfiesExtensions +{ + public static ListItemAtSatisfiesAssertion> Satisfies( + this ListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new CollectionAssertion(item, $"item[{index}]")), + expression); + } + + public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( + this ReadOnlyListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new CollectionAssertion(item, $"item[{index}]")), + expression); + } + + public static ListItemAtSatisfiesAssertion> Satisfies( + this ListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new ListAssertion(item, $"item[{index}]")), + expression); + } + + public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( + this ReadOnlyListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new ListAssertion(item, $"item[{index}]")), + expression); + } + + public static ListItemAtSatisfiesAssertion> Satisfies( + this ListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new ReadOnlyListAssertion(item, $"item[{index}]")), + expression); + } + + public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( + this ReadOnlyListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new ReadOnlyListAssertion(item, $"item[{index}]")), + expression); + } + + public static ListItemAtSatisfiesAssertion> Satisfies( + this ListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + where TKey : notnull + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new DictionaryAssertion(item, $"item[{index}]")), + expression); + } + + public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( + this ReadOnlyListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + where TKey : notnull + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new DictionaryAssertion(item, $"item[{index}]")), + expression); + } + + public static ListItemAtSatisfiesAssertion> Satisfies( + this ListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + where TKey : notnull + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new MutableDictionaryAssertion(item, $"item[{index}]")), + expression); + } + + public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( + this ReadOnlyListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + where TKey : notnull + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new MutableDictionaryAssertion(item, $"item[{index}]")), + expression); + } + + public static ListItemAtSatisfiesAssertion> Satisfies( + this ListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new SetAssertion(item, $"item[{index}]")), + expression); + } + + public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( + this ReadOnlyListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new SetAssertion(item, $"item[{index}]")), + expression); + } + +#if NET5_0_OR_GREATER + public static ListItemAtSatisfiesAssertion> Satisfies( + this ListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new ReadOnlySetAssertion(item, $"item[{index}]")), + expression); + } + + public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( + this ReadOnlyListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new ReadOnlySetAssertion(item, $"item[{index}]")), + expression); + } +#endif + + public static ListItemAtSatisfiesAssertion Satisfies( + this ListItemAtSource source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new ArrayAssertion(item, $"item[{index}]")), + expression); + } + + public static ReadOnlyListItemAtSatisfiesAssertion Satisfies( + this ReadOnlyListItemAtSource source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new ArrayAssertion(item, $"item[{index}]")), + expression); + } + + public static ListItemAtSatisfiesAssertion> Satisfies( + this ListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new ListAssertion(item, $"item[{index}]")), + expression); + } + + public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( + this ReadOnlyListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new ListAssertion(item, $"item[{index}]")), + expression); + } + + public static ListItemAtSatisfiesAssertion> Satisfies( + this ListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new HashSetAssertion(item, $"item[{index}]")), + expression); + } + + public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( + this ReadOnlyListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new HashSetAssertion(item, $"item[{index}]")), + expression); + } + + public static ListItemAtSatisfiesAssertion> Satisfies( + this ListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + where TKey : notnull + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new MutableDictionaryAssertion(item, $"item[{index}]")), + expression); + } + + public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( + this ReadOnlyListItemAtSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + where TKey : notnull + { + return SatisfiesSpecialised( + source, + (item, index) => assertion(new MutableDictionaryAssertion(item, $"item[{index}]")), + expression); + } + + private static ListItemAtSatisfiesAssertion SatisfiesSpecialised( + ListItemAtSource source, + Func assertionFactory, + string? expression) + where TList : IList + { + source.InternalListContext.ExpressionBuilder.Append($".Satisfies({expression})"); + return new ListItemAtSatisfiesAssertion( + source.InternalListContext, + source.InternalIndex, + assertionFactory); + } + + private static ReadOnlyListItemAtSatisfiesAssertion SatisfiesSpecialised( + ReadOnlyListItemAtSource source, + Func assertionFactory, + string? expression) + where TList : IReadOnlyList + { + source.InternalListContext.ExpressionBuilder.Append($".Satisfies({expression})"); + return new ReadOnlyListItemAtSatisfiesAssertion( + source.InternalListContext, + source.InternalIndex, + assertionFactory); + } +} 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 1923ab110f..6655d53a00 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 @@ -4715,6 +4715,59 @@ namespace .Extensions protected override .<.> CheckAsync(.<> metadata) { } protected override string GetExpectation() { } } + public static class ListItemAtSatisfiesExtensions + { + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : . { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : . { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + } public static class LongAssertions { public static ._IsEven_Assertion IsEven(this . source) { } 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 c184337bb5..b58574b7ef 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 @@ -4650,6 +4650,59 @@ namespace .Extensions protected override .<.> CheckAsync(.<> metadata) { } protected override string GetExpectation() { } } + public static class ListItemAtSatisfiesExtensions + { + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : . { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : . { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + } public static class LongAssertions { public static ._IsEven_Assertion IsEven(this . source) { } 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 c49c22d3f6..3c50d332ea 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 @@ -4715,6 +4715,59 @@ namespace .Extensions protected override .<.> CheckAsync(.<> metadata) { } protected override string GetExpectation() { } } + public static class ListItemAtSatisfiesExtensions + { + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : . { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : . { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + } public static class LongAssertions { public static ._IsEven_Assertion IsEven(this . source) { } diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt index 8b9183194a..5259b2f5f2 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -4143,6 +4143,55 @@ namespace .Extensions protected override .<.> CheckAsync(.<> metadata) { } protected override string GetExpectation() { } } + public static class ListItemAtSatisfiesExtensions + { + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : . { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : . { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + } public static class LongAssertions { public static ._IsEven_Assertion IsEven(this . source) { } From ce722f924b23c034bb336247d036b7074cb3cd6f Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:58:13 +0100 Subject: [PATCH 02/10] refactor(assertions): hide item-at internals --- TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs | 60 +++++++++++++++++++ TUnit.Assertions/Conditions/ListAssertions.cs | 13 ++-- .../Conditions/ReadOnlyListAssertions.cs | 13 ++-- .../ListItemAtSatisfiesExtensions.cs | 12 +--- 4 files changed, 80 insertions(+), 18 deletions(-) diff --git a/TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs b/TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs index 48c13fb947..0dd2f2a945 100644 --- a/TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs +++ b/TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs @@ -23,6 +23,14 @@ public async Task List_ItemAt_Satisfies_Preserves_Collection_Item_Source() await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); } + [Test] + public async Task List_ItemAt_Satisfies_Preserves_Array_Item_Source() + { + IList items = new List { new[] { 1, 2, 3 } }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); + } + [Test] public async Task List_ItemAt_Satisfies_Preserves_Dictionary_Item_Source() { @@ -34,6 +42,17 @@ public async Task List_ItemAt_Satisfies_Preserves_Dictionary_Item_Source() await Assert.That(items).ItemAt(0).Satisfies(item => item.ContainsKey("answer")); } + [Test] + public async Task List_ItemAt_Satisfies_Preserves_Concrete_Dictionary_Item_Source() + { + IList> items = new List> + { + new() { ["answer"] = 42 } + }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.ContainsKey("answer")); + } + [Test] public async Task List_ItemAt_Satisfies_Preserves_Set_Item_Source() { @@ -45,6 +64,17 @@ public async Task List_ItemAt_Satisfies_Preserves_Set_Item_Source() await Assert.That(items).ItemAt(0).Satisfies(item => item.IsSupersetOf(new[] { 1, 2 })); } + [Test] + public async Task List_ItemAt_Satisfies_Preserves_HashSet_Item_Source() + { + IList> items = new List> + { + new() { 1, 2, 3 } + }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.IsSupersetOf(new[] { 1, 2 })); + } + [Test] public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_String_Item_Source() { @@ -64,6 +94,14 @@ public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_Collection_Item_Source await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); } + [Test] + public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_Array_Item_Source() + { + IReadOnlyList items = new List { new[] { 1, 2, 3 } }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); + } + [Test] public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_Dictionary_Item_Source() { @@ -75,6 +113,17 @@ public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_Dictionary_Item_Source await Assert.That(items).ItemAt(0).Satisfies(item => item.ContainsKey("answer")); } + [Test] + public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_ReadOnly_Dictionary_Item_Source() + { + IReadOnlyList> items = new List> + { + new Dictionary { ["answer"] = 42 } + }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.ContainsKey("answer")); + } + [Test] public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_Set_Item_Source() { @@ -85,4 +134,15 @@ public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_Set_Item_Source() await Assert.That(items).ItemAt(0).Satisfies(item => item.IsSupersetOf(new[] { 1, 2 })); } + + [Test] + public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_ReadOnly_Set_Item_Source() + { + IReadOnlyList> items = new List> + { + new HashSet { 1, 2, 3 } + }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.IsSupersetOf(new[] { 1, 2 })); + } } diff --git a/TUnit.Assertions/Conditions/ListAssertions.cs b/TUnit.Assertions/Conditions/ListAssertions.cs index 7b534956bd..8e77ccd0de 100644 --- a/TUnit.Assertions/Conditions/ListAssertions.cs +++ b/TUnit.Assertions/Conditions/ListAssertions.cs @@ -67,10 +67,6 @@ public class ListItemAtSource : IAssertionSource private readonly AssertionContext _listContext; private readonly int _index; - internal AssertionContext InternalListContext => _listContext; - - internal int InternalIndex => _index; - public AssertionContext Context { get; } public ListItemAtSource(AssertionContext listContext, int index) @@ -130,6 +126,15 @@ public ListItemAtNullAssertion IsNotNull() public ListItemAtSatisfiesAssertion Satisfies( Func, Assertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) + { + return CreateSatisfiesAssertion( + (item, index) => assertion(new ValueAssertion(item, $"item[{index}]")), + expression); + } + + internal ListItemAtSatisfiesAssertion CreateSatisfiesAssertion( + Func assertion, + string? expression) { _listContext.ExpressionBuilder.Append($".Satisfies({expression})"); return new ListItemAtSatisfiesAssertion(_listContext, _index, assertion); diff --git a/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs b/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs index 40397828f5..d76aba4d19 100644 --- a/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs +++ b/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs @@ -69,10 +69,6 @@ public class ReadOnlyListItemAtSource : IAssertionSource private readonly AssertionContext _listContext; private readonly int _index; - internal AssertionContext InternalListContext => _listContext; - - internal int InternalIndex => _index; - public AssertionContext Context { get; } public ReadOnlyListItemAtSource(AssertionContext listContext, int index) @@ -132,6 +128,15 @@ public ReadOnlyListItemAtNullAssertion IsNotNull() public ReadOnlyListItemAtSatisfiesAssertion Satisfies( Func, Assertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) + { + return CreateSatisfiesAssertion( + (item, index) => assertion(new ValueAssertion(item, $"item[{index}]")), + expression); + } + + internal ReadOnlyListItemAtSatisfiesAssertion CreateSatisfiesAssertion( + Func assertion, + string? expression) { _listContext.ExpressionBuilder.Append($".Satisfies({expression})"); return new ReadOnlyListItemAtSatisfiesAssertion(_listContext, _index, assertion); diff --git a/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs b/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs index 177257fc0a..a9a10c83aa 100644 --- a/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs +++ b/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs @@ -289,11 +289,7 @@ private static ListItemAtSatisfiesAssertion SatisfiesSpecialised { - source.InternalListContext.ExpressionBuilder.Append($".Satisfies({expression})"); - return new ListItemAtSatisfiesAssertion( - source.InternalListContext, - source.InternalIndex, - assertionFactory); + return source.CreateSatisfiesAssertion(assertionFactory, expression); } private static ReadOnlyListItemAtSatisfiesAssertion SatisfiesSpecialised( @@ -302,10 +298,6 @@ private static ReadOnlyListItemAtSatisfiesAssertion SatisfiesSpeci string? expression) where TList : IReadOnlyList { - source.InternalListContext.ExpressionBuilder.Append($".Satisfies({expression})"); - return new ReadOnlyListItemAtSatisfiesAssertion( - source.InternalListContext, - source.InternalIndex, - assertionFactory); + return source.CreateSatisfiesAssertion(assertionFactory, expression); } } From 3b5f64cff436a30e29a8c60b4782d304874f24ec Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:14:23 +0100 Subject: [PATCH 03/10] refactor(assertions): generic Satisfies via IAssertionSourceFor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace per-type Satisfies overload bodies with delegators through a new static-abstract factory interface. Interface-shape overloads collapse to one-liners; concrete-type overloads (List, Dictionary) keep direct construction since their assertion sources implement IAssertionSourceFor against the matching interface, not the concrete type. Pattern is reusable for the remaining #5703-5707 family (Member, Count, etc.) — each new target needs only the generic Satisfies entry plus delegators, no per-shape construction logic. netstandard2.0 drops the specialised overloads (no static abstracts); net8+/net9+/net10+ get the full surface. --- TUnit.Assertions/Conditions/ListAssertions.cs | 18 ++ .../Conditions/ReadOnlyListAssertions.cs | 18 ++ TUnit.Assertions/Core/IAssertionSourceFor.cs | 17 ++ .../ListItemAtSatisfiesExtensions.cs | 177 ++++-------------- TUnit.Assertions/Sources/ArrayAssertion.cs | 7 + .../Sources/CollectionAssertion.cs | 7 + .../Sources/DictionaryAssertion.cs | 7 + TUnit.Assertions/Sources/ListAssertion.cs | 7 + .../Sources/MutableDictionaryAssertion.cs | 7 + .../Sources/ReadOnlyListAssertion.cs | 7 + TUnit.Assertions/Sources/SetAssertion.cs | 21 +++ ...Has_No_API_Changes.DotNet10_0.verified.txt | 36 +++- ..._Has_No_API_Changes.DotNet8_0.verified.txt | 36 +++- ..._Has_No_API_Changes.DotNet9_0.verified.txt | 36 +++- ...ary_Has_No_API_Changes.Net4_7.verified.txt | 49 ----- 15 files changed, 229 insertions(+), 221 deletions(-) create mode 100644 TUnit.Assertions/Core/IAssertionSourceFor.cs diff --git a/TUnit.Assertions/Conditions/ListAssertions.cs b/TUnit.Assertions/Conditions/ListAssertions.cs index 8e77ccd0de..4c5a9b2a3a 100644 --- a/TUnit.Assertions/Conditions/ListAssertions.cs +++ b/TUnit.Assertions/Conditions/ListAssertions.cs @@ -132,6 +132,24 @@ public ListItemAtSatisfiesAssertion Satisfies( expression); } +#if !NETSTANDARD2_0 + /// + /// Asserts that the item at the index satisfies the given assertion expressed against + /// a specialised assertion source . The source is constructed + /// per-item via its static factory. + /// Specify explicitly or via a typed lambda parameter. + /// + public ListItemAtSatisfiesAssertion Satisfies( + Func assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TSource : IAssertionSourceFor + { + return CreateSatisfiesAssertion( + (item, index) => assertion(TSource.Create(item, $"item[{index}]")), + expression); + } +#endif + internal ListItemAtSatisfiesAssertion CreateSatisfiesAssertion( Func assertion, string? expression) diff --git a/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs b/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs index d76aba4d19..f3b85b1fb5 100644 --- a/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs +++ b/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs @@ -134,6 +134,24 @@ public ReadOnlyListItemAtSatisfiesAssertion Satisfies( expression); } +#if !NETSTANDARD2_0 + /// + /// Asserts that the item at the index satisfies the given assertion expressed against + /// a specialised assertion source . The source is constructed + /// per-item via its static factory. + /// Specify explicitly or via a typed lambda parameter. + /// + public ReadOnlyListItemAtSatisfiesAssertion Satisfies( + Func assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TSource : IAssertionSourceFor + { + return CreateSatisfiesAssertion( + (item, index) => assertion(TSource.Create(item, $"item[{index}]")), + expression); + } +#endif + internal ReadOnlyListItemAtSatisfiesAssertion CreateSatisfiesAssertion( Func assertion, string? expression) diff --git a/TUnit.Assertions/Core/IAssertionSourceFor.cs b/TUnit.Assertions/Core/IAssertionSourceFor.cs new file mode 100644 index 0000000000..31f11bb0de --- /dev/null +++ b/TUnit.Assertions/Core/IAssertionSourceFor.cs @@ -0,0 +1,17 @@ +#if !NETSTANDARD2_0 +namespace TUnit.Assertions.Core; + +/// +/// Marks an assertion source as constructible from a raw value, enabling generic +/// dispatch through Satisfies<TSource> on item-at and similar entry points. +/// Implementations expose a static factory used to materialise the specialised source +/// per-item without per-shape overload enumeration. +/// +/// The value type the source wraps. +/// The implementing source type (CRTP). +public interface IAssertionSourceFor : IAssertionSource + where TSelf : IAssertionSourceFor +{ + static abstract TSelf Create(TItem item, string label); +} +#endif diff --git a/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs b/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs index a9a10c83aa..09e8b550e1 100644 --- a/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs +++ b/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs @@ -1,3 +1,4 @@ +#if !NETSTANDARD2_0 using System.Runtime.CompilerServices; using TUnit.Assertions.Conditions; using TUnit.Assertions.Core; @@ -7,7 +8,9 @@ namespace TUnit.Assertions.Extensions; /// /// Specialised overloads for ItemAt(...).Satisfies(...) when the item is itself -/// a collection-like value. +/// a collection-like value. Each overload delegates to the generic +/// Satisfies<TSource> on the item-at source, picking the matching +/// implementation. /// public static class ListItemAtSatisfiesExtensions { @@ -16,72 +19,42 @@ public static ListItemAtSatisfiesAssertion> Satisfies Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IList> - { - return SatisfiesSpecialised( - source, - (item, index) => assertion(new CollectionAssertion(item, $"item[{index}]")), - expression); - } + => source.Satisfies>(assertion, expression); public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( this ReadOnlyListItemAtSource> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IReadOnlyList> - { - return SatisfiesSpecialised( - source, - (item, index) => assertion(new CollectionAssertion(item, $"item[{index}]")), - expression); - } + => source.Satisfies>(assertion, expression); public static ListItemAtSatisfiesAssertion> Satisfies( this ListItemAtSource> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IList> - { - return SatisfiesSpecialised( - source, - (item, index) => assertion(new ListAssertion(item, $"item[{index}]")), - expression); - } + => source.Satisfies>(assertion, expression); public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( this ReadOnlyListItemAtSource> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IReadOnlyList> - { - return SatisfiesSpecialised( - source, - (item, index) => assertion(new ListAssertion(item, $"item[{index}]")), - expression); - } + => source.Satisfies>(assertion, expression); public static ListItemAtSatisfiesAssertion> Satisfies( this ListItemAtSource> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IList> - { - return SatisfiesSpecialised( - source, - (item, index) => assertion(new ReadOnlyListAssertion(item, $"item[{index}]")), - expression); - } + => source.Satisfies>(assertion, expression); public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( this ReadOnlyListItemAtSource> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IReadOnlyList> - { - return SatisfiesSpecialised( - source, - (item, index) => assertion(new ReadOnlyListAssertion(item, $"item[{index}]")), - expression); - } + => source.Satisfies>(assertion, expression); public static ListItemAtSatisfiesAssertion> Satisfies( this ListItemAtSource> source, @@ -89,12 +62,7 @@ public static ListItemAtSatisfiesAssertion> where TKey : notnull - { - return SatisfiesSpecialised( - source, - (item, index) => assertion(new DictionaryAssertion(item, $"item[{index}]")), - expression); - } + => source.Satisfies>(assertion, expression); public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( this ReadOnlyListItemAtSource> source, @@ -102,12 +70,7 @@ public static ReadOnlyListItemAtSatisfiesAssertion> where TKey : notnull - { - return SatisfiesSpecialised( - source, - (item, index) => assertion(new DictionaryAssertion(item, $"item[{index}]")), - expression); - } + => source.Satisfies>(assertion, expression); public static ListItemAtSatisfiesAssertion> Satisfies( this ListItemAtSource> source, @@ -115,12 +78,7 @@ public static ListItemAtSatisfiesAssertion> Sat [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IList> where TKey : notnull - { - return SatisfiesSpecialised( - source, - (item, index) => assertion(new MutableDictionaryAssertion(item, $"item[{index}]")), - expression); - } + => source.Satisfies>(assertion, expression); public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( this ReadOnlyListItemAtSource> source, @@ -128,147 +86,96 @@ public static ReadOnlyListItemAtSatisfiesAssertion> where TKey : notnull - { - return SatisfiesSpecialised( - source, - (item, index) => assertion(new MutableDictionaryAssertion(item, $"item[{index}]")), - expression); - } + => source.Satisfies>(assertion, expression); public static ListItemAtSatisfiesAssertion> Satisfies( this ListItemAtSource> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IList> - { - return SatisfiesSpecialised( - source, - (item, index) => assertion(new SetAssertion(item, $"item[{index}]")), - expression); - } + => source.Satisfies>(assertion, expression); public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( this ReadOnlyListItemAtSource> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IReadOnlyList> - { - return SatisfiesSpecialised( - source, - (item, index) => assertion(new SetAssertion(item, $"item[{index}]")), - expression); - } + => source.Satisfies>(assertion, expression); -#if NET5_0_OR_GREATER public static ListItemAtSatisfiesAssertion> Satisfies( this ListItemAtSource> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IList> - { - return SatisfiesSpecialised( - source, - (item, index) => assertion(new ReadOnlySetAssertion(item, $"item[{index}]")), - expression); - } + => source.Satisfies>(assertion, expression); public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( this ReadOnlyListItemAtSource> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IReadOnlyList> - { - return SatisfiesSpecialised( - source, - (item, index) => assertion(new ReadOnlySetAssertion(item, $"item[{index}]")), - expression); - } -#endif + => source.Satisfies>(assertion, expression); public static ListItemAtSatisfiesAssertion Satisfies( this ListItemAtSource source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IList - { - return SatisfiesSpecialised( - source, - (item, index) => assertion(new ArrayAssertion(item, $"item[{index}]")), - expression); - } + => source.Satisfies>(assertion, expression); public static ReadOnlyListItemAtSatisfiesAssertion Satisfies( this ReadOnlyListItemAtSource source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IReadOnlyList - { - return SatisfiesSpecialised( - source, - (item, index) => assertion(new ArrayAssertion(item, $"item[{index}]")), - expression); - } + => source.Satisfies>(assertion, expression); + // Concrete List overloads bridge to the IList-typed ListAssertion directly. + // ListAssertion implements IAssertionSourceFor>, not IAssertionSourceFor>, + // so the generic Satisfies constraint cannot bind here. public static ListItemAtSatisfiesAssertion> Satisfies( this ListItemAtSource> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IList> - { - return SatisfiesSpecialised( - source, + => source.CreateSatisfiesAssertion( (item, index) => assertion(new ListAssertion(item, $"item[{index}]")), expression); - } public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( this ReadOnlyListItemAtSource> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IReadOnlyList> - { - return SatisfiesSpecialised( - source, + => source.CreateSatisfiesAssertion( (item, index) => assertion(new ListAssertion(item, $"item[{index}]")), expression); - } public static ListItemAtSatisfiesAssertion> Satisfies( this ListItemAtSource> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IList> - { - return SatisfiesSpecialised( - source, - (item, index) => assertion(new HashSetAssertion(item, $"item[{index}]")), - expression); - } + => source.Satisfies>(assertion, expression); public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( this ReadOnlyListItemAtSource> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IReadOnlyList> - { - return SatisfiesSpecialised( - source, - (item, index) => assertion(new HashSetAssertion(item, $"item[{index}]")), - expression); - } + => source.Satisfies>(assertion, expression); + // Concrete Dictionary overloads bridge directly: MutableDictionaryAssertion + // implements IAssertionSourceFor>, not the concrete Dictionary<,>. public static ListItemAtSatisfiesAssertion> Satisfies( this ListItemAtSource> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IList> where TKey : notnull - { - return SatisfiesSpecialised( - source, + => source.CreateSatisfiesAssertion( (item, index) => assertion(new MutableDictionaryAssertion(item, $"item[{index}]")), expression); - } public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( this ReadOnlyListItemAtSource> source, @@ -276,28 +183,8 @@ public static ReadOnlyListItemAtSatisfiesAssertion> where TKey : notnull - { - return SatisfiesSpecialised( - source, + => source.CreateSatisfiesAssertion( (item, index) => assertion(new MutableDictionaryAssertion(item, $"item[{index}]")), expression); - } - - private static ListItemAtSatisfiesAssertion SatisfiesSpecialised( - ListItemAtSource source, - Func assertionFactory, - string? expression) - where TList : IList - { - return source.CreateSatisfiesAssertion(assertionFactory, expression); - } - - private static ReadOnlyListItemAtSatisfiesAssertion SatisfiesSpecialised( - ReadOnlyListItemAtSource source, - Func assertionFactory, - string? expression) - where TList : IReadOnlyList - { - return source.CreateSatisfiesAssertion(assertionFactory, expression); - } } +#endif diff --git a/TUnit.Assertions/Sources/ArrayAssertion.cs b/TUnit.Assertions/Sources/ArrayAssertion.cs index 6efe0fc411..040a6f066e 100644 --- a/TUnit.Assertions/Sources/ArrayAssertion.cs +++ b/TUnit.Assertions/Sources/ArrayAssertion.cs @@ -10,6 +10,9 @@ namespace TUnit.Assertions.Sources; /// enabling generated assertions that target concrete array types (e.g., string[]). /// public class ArrayAssertion : CollectionAssertionBase +#if !NETSTANDARD2_0 + , IAssertionSourceFor> +#endif { public ArrayAssertion(TItem[]? value, string? expression) : base(new AssertionContext(value!, CreateExpressionBuilder(expression))) @@ -21,6 +24,10 @@ internal ArrayAssertion(AssertionContext context) { } +#if !NETSTANDARD2_0 + public static ArrayAssertion Create(TItem[] item, string label) => new(item, label); +#endif + private static StringBuilder CreateExpressionBuilder(string? expression) { var builder = new StringBuilder(); diff --git a/TUnit.Assertions/Sources/CollectionAssertion.cs b/TUnit.Assertions/Sources/CollectionAssertion.cs index 9d91a042fe..8bddae8fa5 100644 --- a/TUnit.Assertions/Sources/CollectionAssertion.cs +++ b/TUnit.Assertions/Sources/CollectionAssertion.cs @@ -11,6 +11,9 @@ namespace TUnit.Assertions.Sources; /// that persist through And/Or continuations. /// public class CollectionAssertion : CollectionAssertionBase, TItem> +#if !NETSTANDARD2_0 + , IAssertionSourceFor, CollectionAssertion> +#endif { public CollectionAssertion(IEnumerable value, string? expression) : base(new AssertionContext>(value, CreateExpressionBuilder(expression))) @@ -22,6 +25,10 @@ internal CollectionAssertion(AssertionContext> context) { } +#if !NETSTANDARD2_0 + public static CollectionAssertion Create(IEnumerable item, string label) => new(item, label); +#endif + private static StringBuilder CreateExpressionBuilder(string? expression) { var builder = new StringBuilder(); diff --git a/TUnit.Assertions/Sources/DictionaryAssertion.cs b/TUnit.Assertions/Sources/DictionaryAssertion.cs index d08ef3fcb5..74c7bb6d5a 100644 --- a/TUnit.Assertions/Sources/DictionaryAssertion.cs +++ b/TUnit.Assertions/Sources/DictionaryAssertion.cs @@ -11,6 +11,9 @@ namespace TUnit.Assertions.Sources; /// plus collection methods (Contains, IsEmpty, All, etc.) since dictionaries are collections of KeyValuePair items. /// public class DictionaryAssertion : DictionaryAssertionBase, TKey, TValue> +#if !NETSTANDARD2_0 + , IAssertionSourceFor, DictionaryAssertion> +#endif where TKey : notnull { public DictionaryAssertion(IReadOnlyDictionary? value, string? expression) @@ -18,6 +21,10 @@ public DictionaryAssertion(IReadOnlyDictionary? value, string? exp { } +#if !NETSTANDARD2_0 + public static DictionaryAssertion Create(IReadOnlyDictionary item, string label) => new(item, label); +#endif + private static AssertionContext> CreateContext( IReadOnlyDictionary? value, string? expression) diff --git a/TUnit.Assertions/Sources/ListAssertion.cs b/TUnit.Assertions/Sources/ListAssertion.cs index 0dc4c587ea..c13a816155 100644 --- a/TUnit.Assertions/Sources/ListAssertion.cs +++ b/TUnit.Assertions/Sources/ListAssertion.cs @@ -10,6 +10,9 @@ namespace TUnit.Assertions.Sources; /// in addition to all standard collection methods. /// public class ListAssertion : ListAssertionBase, TItem> +#if !NETSTANDARD2_0 + , IAssertionSourceFor, ListAssertion> +#endif { public ListAssertion(IList? value, string? expression) : base(CreateContext(value, expression)) @@ -21,6 +24,10 @@ internal ListAssertion(AssertionContext> context) { } +#if !NETSTANDARD2_0 + public static ListAssertion Create(IList item, string label) => new(item, label); +#endif + private static AssertionContext> CreateContext( IList? value, string? expression) diff --git a/TUnit.Assertions/Sources/MutableDictionaryAssertion.cs b/TUnit.Assertions/Sources/MutableDictionaryAssertion.cs index 2d0afd5ae0..7e9a5f4561 100644 --- a/TUnit.Assertions/Sources/MutableDictionaryAssertion.cs +++ b/TUnit.Assertions/Sources/MutableDictionaryAssertion.cs @@ -11,6 +11,9 @@ namespace TUnit.Assertions.Sources; /// plus collection methods (Contains, IsEmpty, All, etc.) since dictionaries are collections of KeyValuePair items. /// public class MutableDictionaryAssertion : MutableDictionaryAssertionBase, TKey, TValue> +#if !NETSTANDARD2_0 + , IAssertionSourceFor, MutableDictionaryAssertion> +#endif where TKey : notnull { public MutableDictionaryAssertion(IDictionary? value, string? expression) @@ -18,6 +21,10 @@ public MutableDictionaryAssertion(IDictionary? value, string? expr { } +#if !NETSTANDARD2_0 + public static MutableDictionaryAssertion Create(IDictionary item, string label) => new(item, label); +#endif + private static AssertionContext> CreateContext( IDictionary? value, string? expression) diff --git a/TUnit.Assertions/Sources/ReadOnlyListAssertion.cs b/TUnit.Assertions/Sources/ReadOnlyListAssertion.cs index 367a079b6e..3645f0fe01 100644 --- a/TUnit.Assertions/Sources/ReadOnlyListAssertion.cs +++ b/TUnit.Assertions/Sources/ReadOnlyListAssertion.cs @@ -8,6 +8,9 @@ namespace TUnit.Assertions.Sources; /// /// The type of items in the read-only list public class ReadOnlyListAssertion : ReadOnlyListAssertionBase, TItem> +#if !NETSTANDARD2_0 + , IAssertionSourceFor, ReadOnlyListAssertion> +#endif { public ReadOnlyListAssertion(IReadOnlyList? value, string? expression) : base(CreateContext(value, expression)) @@ -19,6 +22,10 @@ internal ReadOnlyListAssertion(AssertionContext> context) { } +#if !NETSTANDARD2_0 + public static ReadOnlyListAssertion Create(IReadOnlyList item, string label) => new(item, label); +#endif + private static AssertionContext> CreateContext( IReadOnlyList? value, string? expression) diff --git a/TUnit.Assertions/Sources/SetAssertion.cs b/TUnit.Assertions/Sources/SetAssertion.cs index 155b306051..9e9a3012cb 100644 --- a/TUnit.Assertions/Sources/SetAssertion.cs +++ b/TUnit.Assertions/Sources/SetAssertion.cs @@ -11,6 +11,9 @@ namespace TUnit.Assertions.Sources; /// /// The type of items in the set. public class SetAssertion : SetAssertionBase, TItem>, IAssertionSource> +#if !NETSTANDARD2_0 + , IAssertionSourceFor, SetAssertion> +#endif { /// /// Creates a new SetAssertion for the given set. @@ -34,6 +37,10 @@ private protected SetAssertion(AssertionContext> context) /// AssertionContext> IAssertionSource>.Context => Context; +#if !NETSTANDARD2_0 + public static SetAssertion Create(ISet item, string label) => new(item, label); +#endif + private static StringBuilder CreateExpressionBuilder(string? expression) { var builder = new StringBuilder(); @@ -49,6 +56,9 @@ private static StringBuilder CreateExpressionBuilder(string? expression) /// /// The type of items in the set. public class ReadOnlySetAssertion : SetAssertionBase, TItem>, IAssertionSource> +#if !NETSTANDARD2_0 + , IAssertionSourceFor, ReadOnlySetAssertion> +#endif { /// /// Creates a new ReadOnlySetAssertion for the given read-only set. @@ -72,6 +82,10 @@ private protected ReadOnlySetAssertion(AssertionContext> con /// AssertionContext> IAssertionSource>.Context => Context; +#if !NETSTANDARD2_0 + public static ReadOnlySetAssertion Create(IReadOnlySet item, string label) => new(item, label); +#endif + private static StringBuilder CreateExpressionBuilder(string? expression) { var builder = new StringBuilder(); @@ -87,6 +101,9 @@ private static StringBuilder CreateExpressionBuilder(string? expression) /// /// The type of items in the set. public class HashSetAssertion : SetAssertionBase, TItem>, IAssertionSource> +#if !NETSTANDARD2_0 + , IAssertionSourceFor, HashSetAssertion> +#endif { /// /// Creates a new HashSetAssertion for the given hash set. @@ -110,6 +127,10 @@ private protected HashSetAssertion(AssertionContext> context) /// AssertionContext> IAssertionSource>.Context => Context; +#if !NETSTANDARD2_0 + public static HashSetAssertion Create(HashSet item, string label) => new(item, label); +#endif + private static StringBuilder CreateExpressionBuilder(string? expression) { var builder = new StringBuilder(); 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 6655d53a00..f35fc828cc 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 @@ -1550,6 +1550,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } public class ListLastItemEqualsAssertion : . where TList : . @@ -1802,6 +1804,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } public class ReadOnlyListLastItemEqualsAssertion : . where TList : . @@ -2501,6 +2505,11 @@ namespace .Core . AssertAsync(); } public interface IAssertionSource { } + public interface IAssertionSourceFor : ., . + where TSelf : . + { + TSelf Create(TItem item, string label); + } public interface IAssertionSource : . { . Context { get; } @@ -6141,9 +6150,10 @@ namespace .Extensions } namespace .Sources { - public class ArrayAssertion : . + public class ArrayAssertion : ., ., .>, . { public ArrayAssertion(TItem[]? value, string? expression) { } + public static . Create(TItem[] item, string label) { } } public class AsyncDelegateAssertion : ., ., .<.>, . { @@ -6274,9 +6284,10 @@ namespace .Sources public . IsOrderedByDescending( keySelector, .? comparer, [.("keySelector")] string? selectorExpression = null, [.("comparer")] string? comparerExpression = null) { } public . IsTypeOf() { } } - public class CollectionAssertion : .<., TItem> + public class CollectionAssertion : .<., TItem>, ., .<., .>, .<.> { public CollectionAssertion(. value, string? expression) { } + public static . Create(. item, string label) { } } public class DelegateAssertion : ., ., . { @@ -6312,10 +6323,11 @@ namespace .Sources public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } } - public class DictionaryAssertion : .<., TKey, TValue> + public class DictionaryAssertion : .<., TKey, TValue>, ., .<., .>, .<.> where TKey : notnull { public DictionaryAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } } public class FuncAssertion : ., ., . { @@ -6340,10 +6352,11 @@ namespace .Sources public . ThrowsExactly() where TException : { } } - public class HashSetAssertion : .<., TItem>, ., .<.> + public class HashSetAssertion : .<., TItem>, ., .<., .>, .<.> { public HashSetAssertion(.? value, string expression) { } protected override . CreateSetAdapter(. value) { } + public static . Create(. item, string label) { } } public abstract class ListAssertionBase : . where TList : . @@ -6356,9 +6369,10 @@ namespace .Sources public . ItemAt(int index, [.("index")] string? expression = null) { } public . LastItem() { } } - public class ListAssertion : .<., TItem> + public class ListAssertion : .<., TItem>, ., .<., .>, .<.> { public ListAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } } public class MemoryAndContinuation : . { @@ -6423,10 +6437,11 @@ namespace .Sources public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } } - public class MutableDictionaryAssertion : .<., TKey, TValue> + public class MutableDictionaryAssertion : .<., TKey, TValue>, ., .<., .>, .<.> where TKey : notnull { public MutableDictionaryAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } } public abstract class ReadOnlyListAssertionBase : . where TList : . @@ -6439,19 +6454,21 @@ namespace .Sources public . ItemAt(int index, [.("index")] string? indexExpression = null) { } public . LastItem() { } } - public class ReadOnlyListAssertion : .<., TItem> + public class ReadOnlyListAssertion : .<., TItem>, ., .<., .>, .<.> { public ReadOnlyListAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } } public class ReadOnlyMemoryAssertion : .<, TItem>, ., .<> { public ReadOnlyMemoryAssertion( value, string expression) { } protected override . CreateAdapter( value) { } } - public class ReadOnlySetAssertion : .<., TItem>, ., .<.> + public class ReadOnlySetAssertion : .<., TItem>, ., .<., .>, .<.> { public ReadOnlySetAssertion(.? value, string expression) { } protected override . CreateSetAdapter(. value) { } + public static . Create(. item, string label) { } } public class SetAndContinuation : . where TSet : . @@ -6474,10 +6491,11 @@ namespace .Sources public . Overlaps(. other, [.("other")] string? expression = null) { } public . SetEquals(. other, [.("other")] string? expression = null) { } } - public class SetAssertion : .<., TItem>, ., .<.> + public class SetAssertion : .<., TItem>, ., .<., .>, .<.> { public SetAssertion(.? value, string expression) { } protected override . CreateSetAdapter(. value) { } + public static . Create(. item, string label) { } } public class SetOrContinuation : . where TSet : . 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 b58574b7ef..762b0b4ecd 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 @@ -1533,6 +1533,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } public class ListLastItemEqualsAssertion : . where TList : . @@ -1785,6 +1787,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } public class ReadOnlyListLastItemEqualsAssertion : . where TList : . @@ -2480,6 +2484,11 @@ namespace .Core . AssertAsync(); } public interface IAssertionSource { } + public interface IAssertionSourceFor : ., . + where TSelf : . + { + TSelf Create(TItem item, string label); + } public interface IAssertionSource : . { . Context { get; } @@ -6070,9 +6079,10 @@ namespace .Extensions } namespace .Sources { - public class ArrayAssertion : . + public class ArrayAssertion : ., ., .>, . { public ArrayAssertion(TItem[]? value, string? expression) { } + public static . Create(TItem[] item, string label) { } } public class AsyncDelegateAssertion : ., ., .<.>, . { @@ -6202,9 +6212,10 @@ namespace .Sources public . IsOrderedByDescending( keySelector, .? comparer, [.("keySelector")] string? selectorExpression = null, [.("comparer")] string? comparerExpression = null) { } public . IsTypeOf() { } } - public class CollectionAssertion : .<., TItem> + public class CollectionAssertion : .<., TItem>, ., .<., .>, .<.> { public CollectionAssertion(. value, string? expression) { } + public static . Create(. item, string label) { } } public class DelegateAssertion : ., ., . { @@ -6240,10 +6251,11 @@ namespace .Sources public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } } - public class DictionaryAssertion : .<., TKey, TValue> + public class DictionaryAssertion : .<., TKey, TValue>, ., .<., .>, .<.> where TKey : notnull { public DictionaryAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } } public class FuncAssertion : ., ., . { @@ -6268,10 +6280,11 @@ namespace .Sources public . ThrowsExactly() where TException : { } } - public class HashSetAssertion : .<., TItem>, ., .<.> + public class HashSetAssertion : .<., TItem>, ., .<., .>, .<.> { public HashSetAssertion(.? value, string expression) { } protected override . CreateSetAdapter(. value) { } + public static . Create(. item, string label) { } } public abstract class ListAssertionBase : . where TList : . @@ -6284,9 +6297,10 @@ namespace .Sources public . ItemAt(int index, [.("index")] string? expression = null) { } public . LastItem() { } } - public class ListAssertion : .<., TItem> + public class ListAssertion : .<., TItem>, ., .<., .>, .<.> { public ListAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } } public class MemoryAndContinuation : . { @@ -6351,10 +6365,11 @@ namespace .Sources public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } } - public class MutableDictionaryAssertion : .<., TKey, TValue> + public class MutableDictionaryAssertion : .<., TKey, TValue>, ., .<., .>, .<.> where TKey : notnull { public MutableDictionaryAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } } public abstract class ReadOnlyListAssertionBase : . where TList : . @@ -6367,19 +6382,21 @@ namespace .Sources public . ItemAt(int index, [.("index")] string? indexExpression = null) { } public . LastItem() { } } - public class ReadOnlyListAssertion : .<., TItem> + public class ReadOnlyListAssertion : .<., TItem>, ., .<., .>, .<.> { public ReadOnlyListAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } } public class ReadOnlyMemoryAssertion : .<, TItem>, ., .<> { public ReadOnlyMemoryAssertion( value, string expression) { } protected override . CreateAdapter( value) { } } - public class ReadOnlySetAssertion : .<., TItem>, ., .<.> + public class ReadOnlySetAssertion : .<., TItem>, ., .<., .>, .<.> { public ReadOnlySetAssertion(.? value, string expression) { } protected override . CreateSetAdapter(. value) { } + public static . Create(. item, string label) { } } public class SetAndContinuation : . where TSet : . @@ -6402,10 +6419,11 @@ namespace .Sources public . Overlaps(. other, [.("other")] string? expression = null) { } public . SetEquals(. other, [.("other")] string? expression = null) { } } - public class SetAssertion : .<., TItem>, ., .<.> + public class SetAssertion : .<., TItem>, ., .<., .>, .<.> { public SetAssertion(.? value, string expression) { } protected override . CreateSetAdapter(. value) { } + public static . Create(. item, string label) { } } public class SetOrContinuation : . where TSet : . 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 3c50d332ea..6849c2d4cc 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 @@ -1550,6 +1550,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } public class ListLastItemEqualsAssertion : . where TList : . @@ -1802,6 +1804,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } public class ReadOnlyListLastItemEqualsAssertion : . where TList : . @@ -2501,6 +2505,11 @@ namespace .Core . AssertAsync(); } public interface IAssertionSource { } + public interface IAssertionSourceFor : ., . + where TSelf : . + { + TSelf Create(TItem item, string label); + } public interface IAssertionSource : . { . Context { get; } @@ -6141,9 +6150,10 @@ namespace .Extensions } namespace .Sources { - public class ArrayAssertion : . + public class ArrayAssertion : ., ., .>, . { public ArrayAssertion(TItem[]? value, string? expression) { } + public static . Create(TItem[] item, string label) { } } public class AsyncDelegateAssertion : ., ., .<.>, . { @@ -6274,9 +6284,10 @@ namespace .Sources public . IsOrderedByDescending( keySelector, .? comparer, [.("keySelector")] string? selectorExpression = null, [.("comparer")] string? comparerExpression = null) { } public . IsTypeOf() { } } - public class CollectionAssertion : .<., TItem> + public class CollectionAssertion : .<., TItem>, ., .<., .>, .<.> { public CollectionAssertion(. value, string? expression) { } + public static . Create(. item, string label) { } } public class DelegateAssertion : ., ., . { @@ -6312,10 +6323,11 @@ namespace .Sources public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } } - public class DictionaryAssertion : .<., TKey, TValue> + public class DictionaryAssertion : .<., TKey, TValue>, ., .<., .>, .<.> where TKey : notnull { public DictionaryAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } } public class FuncAssertion : ., ., . { @@ -6340,10 +6352,11 @@ namespace .Sources public . ThrowsExactly() where TException : { } } - public class HashSetAssertion : .<., TItem>, ., .<.> + public class HashSetAssertion : .<., TItem>, ., .<., .>, .<.> { public HashSetAssertion(.? value, string expression) { } protected override . CreateSetAdapter(. value) { } + public static . Create(. item, string label) { } } public abstract class ListAssertionBase : . where TList : . @@ -6356,9 +6369,10 @@ namespace .Sources public . ItemAt(int index, [.("index")] string? expression = null) { } public . LastItem() { } } - public class ListAssertion : .<., TItem> + public class ListAssertion : .<., TItem>, ., .<., .>, .<.> { public ListAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } } public class MemoryAndContinuation : . { @@ -6423,10 +6437,11 @@ namespace .Sources public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } } - public class MutableDictionaryAssertion : .<., TKey, TValue> + public class MutableDictionaryAssertion : .<., TKey, TValue>, ., .<., .>, .<.> where TKey : notnull { public MutableDictionaryAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } } public abstract class ReadOnlyListAssertionBase : . where TList : . @@ -6439,19 +6454,21 @@ namespace .Sources public . ItemAt(int index, [.("index")] string? indexExpression = null) { } public . LastItem() { } } - public class ReadOnlyListAssertion : .<., TItem> + public class ReadOnlyListAssertion : .<., TItem>, ., .<., .>, .<.> { public ReadOnlyListAssertion(.? value, string? expression) { } + public static . Create(. item, string label) { } } public class ReadOnlyMemoryAssertion : .<, TItem>, ., .<> { public ReadOnlyMemoryAssertion( value, string expression) { } protected override . CreateAdapter( value) { } } - public class ReadOnlySetAssertion : .<., TItem>, ., .<.> + public class ReadOnlySetAssertion : .<., TItem>, ., .<., .>, .<.> { public ReadOnlySetAssertion(.? value, string expression) { } protected override . CreateSetAdapter(. value) { } + public static . Create(. item, string label) { } } public class SetAndContinuation : . where TSet : . @@ -6474,10 +6491,11 @@ namespace .Sources public . Overlaps(. other, [.("other")] string? expression = null) { } public . SetEquals(. other, [.("other")] string? expression = null) { } } - public class SetAssertion : .<., TItem>, ., .<.> + public class SetAssertion : .<., TItem>, ., .<., .>, .<.> { public SetAssertion(.? value, string expression) { } protected override . CreateSetAdapter(. value) { } + public static . Create(. item, string label) { } } public class SetOrContinuation : . where TSet : . diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt index 5259b2f5f2..8b9183194a 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -4143,55 +4143,6 @@ namespace .Extensions protected override .<.> CheckAsync(.<> metadata) { } protected override string GetExpectation() { } } - public static class ListItemAtSatisfiesExtensions - { - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : . { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : . { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - } public static class LongAssertions { public static ._IsEven_Assertion IsEven(this . source) { } From 0080687e048763cc23f089526b67db951823a66c Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:27:41 +0100 Subject: [PATCH 04/10] refactor(assertions): unify concrete-type Satisfies overloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ListAssertion and MutableDictionaryAssertion now implement two IAssertionSourceFor parameterisations each (interface + concrete item types), enabling the concrete List and Dictionary Satisfies overloads to delegate through the generic Satisfies path instead of bypassing it via direct CreateSatisfiesAssertion calls. This eliminates the per-type construction asymmetry that would otherwise propagate to every future combinator following the same pattern. IAssertionSourceFor no longer extends IAssertionSource — the framework only ever calls TSource.Create(...), and the multi-variant implementation is what required dropping the base (an IAssertionSource> Context property cannot be derived from AssertionContext> without invariance breakage). Concrete classes still expose IAssertionSource through their existing base hierarchies, so user lambdas see no surface change. Adds nested-list test coverage: IList>, IList>, IReadOnlyList>, IReadOnlyList>. --- TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs | 32 +++++++++++++++++++ TUnit.Assertions/Core/IAssertionSourceFor.cs | 8 ++++- .../ListItemAtSatisfiesExtensions.cs | 21 +++--------- TUnit.Assertions/Sources/ListAssertion.cs | 2 ++ .../Sources/MutableDictionaryAssertion.cs | 2 ++ ...Has_No_API_Changes.DotNet10_0.verified.txt | 16 ++++++---- ..._Has_No_API_Changes.DotNet8_0.verified.txt | 16 ++++++---- ..._Has_No_API_Changes.DotNet9_0.verified.txt | 16 ++++++---- 8 files changed, 74 insertions(+), 39 deletions(-) diff --git a/TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs b/TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs index 0dd2f2a945..56874c998a 100644 --- a/TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs +++ b/TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs @@ -23,6 +23,22 @@ public async Task List_ItemAt_Satisfies_Preserves_Collection_Item_Source() await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); } + [Test] + public async Task List_ItemAt_Satisfies_Preserves_IList_Item_Source() + { + IList> items = new List> { new List { 1, 2, 3 } }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task List_ItemAt_Satisfies_Preserves_IReadOnlyList_Item_Source() + { + IList> items = new List> { new List { 1, 2, 3 } }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); + } + [Test] public async Task List_ItemAt_Satisfies_Preserves_Array_Item_Source() { @@ -94,6 +110,22 @@ public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_Collection_Item_Source await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); } + [Test] + public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_IList_Item_Source() + { + IReadOnlyList> items = new List> { new List { 1, 2, 3 } }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_IReadOnlyList_Item_Source() + { + IReadOnlyList> items = new List> { new List { 1, 2, 3 } }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); + } + [Test] public async Task ReadOnlyList_ItemAt_Satisfies_Preserves_Array_Item_Source() { diff --git a/TUnit.Assertions/Core/IAssertionSourceFor.cs b/TUnit.Assertions/Core/IAssertionSourceFor.cs index 31f11bb0de..0580d1acfb 100644 --- a/TUnit.Assertions/Core/IAssertionSourceFor.cs +++ b/TUnit.Assertions/Core/IAssertionSourceFor.cs @@ -7,9 +7,15 @@ namespace TUnit.Assertions.Core; /// Implementations expose a static factory used to materialise the specialised source /// per-item without per-shape overload enumeration. /// +/// +/// Intentionally does not extend : a single +/// implementing class can implement multiple parameterisations of this interface +/// (e.g. against both an interface item type and a matching concrete type) without +/// triggering conflicting Context property requirements. +/// /// The value type the source wraps. /// The implementing source type (CRTP). -public interface IAssertionSourceFor : IAssertionSource +public interface IAssertionSourceFor where TSelf : IAssertionSourceFor { static abstract TSelf Create(TItem item, string label); diff --git a/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs b/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs index 09e8b550e1..4bbfeed2b2 100644 --- a/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs +++ b/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs @@ -130,26 +130,19 @@ public static ReadOnlyListItemAtSatisfiesAssertion Satisfies => source.Satisfies>(assertion, expression); - // Concrete List overloads bridge to the IList-typed ListAssertion directly. - // ListAssertion implements IAssertionSourceFor>, not IAssertionSourceFor>, - // so the generic Satisfies constraint cannot bind here. public static ListItemAtSatisfiesAssertion> Satisfies( this ListItemAtSource> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IList> - => source.CreateSatisfiesAssertion( - (item, index) => assertion(new ListAssertion(item, $"item[{index}]")), - expression); + => source.Satisfies>(assertion, expression); public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( this ReadOnlyListItemAtSource> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IReadOnlyList> - => source.CreateSatisfiesAssertion( - (item, index) => assertion(new ListAssertion(item, $"item[{index}]")), - expression); + => source.Satisfies>(assertion, expression); public static ListItemAtSatisfiesAssertion> Satisfies( this ListItemAtSource> source, @@ -165,17 +158,13 @@ public static ReadOnlyListItemAtSatisfiesAssertion> Satis where TList : IReadOnlyList> => source.Satisfies>(assertion, expression); - // Concrete Dictionary overloads bridge directly: MutableDictionaryAssertion - // implements IAssertionSourceFor>, not the concrete Dictionary<,>. public static ListItemAtSatisfiesAssertion> Satisfies( this ListItemAtSource> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TList : IList> where TKey : notnull - => source.CreateSatisfiesAssertion( - (item, index) => assertion(new MutableDictionaryAssertion(item, $"item[{index}]")), - expression); + => source.Satisfies>(assertion, expression); public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( this ReadOnlyListItemAtSource> source, @@ -183,8 +172,6 @@ public static ReadOnlyListItemAtSatisfiesAssertion> where TKey : notnull - => source.CreateSatisfiesAssertion( - (item, index) => assertion(new MutableDictionaryAssertion(item, $"item[{index}]")), - expression); + => source.Satisfies>(assertion, expression); } #endif diff --git a/TUnit.Assertions/Sources/ListAssertion.cs b/TUnit.Assertions/Sources/ListAssertion.cs index c13a816155..cd34cf370e 100644 --- a/TUnit.Assertions/Sources/ListAssertion.cs +++ b/TUnit.Assertions/Sources/ListAssertion.cs @@ -12,6 +12,7 @@ namespace TUnit.Assertions.Sources; public class ListAssertion : ListAssertionBase, TItem> #if !NETSTANDARD2_0 , IAssertionSourceFor, ListAssertion> + , IAssertionSourceFor, ListAssertion> #endif { public ListAssertion(IList? value, string? expression) @@ -26,6 +27,7 @@ internal ListAssertion(AssertionContext> context) #if !NETSTANDARD2_0 public static ListAssertion Create(IList item, string label) => new(item, label); + public static ListAssertion Create(List item, string label) => new(item, label); #endif private static AssertionContext> CreateContext( diff --git a/TUnit.Assertions/Sources/MutableDictionaryAssertion.cs b/TUnit.Assertions/Sources/MutableDictionaryAssertion.cs index 7e9a5f4561..3401c197de 100644 --- a/TUnit.Assertions/Sources/MutableDictionaryAssertion.cs +++ b/TUnit.Assertions/Sources/MutableDictionaryAssertion.cs @@ -13,6 +13,7 @@ namespace TUnit.Assertions.Sources; public class MutableDictionaryAssertion : MutableDictionaryAssertionBase, TKey, TValue> #if !NETSTANDARD2_0 , IAssertionSourceFor, MutableDictionaryAssertion> + , IAssertionSourceFor, MutableDictionaryAssertion> #endif where TKey : notnull { @@ -23,6 +24,7 @@ public MutableDictionaryAssertion(IDictionary? value, string? expr #if !NETSTANDARD2_0 public static MutableDictionaryAssertion Create(IDictionary item, string label) => new(item, label); + public static MutableDictionaryAssertion Create(Dictionary item, string label) => new(item, label); #endif private static AssertionContext> CreateContext( 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 f35fc828cc..38906057f8 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 @@ -2505,7 +2505,7 @@ namespace .Core . AssertAsync(); } public interface IAssertionSource { } - public interface IAssertionSourceFor : ., . + public interface IAssertionSourceFor where TSelf : . { TSelf Create(TItem item, string label); @@ -6150,7 +6150,7 @@ namespace .Extensions } namespace .Sources { - public class ArrayAssertion : ., ., .>, . + public class ArrayAssertion : ., .> { public ArrayAssertion(TItem[]? value, string? expression) { } public static . Create(TItem[] item, string label) { } @@ -6284,7 +6284,7 @@ namespace .Sources public . IsOrderedByDescending( keySelector, .? comparer, [.("keySelector")] string? selectorExpression = null, [.("comparer")] string? comparerExpression = null) { } public . IsTypeOf() { } } - public class CollectionAssertion : .<., TItem>, ., .<., .>, .<.> + public class CollectionAssertion : .<., TItem>, .<., .> { public CollectionAssertion(. value, string? expression) { } public static . Create(. item, string label) { } @@ -6323,7 +6323,7 @@ namespace .Sources public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } } - public class DictionaryAssertion : .<., TKey, TValue>, ., .<., .>, .<.> + public class DictionaryAssertion : .<., TKey, TValue>, .<., .> where TKey : notnull { public DictionaryAssertion(.? value, string? expression) { } @@ -6369,10 +6369,11 @@ namespace .Sources public . ItemAt(int index, [.("index")] string? expression = null) { } public . LastItem() { } } - public class ListAssertion : .<., TItem>, ., .<., .>, .<.> + public class ListAssertion : .<., TItem>, .<., .>, .<., .> { public ListAssertion(.? value, string? expression) { } public static . Create(. item, string label) { } + public static . Create(. item, string label) { } } public class MemoryAndContinuation : . { @@ -6437,11 +6438,12 @@ namespace .Sources public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } } - public class MutableDictionaryAssertion : .<., TKey, TValue>, ., .<., .>, .<.> + public class MutableDictionaryAssertion : .<., TKey, TValue>, .<., .>, .<., .> where TKey : notnull { public MutableDictionaryAssertion(.? value, string? expression) { } public static . Create(. item, string label) { } + public static . Create(. item, string label) { } } public abstract class ReadOnlyListAssertionBase : . where TList : . @@ -6454,7 +6456,7 @@ namespace .Sources public . ItemAt(int index, [.("index")] string? indexExpression = null) { } public . LastItem() { } } - public class ReadOnlyListAssertion : .<., TItem>, ., .<., .>, .<.> + public class ReadOnlyListAssertion : .<., TItem>, .<., .> { public ReadOnlyListAssertion(.? value, string? expression) { } public static . Create(. item, string label) { } 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 762b0b4ecd..c884176751 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 @@ -2484,7 +2484,7 @@ namespace .Core . AssertAsync(); } public interface IAssertionSource { } - public interface IAssertionSourceFor : ., . + public interface IAssertionSourceFor where TSelf : . { TSelf Create(TItem item, string label); @@ -6079,7 +6079,7 @@ namespace .Extensions } namespace .Sources { - public class ArrayAssertion : ., ., .>, . + public class ArrayAssertion : ., .> { public ArrayAssertion(TItem[]? value, string? expression) { } public static . Create(TItem[] item, string label) { } @@ -6212,7 +6212,7 @@ namespace .Sources public . IsOrderedByDescending( keySelector, .? comparer, [.("keySelector")] string? selectorExpression = null, [.("comparer")] string? comparerExpression = null) { } public . IsTypeOf() { } } - public class CollectionAssertion : .<., TItem>, ., .<., .>, .<.> + public class CollectionAssertion : .<., TItem>, .<., .> { public CollectionAssertion(. value, string? expression) { } public static . Create(. item, string label) { } @@ -6251,7 +6251,7 @@ namespace .Sources public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } } - public class DictionaryAssertion : .<., TKey, TValue>, ., .<., .>, .<.> + public class DictionaryAssertion : .<., TKey, TValue>, .<., .> where TKey : notnull { public DictionaryAssertion(.? value, string? expression) { } @@ -6297,10 +6297,11 @@ namespace .Sources public . ItemAt(int index, [.("index")] string? expression = null) { } public . LastItem() { } } - public class ListAssertion : .<., TItem>, ., .<., .>, .<.> + public class ListAssertion : .<., TItem>, .<., .>, .<., .> { public ListAssertion(.? value, string? expression) { } public static . Create(. item, string label) { } + public static . Create(. item, string label) { } } public class MemoryAndContinuation : . { @@ -6365,11 +6366,12 @@ namespace .Sources public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } } - public class MutableDictionaryAssertion : .<., TKey, TValue>, ., .<., .>, .<.> + public class MutableDictionaryAssertion : .<., TKey, TValue>, .<., .>, .<., .> where TKey : notnull { public MutableDictionaryAssertion(.? value, string? expression) { } public static . Create(. item, string label) { } + public static . Create(. item, string label) { } } public abstract class ReadOnlyListAssertionBase : . where TList : . @@ -6382,7 +6384,7 @@ namespace .Sources public . ItemAt(int index, [.("index")] string? indexExpression = null) { } public . LastItem() { } } - public class ReadOnlyListAssertion : .<., TItem>, ., .<., .>, .<.> + public class ReadOnlyListAssertion : .<., TItem>, .<., .> { public ReadOnlyListAssertion(.? value, string? expression) { } public static . Create(. item, string label) { } 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 6849c2d4cc..ef59c0f00c 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 @@ -2505,7 +2505,7 @@ namespace .Core . AssertAsync(); } public interface IAssertionSource { } - public interface IAssertionSourceFor : ., . + public interface IAssertionSourceFor where TSelf : . { TSelf Create(TItem item, string label); @@ -6150,7 +6150,7 @@ namespace .Extensions } namespace .Sources { - public class ArrayAssertion : ., ., .>, . + public class ArrayAssertion : ., .> { public ArrayAssertion(TItem[]? value, string? expression) { } public static . Create(TItem[] item, string label) { } @@ -6284,7 +6284,7 @@ namespace .Sources public . IsOrderedByDescending( keySelector, .? comparer, [.("keySelector")] string? selectorExpression = null, [.("comparer")] string? comparerExpression = null) { } public . IsTypeOf() { } } - public class CollectionAssertion : .<., TItem>, ., .<., .>, .<.> + public class CollectionAssertion : .<., TItem>, .<., .> { public CollectionAssertion(. value, string? expression) { } public static . Create(. item, string label) { } @@ -6323,7 +6323,7 @@ namespace .Sources public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } } - public class DictionaryAssertion : .<., TKey, TValue>, ., .<., .>, .<.> + public class DictionaryAssertion : .<., TKey, TValue>, .<., .> where TKey : notnull { public DictionaryAssertion(.? value, string? expression) { } @@ -6369,10 +6369,11 @@ namespace .Sources public . ItemAt(int index, [.("index")] string? expression = null) { } public . LastItem() { } } - public class ListAssertion : .<., TItem>, ., .<., .>, .<.> + public class ListAssertion : .<., TItem>, .<., .>, .<., .> { public ListAssertion(.? value, string? expression) { } public static . Create(. item, string label) { } + public static . Create(. item, string label) { } } public class MemoryAndContinuation : . { @@ -6437,11 +6438,12 @@ namespace .Sources public . DoesNotContainValue(TValue expectedValue, [.("expectedValue")] string? expression = null) { } protected override string GetExpectation() { } } - public class MutableDictionaryAssertion : .<., TKey, TValue>, ., .<., .>, .<.> + public class MutableDictionaryAssertion : .<., TKey, TValue>, .<., .>, .<., .> where TKey : notnull { public MutableDictionaryAssertion(.? value, string? expression) { } public static . Create(. item, string label) { } + public static . Create(. item, string label) { } } public abstract class ReadOnlyListAssertionBase : . where TList : . @@ -6454,7 +6456,7 @@ namespace .Sources public . ItemAt(int index, [.("index")] string? indexExpression = null) { } public . LastItem() { } } - public class ReadOnlyListAssertion : .<., TItem>, ., .<., .>, .<.> + public class ReadOnlyListAssertion : .<., TItem>, .<., .> { public ReadOnlyListAssertion(.? value, string? expression) { } public static . Create(. item, string label) { } From 0b39c8cc6e118db16f53b1fdf45ef4c16dbecb3e Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:35:15 +0100 Subject: [PATCH 05/10] test(assertions): cover IList> item-at path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the IList> case the previous round missed — this is the concrete-list path now routed through Satisfies> via the new IAssertionSourceFor, ListAssertion> implementation. Also documents the why behind the interface- and concrete-shaped overload coexistence on ListItemAtSatisfiesExtensions: C# overload resolution is exact on TItem, so IList> binds the List overload and never the IList one. --- TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs | 8 ++++++++ .../Extensions/ListItemAtSatisfiesExtensions.cs | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs b/TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs index 56874c998a..10dca85cc3 100644 --- a/TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs +++ b/TUnit.Assertions.Tests/Bugs/Issue5706Tests.cs @@ -47,6 +47,14 @@ public async Task List_ItemAt_Satisfies_Preserves_Array_Item_Source() await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); } + [Test] + public async Task List_ItemAt_Satisfies_Preserves_ConcreteList_Item_Source() + { + IList> items = new List> { new() { 1, 2, 3 } }; + + await Assert.That(items).ItemAt(0).Satisfies(item => item.Count().IsEqualTo(3)); + } + [Test] public async Task List_ItemAt_Satisfies_Preserves_Dictionary_Item_Source() { diff --git a/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs b/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs index 4bbfeed2b2..1129fdbe1e 100644 --- a/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs +++ b/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs @@ -12,6 +12,13 @@ namespace TUnit.Assertions.Extensions; /// Satisfies<TSource> on the item-at source, picking the matching /// implementation. /// +/// +/// Both interface-shaped (e.g. IList<T>) and concrete-shaped +/// (e.g. List<T>) overloads are required because C# overload +/// resolution performs exact type matching on the source's TItem: +/// IList<List<int>> binds the List<TInner> +/// overload, never the IList<TInner> one. +/// public static class ListItemAtSatisfiesExtensions { public static ListItemAtSatisfiesAssertion> Satisfies( From 39bccf43622d73fb2b3daaaf700e6f6672989238 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:51:40 +0100 Subject: [PATCH 06/10] fix(assertions): preserve last-item source types Apply the IAssertionSourceFor pattern (introduced for ItemAt in this PR) to LastItem so .LastItem().Satisfies(item => ...) preserves the specialised assertion source for collection-shaped items. Without this, nested collections, dictionaries, and sets inside a list lose the typed surface (.Count(), .ContainsKey(), .IsSupersetOf(), etc.) when asserted via LastItem. ListLastItemSatisfiesAssertion and ReadOnlyListLastItemSatisfiesAssertion now hold a Func internally; the public Func, Assertion?> constructor is retained and chains to the internal raw-item one, preserving binary compatibility with any consumer building assertions manually. Adds the parallel ListLastItemSatisfiesExtensions file with per-shape delegators mirroring the ItemAt set, plus 19 regression tests covering the same shapes Issue5706Tests already exercises against ItemAt. Documents the contributor guidance the previous review asked for: when adding a new IAssertionSourceFor implementation, add the matching overload pair below or users must spell out the type argument. --- TUnit.Assertions.Tests/Bugs/Issue5778Tests.cs | 188 +++++++++++++++++ TUnit.Assertions/Conditions/ListAssertions.cs | 39 +++- .../Conditions/ReadOnlyListAssertions.cs | 39 +++- .../ListItemAtSatisfiesExtensions.cs | 6 + .../ListLastItemSatisfiesExtensions.cs | 190 ++++++++++++++++++ ...Has_No_API_Changes.DotNet10_0.verified.txt | 57 ++++++ ..._Has_No_API_Changes.DotNet8_0.verified.txt | 57 ++++++ ..._Has_No_API_Changes.DotNet9_0.verified.txt | 57 ++++++ 8 files changed, 627 insertions(+), 6 deletions(-) create mode 100644 TUnit.Assertions.Tests/Bugs/Issue5778Tests.cs create mode 100644 TUnit.Assertions/Extensions/ListLastItemSatisfiesExtensions.cs diff --git a/TUnit.Assertions.Tests/Bugs/Issue5778Tests.cs b/TUnit.Assertions.Tests/Bugs/Issue5778Tests.cs new file mode 100644 index 0000000000..64f789bba0 --- /dev/null +++ b/TUnit.Assertions.Tests/Bugs/Issue5778Tests.cs @@ -0,0 +1,188 @@ +namespace TUnit.Assertions.Tests.Bugs; + +/// +/// Regression tests mirroring for the LastItem path. +/// LastItem(...).Satisfies(...) should preserve specialised assertion sources for +/// collection-like item values instead of exposing only IAssertionSource<TItem>. +/// +public class Issue5778Tests +{ + [Test] + public async Task List_LastItem_Satisfies_Preserves_String_Item_Source() + { + IList items = new List { "alpha" }; + + await Assert.That(items).LastItem().Satisfies(item => item.Contains("pha")); + } + + [Test] + public async Task List_LastItem_Satisfies_Preserves_Collection_Item_Source() + { + IList> items = new List> { new[] { 1, 2, 3 } }; + + await Assert.That(items).LastItem().Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task List_LastItem_Satisfies_Preserves_IList_Item_Source() + { + IList> items = new List> { new List { 1, 2, 3 } }; + + await Assert.That(items).LastItem().Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task List_LastItem_Satisfies_Preserves_IReadOnlyList_Item_Source() + { + IList> items = new List> { new List { 1, 2, 3 } }; + + await Assert.That(items).LastItem().Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task List_LastItem_Satisfies_Preserves_Array_Item_Source() + { + IList items = new List { new[] { 1, 2, 3 } }; + + await Assert.That(items).LastItem().Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task List_LastItem_Satisfies_Preserves_ConcreteList_Item_Source() + { + IList> items = new List> { new() { 1, 2, 3 } }; + + await Assert.That(items).LastItem().Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task List_LastItem_Satisfies_Preserves_Dictionary_Item_Source() + { + IList> items = new List> + { + new Dictionary { ["answer"] = 42 } + }; + + await Assert.That(items).LastItem().Satisfies(item => item.ContainsKey("answer")); + } + + [Test] + public async Task List_LastItem_Satisfies_Preserves_Concrete_Dictionary_Item_Source() + { + IList> items = new List> + { + new() { ["answer"] = 42 } + }; + + await Assert.That(items).LastItem().Satisfies(item => item.ContainsKey("answer")); + } + + [Test] + public async Task List_LastItem_Satisfies_Preserves_Set_Item_Source() + { + IList> items = new List> + { + new HashSet { 1, 2, 3 } + }; + + await Assert.That(items).LastItem().Satisfies(item => item.IsSupersetOf(new[] { 1, 2 })); + } + + [Test] + public async Task List_LastItem_Satisfies_Preserves_HashSet_Item_Source() + { + IList> items = new List> + { + new() { 1, 2, 3 } + }; + + await Assert.That(items).LastItem().Satisfies(item => item.IsSupersetOf(new[] { 1, 2 })); + } + + [Test] + public async Task ReadOnlyList_LastItem_Satisfies_Preserves_String_Item_Source() + { + IReadOnlyList items = new List { "alpha" }; + + await Assert.That(items).LastItem().Satisfies(item => item.Contains("pha")); + } + + [Test] + public async Task ReadOnlyList_LastItem_Satisfies_Preserves_Collection_Item_Source() + { + IReadOnlyList> items = new List> + { + new() { 1, 2, 3 } + }; + + await Assert.That(items).LastItem().Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task ReadOnlyList_LastItem_Satisfies_Preserves_IList_Item_Source() + { + IReadOnlyList> items = new List> { new List { 1, 2, 3 } }; + + await Assert.That(items).LastItem().Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task ReadOnlyList_LastItem_Satisfies_Preserves_IReadOnlyList_Item_Source() + { + IReadOnlyList> items = new List> { new List { 1, 2, 3 } }; + + await Assert.That(items).LastItem().Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task ReadOnlyList_LastItem_Satisfies_Preserves_Array_Item_Source() + { + IReadOnlyList items = new List { new[] { 1, 2, 3 } }; + + await Assert.That(items).LastItem().Satisfies(item => item.Count().IsEqualTo(3)); + } + + [Test] + public async Task ReadOnlyList_LastItem_Satisfies_Preserves_Dictionary_Item_Source() + { + IReadOnlyList> items = new List> + { + new() { ["answer"] = 42 } + }; + + await Assert.That(items).LastItem().Satisfies(item => item.ContainsKey("answer")); + } + + [Test] + public async Task ReadOnlyList_LastItem_Satisfies_Preserves_ReadOnly_Dictionary_Item_Source() + { + IReadOnlyList> items = new List> + { + new Dictionary { ["answer"] = 42 } + }; + + await Assert.That(items).LastItem().Satisfies(item => item.ContainsKey("answer")); + } + + [Test] + public async Task ReadOnlyList_LastItem_Satisfies_Preserves_Set_Item_Source() + { + IReadOnlyList> items = new List> + { + new() { 1, 2, 3 } + }; + + await Assert.That(items).LastItem().Satisfies(item => item.IsSupersetOf(new[] { 1, 2 })); + } + + [Test] + public async Task ReadOnlyList_LastItem_Satisfies_Preserves_ReadOnly_Set_Item_Source() + { + IReadOnlyList> items = new List> + { + new HashSet { 1, 2, 3 } + }; + + await Assert.That(items).LastItem().Satisfies(item => item.IsSupersetOf(new[] { 1, 2 })); + } +} diff --git a/TUnit.Assertions/Conditions/ListAssertions.cs b/TUnit.Assertions/Conditions/ListAssertions.cs index 4c5a9b2a3a..885f8505ba 100644 --- a/TUnit.Assertions/Conditions/ListAssertions.cs +++ b/TUnit.Assertions/Conditions/ListAssertions.cs @@ -268,6 +268,33 @@ public ListLastItemNullAssertion IsNotNull() public ListLastItemSatisfiesAssertion Satisfies( Func, Assertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) + { + return CreateSatisfiesAssertion( + item => assertion(new ValueAssertion(item, "lastItem")), + expression); + } + +#if !NETSTANDARD2_0 + /// + /// Asserts that the last item satisfies the given assertion expressed against + /// a specialised assertion source . The source is constructed + /// from the last item via its static factory. + /// Specify explicitly or via a typed lambda parameter. + /// + public ListLastItemSatisfiesAssertion Satisfies( + Func assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TSource : IAssertionSourceFor + { + return CreateSatisfiesAssertion( + item => assertion(TSource.Create(item, "lastItem")), + expression); + } +#endif + + internal ListLastItemSatisfiesAssertion CreateSatisfiesAssertion( + Func assertion, + string? expression) { _listContext.ExpressionBuilder.Append($".Satisfies({expression})"); return new ListLastItemSatisfiesAssertion(_listContext, assertion); @@ -632,11 +659,18 @@ protected override string GetExpectation() => public class ListLastItemSatisfiesAssertion : ListAssertionBase where TList : IList { - private readonly Func, Assertion?> _assertion; + private readonly Func _assertion; public ListLastItemSatisfiesAssertion( AssertionContext context, Func, Assertion?> assertion) + : this(context, item => assertion(new ValueAssertion(item, "lastItem"))) + { + } + + internal ListLastItemSatisfiesAssertion( + AssertionContext context, + Func assertion) : base(context) { _assertion = assertion; @@ -660,8 +694,7 @@ protected override async Task CheckAsync(EvaluationMetadata(lastItem, "lastItem"); - var resultingAssertion = _assertion(itemSource); + var resultingAssertion = _assertion(lastItem); if (resultingAssertion != null) { diff --git a/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs b/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs index f3b85b1fb5..576233846e 100644 --- a/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs +++ b/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs @@ -270,6 +270,33 @@ public ReadOnlyListLastItemNullAssertion IsNotNull() public ReadOnlyListLastItemSatisfiesAssertion Satisfies( Func, Assertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) + { + return CreateSatisfiesAssertion( + item => assertion(new ValueAssertion(item, "lastItem")), + expression); + } + +#if !NETSTANDARD2_0 + /// + /// Asserts that the last item satisfies the given assertion expressed against + /// a specialised assertion source . The source is constructed + /// from the last item via its static factory. + /// Specify explicitly or via a typed lambda parameter. + /// + public ReadOnlyListLastItemSatisfiesAssertion Satisfies( + Func assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TSource : IAssertionSourceFor + { + return CreateSatisfiesAssertion( + item => assertion(TSource.Create(item, "lastItem")), + expression); + } +#endif + + internal ReadOnlyListLastItemSatisfiesAssertion CreateSatisfiesAssertion( + Func assertion, + string? expression) { _listContext.ExpressionBuilder.Append($".Satisfies({expression})"); return new ReadOnlyListLastItemSatisfiesAssertion(_listContext, assertion); @@ -634,11 +661,18 @@ protected override string GetExpectation() => public class ReadOnlyListLastItemSatisfiesAssertion : ReadOnlyListAssertionBase where TList : IReadOnlyList { - private readonly Func, Assertion?> _assertion; + private readonly Func _assertion; public ReadOnlyListLastItemSatisfiesAssertion( AssertionContext context, Func, Assertion?> assertion) + : this(context, item => assertion(new ValueAssertion(item, "lastItem"))) + { + } + + internal ReadOnlyListLastItemSatisfiesAssertion( + AssertionContext context, + Func assertion) : base(context) { _assertion = assertion; @@ -662,8 +696,7 @@ protected override async Task CheckAsync(EvaluationMetadata(lastItem, "lastItem"); - var resultingAssertion = _assertion(itemSource); + var resultingAssertion = _assertion(lastItem); if (resultingAssertion != null) { diff --git a/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs b/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs index 1129fdbe1e..aca35c3b0b 100644 --- a/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs +++ b/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs @@ -18,6 +18,12 @@ namespace TUnit.Assertions.Extensions; /// resolution performs exact type matching on the source's TItem: /// IList<List<int>> binds the List<TInner> /// overload, never the IList<TInner> one. +/// +/// When introducing a new +/// implementation, add the matching overload pair below for both +/// and +/// ; otherwise users +/// must spell out the type argument explicitly: .Satisfies<MyAssertion<T>>(...). /// public static class ListItemAtSatisfiesExtensions { diff --git a/TUnit.Assertions/Extensions/ListLastItemSatisfiesExtensions.cs b/TUnit.Assertions/Extensions/ListLastItemSatisfiesExtensions.cs new file mode 100644 index 0000000000..595043b65e --- /dev/null +++ b/TUnit.Assertions/Extensions/ListLastItemSatisfiesExtensions.cs @@ -0,0 +1,190 @@ +#if !NETSTANDARD2_0 +using System.Runtime.CompilerServices; +using TUnit.Assertions.Conditions; +using TUnit.Assertions.Core; +using TUnit.Assertions.Sources; + +namespace TUnit.Assertions.Extensions; + +/// +/// Specialised overloads for LastItem(...).Satisfies(...) when the last item is itself +/// a collection-like value. Each overload delegates to the generic +/// Satisfies<TSource> on the last-item source, picking the matching +/// implementation. +/// +/// +/// Both interface-shaped (e.g. IList<T>) and concrete-shaped +/// (e.g. List<T>) overloads are required because C# overload +/// resolution performs exact type matching on the source's TItem: +/// IList<List<int>> binds the List<TInner> +/// overload, never the IList<TInner> one. +/// +/// When introducing a new +/// implementation, add the matching overload pair below for both +/// and +/// ; otherwise users +/// must spell out the type argument explicitly: .Satisfies<MyAssertion<T>>(...). +/// +public static class ListLastItemSatisfiesExtensions +{ + public static ListLastItemSatisfiesAssertion> Satisfies( + this ListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + => source.Satisfies>(assertion, expression); + + public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( + this ReadOnlyListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + => source.Satisfies>(assertion, expression); + + public static ListLastItemSatisfiesAssertion> Satisfies( + this ListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + => source.Satisfies>(assertion, expression); + + public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( + this ReadOnlyListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + => source.Satisfies>(assertion, expression); + + public static ListLastItemSatisfiesAssertion> Satisfies( + this ListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + => source.Satisfies>(assertion, expression); + + public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( + this ReadOnlyListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + => source.Satisfies>(assertion, expression); + + public static ListLastItemSatisfiesAssertion> Satisfies( + this ListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + where TKey : notnull + => source.Satisfies>(assertion, expression); + + public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( + this ReadOnlyListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + where TKey : notnull + => source.Satisfies>(assertion, expression); + + public static ListLastItemSatisfiesAssertion> Satisfies( + this ListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + where TKey : notnull + => source.Satisfies>(assertion, expression); + + public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( + this ReadOnlyListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + where TKey : notnull + => source.Satisfies>(assertion, expression); + + public static ListLastItemSatisfiesAssertion> Satisfies( + this ListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + => source.Satisfies>(assertion, expression); + + public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( + this ReadOnlyListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + => source.Satisfies>(assertion, expression); + + public static ListLastItemSatisfiesAssertion> Satisfies( + this ListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + => source.Satisfies>(assertion, expression); + + public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( + this ReadOnlyListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + => source.Satisfies>(assertion, expression); + + public static ListLastItemSatisfiesAssertion Satisfies( + this ListLastItemSource source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList + => source.Satisfies>(assertion, expression); + + public static ReadOnlyListLastItemSatisfiesAssertion Satisfies( + this ReadOnlyListLastItemSource source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList + => source.Satisfies>(assertion, expression); + + public static ListLastItemSatisfiesAssertion> Satisfies( + this ListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + => source.Satisfies>(assertion, expression); + + public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( + this ReadOnlyListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + => source.Satisfies>(assertion, expression); + + public static ListLastItemSatisfiesAssertion> Satisfies( + this ListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + => source.Satisfies>(assertion, expression); + + public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( + this ReadOnlyListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + => source.Satisfies>(assertion, expression); + + public static ListLastItemSatisfiesAssertion> Satisfies( + this ListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IList> + where TKey : notnull + => source.Satisfies>(assertion, expression); + + public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( + this ReadOnlyListLastItemSource> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TList : IReadOnlyList> + where TKey : notnull + => source.Satisfies>(assertion, expression); +} +#endif 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 38906057f8..4b0b600c0f 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 @@ -1590,6 +1590,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } [.("IsEqualTo", OverloadResolutionPriority=2)] public class LongEqualsAssertion : . @@ -1844,6 +1846,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } [.<.Rune>("IsAscii", CustomName="IsNotAscii", ExpectationMessage="be ASCII", NegateLogic=true)] [.<.Rune>("IsAscii", ExpectationMessage="be ASCII")] @@ -4777,6 +4781,59 @@ namespace .Extensions where TList : .<.> where TKey : notnull { } } + public static class ListLastItemSatisfiesExtensions + { + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : . { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : . { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + } public static class LongAssertions { public static ._IsEven_Assertion IsEven(this . source) { } 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 c884176751..9d9a9d1f42 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 @@ -1573,6 +1573,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } [.("IsEqualTo", OverloadResolutionPriority=2)] public class LongEqualsAssertion : . @@ -1827,6 +1829,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } [.<.Rune>("IsAscii", CustomName="IsNotAscii", ExpectationMessage="be ASCII", NegateLogic=true)] [.<.Rune>("IsAscii", ExpectationMessage="be ASCII")] @@ -4712,6 +4716,59 @@ namespace .Extensions where TList : .<.> where TKey : notnull { } } + public static class ListLastItemSatisfiesExtensions + { + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : . { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : . { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + } public static class LongAssertions { public static ._IsEven_Assertion IsEven(this . source) { } 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 ef59c0f00c..b3abaa8188 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 @@ -1590,6 +1590,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } [.("IsEqualTo", OverloadResolutionPriority=2)] public class LongEqualsAssertion : . @@ -1844,6 +1846,8 @@ namespace .Conditions public . IsNull() { } public . IsTypeOf() { } public . Satisfies(<., .?> assertion, [.("assertion")] string? expression = null) { } + public . Satisfies( assertion, [.("assertion")] string? expression = null) + where TSource : . { } } [.<.Rune>("IsAscii", CustomName="IsNotAscii", ExpectationMessage="be ASCII", NegateLogic=true)] [.<.Rune>("IsAscii", ExpectationMessage="be ASCII")] @@ -4777,6 +4781,59 @@ namespace .Extensions where TList : .<.> where TKey : notnull { } } + public static class ListLastItemSatisfiesExtensions + { + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : . { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> { } + public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : . { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TList : .<.> + where TKey : notnull { } + } public static class LongAssertions { public static ._IsEven_Assertion IsEven(this . source) { } From 96960db7943ecadf2dba13bcfb53b37f0be6f63d Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:57:26 +0100 Subject: [PATCH 07/10] docs(assertions): cross-reference paired Satisfies extensions files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the per-shape overload list grows, the two extension files (ItemAt and LastItem) must stay in sync — each new IAssertionSourceFor implementation needs a pair in each. Documents this explicitly so a future contributor isn't tempted to update only one side. --- TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs | 4 ++++ .../Extensions/ListLastItemSatisfiesExtensions.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs b/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs index aca35c3b0b..ba8947161e 100644 --- a/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs +++ b/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs @@ -24,6 +24,10 @@ namespace TUnit.Assertions.Extensions; /// and /// ; otherwise users /// must spell out the type argument explicitly: .Satisfies<MyAssertion<T>>(...). +/// +/// Changes here must also be mirrored in ListLastItemSatisfiesExtensions +/// (and vice versa) — both files enumerate the same collection shapes but bind +/// to different item-selector sources. /// public static class ListItemAtSatisfiesExtensions { diff --git a/TUnit.Assertions/Extensions/ListLastItemSatisfiesExtensions.cs b/TUnit.Assertions/Extensions/ListLastItemSatisfiesExtensions.cs index 595043b65e..15a95efa22 100644 --- a/TUnit.Assertions/Extensions/ListLastItemSatisfiesExtensions.cs +++ b/TUnit.Assertions/Extensions/ListLastItemSatisfiesExtensions.cs @@ -24,6 +24,10 @@ namespace TUnit.Assertions.Extensions; /// and /// ; otherwise users /// must spell out the type argument explicitly: .Satisfies<MyAssertion<T>>(...). +/// +/// Changes here must also be mirrored in ListItemAtSatisfiesExtensions +/// (and vice versa) — both files enumerate the same collection shapes but bind +/// to different item-selector sources. /// public static class ListLastItemSatisfiesExtensions { From d2befb89a3cb4e4e3c76f599bd44a414ea771ac0 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:15:56 +0100 Subject: [PATCH 08/10] refactor(assertions): unify item-selector Satisfies extensions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapse the parallel ItemAt and LastItem extension files into one behind a new IItemSatisfiesSource interface implemented by all four item-selector sources. Each per-shape overload now binds the interface instead of a concrete source type, so a single 11-overload file replaces the previous 2 × 22-overload files. Eliminates the "must mirror" maintenance burden flagged in review. The interface extends IAssertionSource so that the specialised overloads outrank the predicate-style Satisfies(this IAssertionSource, Func) extension during overload resolution. Without that inheritance, items whose specialised assertion methods return bool (e.g. IDictionary.ContainsKey) would silently bind the predicate path and defeat the typed source. --- TUnit.Assertions/Conditions/ListAssertions.cs | 6 + .../Conditions/ReadOnlyListAssertions.cs | 6 + TUnit.Assertions/Core/IItemSatisfiesSource.cs | 25 +++ .../CollectionItemSatisfiesExtensions.cs | 97 +++++++++ .../ListItemAtSatisfiesExtensions.cs | 194 ------------------ .../ListLastItemSatisfiesExtensions.cs | 194 ------------------ ...Has_No_API_Changes.DotNet10_0.verified.txt | 136 +++--------- ..._Has_No_API_Changes.DotNet8_0.verified.txt | 136 +++--------- ..._Has_No_API_Changes.DotNet9_0.verified.txt | 136 +++--------- 9 files changed, 212 insertions(+), 718 deletions(-) create mode 100644 TUnit.Assertions/Core/IItemSatisfiesSource.cs create mode 100644 TUnit.Assertions/Extensions/CollectionItemSatisfiesExtensions.cs delete mode 100644 TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs delete mode 100644 TUnit.Assertions/Extensions/ListLastItemSatisfiesExtensions.cs diff --git a/TUnit.Assertions/Conditions/ListAssertions.cs b/TUnit.Assertions/Conditions/ListAssertions.cs index 885f8505ba..ffe3feaf50 100644 --- a/TUnit.Assertions/Conditions/ListAssertions.cs +++ b/TUnit.Assertions/Conditions/ListAssertions.cs @@ -62,6 +62,9 @@ protected override Task CheckAsync(EvaluationMetadata me /// This enables patterns like: Assert.That(list).ItemAt(0).IsEqualTo(expected) /// public class ListItemAtSource : IAssertionSource +#if !NETSTANDARD2_0 + , IItemSatisfiesSource> +#endif where TList : IList { private readonly AssertionContext _listContext; @@ -206,6 +209,9 @@ public IsNotAssignableFromAssertion IsNotAssignableFrom public class ListLastItemSource : IAssertionSource +#if !NETSTANDARD2_0 + , IItemSatisfiesSource> +#endif where TList : IList { private readonly AssertionContext _listContext; diff --git a/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs b/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs index 576233846e..6d0bbe0fb3 100644 --- a/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs +++ b/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs @@ -64,6 +64,9 @@ protected override Task CheckAsync(EvaluationMetadata me /// This enables patterns like: Assert.That(list).ItemAt(0).IsEqualTo(expected) /// public class ReadOnlyListItemAtSource : IAssertionSource +#if !NETSTANDARD2_0 + , IItemSatisfiesSource> +#endif where TList : IReadOnlyList { private readonly AssertionContext _listContext; @@ -208,6 +211,9 @@ public IsNotAssignableFromAssertion IsNotAssignableFrom public class ReadOnlyListLastItemSource : IAssertionSource +#if !NETSTANDARD2_0 + , IItemSatisfiesSource> +#endif where TList : IReadOnlyList { private readonly AssertionContext _listContext; diff --git a/TUnit.Assertions/Core/IItemSatisfiesSource.cs b/TUnit.Assertions/Core/IItemSatisfiesSource.cs new file mode 100644 index 0000000000..e0be46c4f4 --- /dev/null +++ b/TUnit.Assertions/Core/IItemSatisfiesSource.cs @@ -0,0 +1,25 @@ +#if !NETSTANDARD2_0 +namespace TUnit.Assertions.Core; + +/// +/// Common shape for item-selector sources (ItemAt, LastItem, etc.) that expose a +/// generic Satisfies<TSource> entry. Lets a single set of per-shape +/// extension overloads dispatch across all selector variants by binding to this +/// interface rather than each concrete source type. +/// +/// +/// Extends so that extension methods targeting +/// this interface bind in preference to those targeting +/// directly — without that, the predicate-style Satisfies(Func<TItem, bool>) +/// extension can win overload resolution for items whose specialised assertion methods +/// return bool (e.g. IDictionary.ContainsKey). +/// +/// The outer collection type the source originates from. +/// The selected item's type. +/// The selector-specific assertion result type. +public interface IItemSatisfiesSource : IAssertionSource +{ + TResult Satisfies(Func assertion, string? expression = null) + where TSource : IAssertionSourceFor; +} +#endif diff --git a/TUnit.Assertions/Extensions/CollectionItemSatisfiesExtensions.cs b/TUnit.Assertions/Extensions/CollectionItemSatisfiesExtensions.cs new file mode 100644 index 0000000000..7e1ba45c78 --- /dev/null +++ b/TUnit.Assertions/Extensions/CollectionItemSatisfiesExtensions.cs @@ -0,0 +1,97 @@ +#if !NETSTANDARD2_0 +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Sources; + +namespace TUnit.Assertions.Extensions; + +/// +/// Specialised Satisfies overloads when the selected item (from ItemAt, +/// LastItem, etc.) is itself a collection-like value. Each overload binds the +/// matching implementation and dispatches +/// through , +/// so a single set of per-shape overloads serves every item-selector source type. +/// +/// +/// Both interface-shaped (e.g. IList<T>) and concrete-shaped +/// (e.g. List<T>) overloads are required because C# overload +/// resolution performs exact type matching on the source's TItem: +/// IList<List<int>> binds the List<TInner> +/// overload, never the IList<TInner> one. +/// +/// When introducing a new +/// implementation, add the matching overload below; otherwise users must +/// spell out the type argument explicitly: .Satisfies<MyAssertion<T>>(...). +/// +public static class CollectionItemSatisfiesExtensions +{ + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + => source.Satisfies>(assertion, expression); + + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + => source.Satisfies>(assertion, expression); + + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + => source.Satisfies>(assertion, expression); + + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TKey : notnull + => source.Satisfies>(assertion, expression); + + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TKey : notnull + => source.Satisfies>(assertion, expression); + + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + => source.Satisfies>(assertion, expression); + + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + => source.Satisfies>(assertion, expression); + + public static TResult Satisfies( + this IItemSatisfiesSource source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + => source.Satisfies>(assertion, expression); + + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + => source.Satisfies>(assertion, expression); + + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + => source.Satisfies>(assertion, expression); + + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, + Func, IAssertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + where TKey : notnull + => source.Satisfies>(assertion, expression); +} +#endif diff --git a/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs b/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs deleted file mode 100644 index ba8947161e..0000000000 --- a/TUnit.Assertions/Extensions/ListItemAtSatisfiesExtensions.cs +++ /dev/null @@ -1,194 +0,0 @@ -#if !NETSTANDARD2_0 -using System.Runtime.CompilerServices; -using TUnit.Assertions.Conditions; -using TUnit.Assertions.Core; -using TUnit.Assertions.Sources; - -namespace TUnit.Assertions.Extensions; - -/// -/// Specialised overloads for ItemAt(...).Satisfies(...) when the item is itself -/// a collection-like value. Each overload delegates to the generic -/// Satisfies<TSource> on the item-at source, picking the matching -/// implementation. -/// -/// -/// Both interface-shaped (e.g. IList<T>) and concrete-shaped -/// (e.g. List<T>) overloads are required because C# overload -/// resolution performs exact type matching on the source's TItem: -/// IList<List<int>> binds the List<TInner> -/// overload, never the IList<TInner> one. -/// -/// When introducing a new -/// implementation, add the matching overload pair below for both -/// and -/// ; otherwise users -/// must spell out the type argument explicitly: .Satisfies<MyAssertion<T>>(...). -/// -/// Changes here must also be mirrored in ListLastItemSatisfiesExtensions -/// (and vice versa) — both files enumerate the same collection shapes but bind -/// to different item-selector sources. -/// -public static class ListItemAtSatisfiesExtensions -{ - public static ListItemAtSatisfiesAssertion> Satisfies( - this ListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( - this ReadOnlyListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - => source.Satisfies>(assertion, expression); - - public static ListItemAtSatisfiesAssertion> Satisfies( - this ListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( - this ReadOnlyListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - => source.Satisfies>(assertion, expression); - - public static ListItemAtSatisfiesAssertion> Satisfies( - this ListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( - this ReadOnlyListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - => source.Satisfies>(assertion, expression); - - public static ListItemAtSatisfiesAssertion> Satisfies( - this ListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - where TKey : notnull - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( - this ReadOnlyListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - where TKey : notnull - => source.Satisfies>(assertion, expression); - - public static ListItemAtSatisfiesAssertion> Satisfies( - this ListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - where TKey : notnull - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( - this ReadOnlyListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - where TKey : notnull - => source.Satisfies>(assertion, expression); - - public static ListItemAtSatisfiesAssertion> Satisfies( - this ListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( - this ReadOnlyListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - => source.Satisfies>(assertion, expression); - - public static ListItemAtSatisfiesAssertion> Satisfies( - this ListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( - this ReadOnlyListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - => source.Satisfies>(assertion, expression); - - public static ListItemAtSatisfiesAssertion Satisfies( - this ListItemAtSource source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListItemAtSatisfiesAssertion Satisfies( - this ReadOnlyListItemAtSource source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList - => source.Satisfies>(assertion, expression); - - public static ListItemAtSatisfiesAssertion> Satisfies( - this ListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( - this ReadOnlyListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - => source.Satisfies>(assertion, expression); - - public static ListItemAtSatisfiesAssertion> Satisfies( - this ListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( - this ReadOnlyListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - => source.Satisfies>(assertion, expression); - - public static ListItemAtSatisfiesAssertion> Satisfies( - this ListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - where TKey : notnull - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListItemAtSatisfiesAssertion> Satisfies( - this ReadOnlyListItemAtSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - where TKey : notnull - => source.Satisfies>(assertion, expression); -} -#endif diff --git a/TUnit.Assertions/Extensions/ListLastItemSatisfiesExtensions.cs b/TUnit.Assertions/Extensions/ListLastItemSatisfiesExtensions.cs deleted file mode 100644 index 15a95efa22..0000000000 --- a/TUnit.Assertions/Extensions/ListLastItemSatisfiesExtensions.cs +++ /dev/null @@ -1,194 +0,0 @@ -#if !NETSTANDARD2_0 -using System.Runtime.CompilerServices; -using TUnit.Assertions.Conditions; -using TUnit.Assertions.Core; -using TUnit.Assertions.Sources; - -namespace TUnit.Assertions.Extensions; - -/// -/// Specialised overloads for LastItem(...).Satisfies(...) when the last item is itself -/// a collection-like value. Each overload delegates to the generic -/// Satisfies<TSource> on the last-item source, picking the matching -/// implementation. -/// -/// -/// Both interface-shaped (e.g. IList<T>) and concrete-shaped -/// (e.g. List<T>) overloads are required because C# overload -/// resolution performs exact type matching on the source's TItem: -/// IList<List<int>> binds the List<TInner> -/// overload, never the IList<TInner> one. -/// -/// When introducing a new -/// implementation, add the matching overload pair below for both -/// and -/// ; otherwise users -/// must spell out the type argument explicitly: .Satisfies<MyAssertion<T>>(...). -/// -/// Changes here must also be mirrored in ListItemAtSatisfiesExtensions -/// (and vice versa) — both files enumerate the same collection shapes but bind -/// to different item-selector sources. -/// -public static class ListLastItemSatisfiesExtensions -{ - public static ListLastItemSatisfiesAssertion> Satisfies( - this ListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( - this ReadOnlyListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - => source.Satisfies>(assertion, expression); - - public static ListLastItemSatisfiesAssertion> Satisfies( - this ListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( - this ReadOnlyListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - => source.Satisfies>(assertion, expression); - - public static ListLastItemSatisfiesAssertion> Satisfies( - this ListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( - this ReadOnlyListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - => source.Satisfies>(assertion, expression); - - public static ListLastItemSatisfiesAssertion> Satisfies( - this ListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - where TKey : notnull - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( - this ReadOnlyListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - where TKey : notnull - => source.Satisfies>(assertion, expression); - - public static ListLastItemSatisfiesAssertion> Satisfies( - this ListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - where TKey : notnull - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( - this ReadOnlyListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - where TKey : notnull - => source.Satisfies>(assertion, expression); - - public static ListLastItemSatisfiesAssertion> Satisfies( - this ListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( - this ReadOnlyListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - => source.Satisfies>(assertion, expression); - - public static ListLastItemSatisfiesAssertion> Satisfies( - this ListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( - this ReadOnlyListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - => source.Satisfies>(assertion, expression); - - public static ListLastItemSatisfiesAssertion Satisfies( - this ListLastItemSource source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListLastItemSatisfiesAssertion Satisfies( - this ReadOnlyListLastItemSource source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList - => source.Satisfies>(assertion, expression); - - public static ListLastItemSatisfiesAssertion> Satisfies( - this ListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( - this ReadOnlyListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - => source.Satisfies>(assertion, expression); - - public static ListLastItemSatisfiesAssertion> Satisfies( - this ListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( - this ReadOnlyListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - => source.Satisfies>(assertion, expression); - - public static ListLastItemSatisfiesAssertion> Satisfies( - this ListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IList> - where TKey : notnull - => source.Satisfies>(assertion, expression); - - public static ReadOnlyListLastItemSatisfiesAssertion> Satisfies( - this ReadOnlyListLastItemSource> source, - Func, IAssertion?> assertion, - [CallerArgumentExpression(nameof(assertion))] string? expression = null) - where TList : IReadOnlyList> - where TKey : notnull - => source.Satisfies>(assertion, expression); -} -#endif 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 4b0b600c0f..f5157bef70 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 @@ -1534,7 +1534,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ListItemAtSource : ., . + public class ListItemAtSource : ., ., .> where TList : . { public ListItemAtSource(. listContext, int index) { } @@ -1574,7 +1574,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ListLastItemSource : ., . + public class ListLastItemSource : ., ., .> where TList : . { public ListLastItemSource(. listContext) { } @@ -1790,7 +1790,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ReadOnlyListItemAtSource : ., . + public class ReadOnlyListItemAtSource : ., ., .> where TList : . { public ReadOnlyListItemAtSource(. listContext, int index) { } @@ -1830,7 +1830,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ReadOnlyListLastItemSource : ., . + public class ReadOnlyListLastItemSource : ., ., .> where TList : . { public ReadOnlyListLastItemSource(. listContext) { } @@ -2525,6 +2525,11 @@ namespace .Core . IsTypeOf(); } public interface IDelegateAssertionSource : ., . { } + public interface IItemSatisfiesSource : ., . + { + TResult Satisfies( assertion, string? expression = null) + where TSource : .; + } public class ListAndContinuation : . where TList : . { } public class ListOrContinuation : . @@ -3106,6 +3111,23 @@ namespace .Extensions public static . IsInOrder(this . source) where TCollection : . { } } + public static class CollectionItemSatisfiesExtensions + { + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TKey : notnull { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TKey : notnull { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TKey : notnull { } + } public static class ComparisonAssertionExtensions { [.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")] @@ -4728,112 +4750,6 @@ namespace .Extensions protected override .<.> CheckAsync(.<> metadata) { } protected override string GetExpectation() { } } - public static class ListItemAtSatisfiesExtensions - { - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : . { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : . { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - } - public static class ListLastItemSatisfiesExtensions - { - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : . { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : . { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - } public static class LongAssertions { public static ._IsEven_Assertion IsEven(this . source) { } 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 9d9a9d1f42..f13d8b1657 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 @@ -1517,7 +1517,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ListItemAtSource : ., . + public class ListItemAtSource : ., ., .> where TList : . { public ListItemAtSource(. listContext, int index) { } @@ -1557,7 +1557,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ListLastItemSource : ., . + public class ListLastItemSource : ., ., .> where TList : . { public ListLastItemSource(. listContext) { } @@ -1773,7 +1773,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ReadOnlyListItemAtSource : ., . + public class ReadOnlyListItemAtSource : ., ., .> where TList : . { public ReadOnlyListItemAtSource(. listContext, int index) { } @@ -1813,7 +1813,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ReadOnlyListLastItemSource : ., . + public class ReadOnlyListLastItemSource : ., ., .> where TList : . { public ReadOnlyListLastItemSource(. listContext) { } @@ -2504,6 +2504,11 @@ namespace .Core . IsTypeOf(); } public interface IDelegateAssertionSource : ., . { } + public interface IItemSatisfiesSource : ., . + { + TResult Satisfies( assertion, string? expression = null) + where TSource : .; + } public class ListAndContinuation : . where TList : . { } public class ListOrContinuation : . @@ -3071,6 +3076,23 @@ namespace .Extensions public static . IsInOrder(this . source) where TCollection : . { } } + public static class CollectionItemSatisfiesExtensions + { + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TKey : notnull { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TKey : notnull { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TKey : notnull { } + } public static class ComparisonAssertionExtensions { [.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")] @@ -4663,112 +4685,6 @@ namespace .Extensions protected override .<.> CheckAsync(.<> metadata) { } protected override string GetExpectation() { } } - public static class ListItemAtSatisfiesExtensions - { - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : . { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : . { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - } - public static class ListLastItemSatisfiesExtensions - { - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : . { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : . { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - } public static class LongAssertions { public static ._IsEven_Assertion IsEven(this . source) { } 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 b3abaa8188..9ef1cba110 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 @@ -1534,7 +1534,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ListItemAtSource : ., . + public class ListItemAtSource : ., ., .> where TList : . { public ListItemAtSource(. listContext, int index) { } @@ -1574,7 +1574,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ListLastItemSource : ., . + public class ListLastItemSource : ., ., .> where TList : . { public ListLastItemSource(. listContext) { } @@ -1790,7 +1790,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ReadOnlyListItemAtSource : ., . + public class ReadOnlyListItemAtSource : ., ., .> where TList : . { public ReadOnlyListItemAtSource(. listContext, int index) { } @@ -1830,7 +1830,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ReadOnlyListLastItemSource : ., . + public class ReadOnlyListLastItemSource : ., ., .> where TList : . { public ReadOnlyListLastItemSource(. listContext) { } @@ -2525,6 +2525,11 @@ namespace .Core . IsTypeOf(); } public interface IDelegateAssertionSource : ., . { } + public interface IItemSatisfiesSource : ., . + { + TResult Satisfies( assertion, string? expression = null) + where TSource : .; + } public class ListAndContinuation : . where TList : . { } public class ListOrContinuation : . @@ -3106,6 +3111,23 @@ namespace .Extensions public static . IsInOrder(this . source) where TCollection : . { } } + public static class CollectionItemSatisfiesExtensions + { + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TKey : notnull { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TKey : notnull { } + public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + where TKey : notnull { } + } public static class ComparisonAssertionExtensions { [.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")] @@ -4728,112 +4750,6 @@ namespace .Extensions protected override .<.> CheckAsync(.<> metadata) { } protected override string GetExpectation() { } } - public static class ListItemAtSatisfiesExtensions - { - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : . { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : . { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - } - public static class ListLastItemSatisfiesExtensions - { - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : . { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> { } - public static . Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : . { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - public static .> Satisfies(this .> source, <., .?> assertion, [.("assertion")] string? expression = null) - where TList : .<.> - where TKey : notnull { } - } public static class LongAssertions { public static ._IsEven_Assertion IsEven(this . source) { } From fc4d114da9673ecfb7a61247013b3ed915a1fb8f Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:20:20 +0100 Subject: [PATCH 09/10] refactor(assertions): drop unused TList from IItemSatisfiesSource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TList never appeared in the interface's method signature — it was redundant with TResult, which already encodes the assertion type that carries TList. Reducing to trims one generic argument across the interface declaration, four source-class implementations, and eleven extension overload signatures. --- TUnit.Assertions/Conditions/ListAssertions.cs | 4 +- .../Conditions/ReadOnlyListAssertions.cs | 4 +- TUnit.Assertions/Core/IItemSatisfiesSource.cs | 3 +- .../CollectionItemSatisfiesExtensions.cs | 46 +++++++++---------- ...Has_No_API_Changes.DotNet10_0.verified.txt | 32 ++++++------- ..._Has_No_API_Changes.DotNet8_0.verified.txt | 32 ++++++------- ..._Has_No_API_Changes.DotNet9_0.verified.txt | 32 ++++++------- 7 files changed, 76 insertions(+), 77 deletions(-) diff --git a/TUnit.Assertions/Conditions/ListAssertions.cs b/TUnit.Assertions/Conditions/ListAssertions.cs index ffe3feaf50..631d683c63 100644 --- a/TUnit.Assertions/Conditions/ListAssertions.cs +++ b/TUnit.Assertions/Conditions/ListAssertions.cs @@ -63,7 +63,7 @@ protected override Task CheckAsync(EvaluationMetadata me /// public class ListItemAtSource : IAssertionSource #if !NETSTANDARD2_0 - , IItemSatisfiesSource> + , IItemSatisfiesSource> #endif where TList : IList { @@ -210,7 +210,7 @@ public IsNotAssignableFromAssertion IsNotAssignableFrom public class ListLastItemSource : IAssertionSource #if !NETSTANDARD2_0 - , IItemSatisfiesSource> + , IItemSatisfiesSource> #endif where TList : IList { diff --git a/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs b/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs index 6d0bbe0fb3..6438b94c82 100644 --- a/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs +++ b/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs @@ -65,7 +65,7 @@ protected override Task CheckAsync(EvaluationMetadata me /// public class ReadOnlyListItemAtSource : IAssertionSource #if !NETSTANDARD2_0 - , IItemSatisfiesSource> + , IItemSatisfiesSource> #endif where TList : IReadOnlyList { @@ -212,7 +212,7 @@ public IsNotAssignableFromAssertion IsNotAssignableFrom public class ReadOnlyListLastItemSource : IAssertionSource #if !NETSTANDARD2_0 - , IItemSatisfiesSource> + , IItemSatisfiesSource> #endif where TList : IReadOnlyList { diff --git a/TUnit.Assertions/Core/IItemSatisfiesSource.cs b/TUnit.Assertions/Core/IItemSatisfiesSource.cs index e0be46c4f4..464851281b 100644 --- a/TUnit.Assertions/Core/IItemSatisfiesSource.cs +++ b/TUnit.Assertions/Core/IItemSatisfiesSource.cs @@ -14,10 +14,9 @@ namespace TUnit.Assertions.Core; /// extension can win overload resolution for items whose specialised assertion methods /// return bool (e.g. IDictionary.ContainsKey). /// -/// The outer collection type the source originates from. /// The selected item's type. /// The selector-specific assertion result type. -public interface IItemSatisfiesSource : IAssertionSource +public interface IItemSatisfiesSource : IAssertionSource { TResult Satisfies(Func assertion, string? expression = null) where TSource : IAssertionSourceFor; diff --git a/TUnit.Assertions/Extensions/CollectionItemSatisfiesExtensions.cs b/TUnit.Assertions/Extensions/CollectionItemSatisfiesExtensions.cs index 7e1ba45c78..82d2041b0e 100644 --- a/TUnit.Assertions/Extensions/CollectionItemSatisfiesExtensions.cs +++ b/TUnit.Assertions/Extensions/CollectionItemSatisfiesExtensions.cs @@ -9,7 +9,7 @@ namespace TUnit.Assertions.Extensions; /// Specialised Satisfies overloads when the selected item (from ItemAt, /// LastItem, etc.) is itself a collection-like value. Each overload binds the /// matching implementation and dispatches -/// through , +/// through , /// so a single set of per-shape overloads serves every item-selector source type. /// /// @@ -25,70 +25,70 @@ namespace TUnit.Assertions.Extensions; /// public static class CollectionItemSatisfiesExtensions { - public static TResult Satisfies( - this IItemSatisfiesSource, TResult> source, + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) => source.Satisfies>(assertion, expression); - public static TResult Satisfies( - this IItemSatisfiesSource, TResult> source, + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) => source.Satisfies>(assertion, expression); - public static TResult Satisfies( - this IItemSatisfiesSource, TResult> source, + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) => source.Satisfies>(assertion, expression); - public static TResult Satisfies( - this IItemSatisfiesSource, TResult> source, + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TKey : notnull => source.Satisfies>(assertion, expression); - public static TResult Satisfies( - this IItemSatisfiesSource, TResult> source, + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TKey : notnull => source.Satisfies>(assertion, expression); - public static TResult Satisfies( - this IItemSatisfiesSource, TResult> source, + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) => source.Satisfies>(assertion, expression); - public static TResult Satisfies( - this IItemSatisfiesSource, TResult> source, + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) => source.Satisfies>(assertion, expression); - public static TResult Satisfies( - this IItemSatisfiesSource source, + public static TResult Satisfies( + this IItemSatisfiesSource source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) => source.Satisfies>(assertion, expression); - public static TResult Satisfies( - this IItemSatisfiesSource, TResult> source, + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) => source.Satisfies>(assertion, expression); - public static TResult Satisfies( - this IItemSatisfiesSource, TResult> source, + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) => source.Satisfies>(assertion, expression); - public static TResult Satisfies( - this IItemSatisfiesSource, TResult> source, + public static TResult Satisfies( + this IItemSatisfiesSource, TResult> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) where TKey : notnull 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 f5157bef70..fc2e65071d 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 @@ -1534,7 +1534,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ListItemAtSource : ., ., .> + public class ListItemAtSource : ., ., .> where TList : . { public ListItemAtSource(. listContext, int index) { } @@ -1574,7 +1574,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ListLastItemSource : ., ., .> + public class ListLastItemSource : ., ., .> where TList : . { public ListLastItemSource(. listContext) { } @@ -1790,7 +1790,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ReadOnlyListItemAtSource : ., ., .> + public class ReadOnlyListItemAtSource : ., ., .> where TList : . { public ReadOnlyListItemAtSource(. listContext, int index) { } @@ -1830,7 +1830,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ReadOnlyListLastItemSource : ., ., .> + public class ReadOnlyListLastItemSource : ., ., .> where TList : . { public ReadOnlyListLastItemSource(. listContext) { } @@ -2525,7 +2525,7 @@ namespace .Core . IsTypeOf(); } public interface IDelegateAssertionSource : ., . { } - public interface IItemSatisfiesSource : ., . + public interface IItemSatisfiesSource : ., . { TResult Satisfies( assertion, string? expression = null) where TSource : .; @@ -3113,19 +3113,19 @@ namespace .Extensions } public static class CollectionItemSatisfiesExtensions { - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) where TKey : notnull { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) where TKey : notnull { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) where TKey : notnull { } } public static class ComparisonAssertionExtensions 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 f13d8b1657..4874df1872 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 @@ -1517,7 +1517,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ListItemAtSource : ., ., .> + public class ListItemAtSource : ., ., .> where TList : . { public ListItemAtSource(. listContext, int index) { } @@ -1557,7 +1557,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ListLastItemSource : ., ., .> + public class ListLastItemSource : ., ., .> where TList : . { public ListLastItemSource(. listContext) { } @@ -1773,7 +1773,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ReadOnlyListItemAtSource : ., ., .> + public class ReadOnlyListItemAtSource : ., ., .> where TList : . { public ReadOnlyListItemAtSource(. listContext, int index) { } @@ -1813,7 +1813,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ReadOnlyListLastItemSource : ., ., .> + public class ReadOnlyListLastItemSource : ., ., .> where TList : . { public ReadOnlyListLastItemSource(. listContext) { } @@ -2504,7 +2504,7 @@ namespace .Core . IsTypeOf(); } public interface IDelegateAssertionSource : ., . { } - public interface IItemSatisfiesSource : ., . + public interface IItemSatisfiesSource : ., . { TResult Satisfies( assertion, string? expression = null) where TSource : .; @@ -3078,19 +3078,19 @@ namespace .Extensions } public static class CollectionItemSatisfiesExtensions { - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) where TKey : notnull { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) where TKey : notnull { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) where TKey : notnull { } } public static class ComparisonAssertionExtensions 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 9ef1cba110..54d39689d4 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 @@ -1534,7 +1534,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ListItemAtSource : ., ., .> + public class ListItemAtSource : ., ., .> where TList : . { public ListItemAtSource(. listContext, int index) { } @@ -1574,7 +1574,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ListLastItemSource : ., ., .> + public class ListLastItemSource : ., ., .> where TList : . { public ListLastItemSource(. listContext) { } @@ -1790,7 +1790,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ReadOnlyListItemAtSource : ., ., .> + public class ReadOnlyListItemAtSource : ., ., .> where TList : . { public ReadOnlyListItemAtSource(. listContext, int index) { } @@ -1830,7 +1830,7 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class ReadOnlyListLastItemSource : ., ., .> + public class ReadOnlyListLastItemSource : ., ., .> where TList : . { public ReadOnlyListLastItemSource(. listContext) { } @@ -2525,7 +2525,7 @@ namespace .Core . IsTypeOf(); } public interface IDelegateAssertionSource : ., . { } - public interface IItemSatisfiesSource : ., . + public interface IItemSatisfiesSource : ., . { TResult Satisfies( assertion, string? expression = null) where TSource : .; @@ -3113,19 +3113,19 @@ namespace .Extensions } public static class CollectionItemSatisfiesExtensions { - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this . source, <., .?> assertion, [.("assertion")] string? expression = null) { } + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) where TKey : notnull { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) where TKey : notnull { } - public static TResult Satisfies(this ., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) + public static TResult Satisfies(this .<., TResult> source, <., .?> assertion, [.("assertion")] string? expression = null) where TKey : notnull { } } public static class ComparisonAssertionExtensions From f4ae4b90c71f4f78f6e05a9a3e29046a136beb71 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:28:05 +0100 Subject: [PATCH 10/10] docs(assertions): align IReadOnlySet guard, document null + legacy ctors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three review-driven clarifications: 1. The IReadOnlySet overload in CollectionItemSatisfiesExtensions now sits inside #if NET5_0_OR_GREATER, matching the guard on ReadOnlySetAssertion itself. Functionally equivalent on the current netstandard2.0;net8;net9;net10 multi-target, but prevents a future netstandard2.1 target from compiling against a type that wouldn't be declared. 2. IAssertionSourceFor.Create gets a remarks block stating that implementations may receive a null item when callers select from a collection that allows nulls — the contract is "construct a source", not "validate non-null". 3. The four backward-compat public constructors on *ItemAtSatisfiesAssertion / *LastItemSatisfiesAssertion classes get remarks explaining why they wrap into ValueAssertion while the internal paths now go through the specialised IAssertionSourceFor factory — preserves binary compat for direct consumers from before the refactor. --- TUnit.Assertions/Conditions/ListAssertions.cs | 14 ++++++++++++++ .../Conditions/ReadOnlyListAssertions.cs | 14 ++++++++++++++ TUnit.Assertions/Core/IAssertionSourceFor.cs | 11 +++++++++++ .../CollectionItemSatisfiesExtensions.cs | 2 ++ 4 files changed, 41 insertions(+) diff --git a/TUnit.Assertions/Conditions/ListAssertions.cs b/TUnit.Assertions/Conditions/ListAssertions.cs index 631d683c63..4ca5e25cc4 100644 --- a/TUnit.Assertions/Conditions/ListAssertions.cs +++ b/TUnit.Assertions/Conditions/ListAssertions.cs @@ -482,6 +482,13 @@ public class ListItemAtSatisfiesAssertion : ListAssertionBase _assertion; + /// + /// Backward-compatible constructor preserved for direct consumers from before the + /// internal refactor to a raw-item delegate. Wraps the supplied factory in a + /// — the unspecialised path. Internal callers should + /// use the entry point + /// to get specialised assertion sources. + /// public ListItemAtSatisfiesAssertion( AssertionContext context, int index, @@ -667,6 +674,13 @@ public class ListLastItemSatisfiesAssertion : ListAssertionBase _assertion; + /// + /// Backward-compatible constructor preserved for direct consumers from before the + /// internal refactor to a raw-item delegate. Wraps the supplied factory in a + /// — the unspecialised path. Internal callers should + /// use the entry point + /// to get specialised assertion sources. + /// public ListLastItemSatisfiesAssertion( AssertionContext context, Func, Assertion?> assertion) diff --git a/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs b/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs index 6438b94c82..f098cedeee 100644 --- a/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs +++ b/TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs @@ -484,6 +484,13 @@ public class ReadOnlyListItemAtSatisfiesAssertion : ReadOnlyListAs private readonly int _index; private readonly Func _assertion; + /// + /// Backward-compatible constructor preserved for direct consumers from before the + /// internal refactor to a raw-item delegate. Wraps the supplied factory in a + /// — the unspecialised path. Internal callers should + /// use the entry + /// point to get specialised assertion sources. + /// public ReadOnlyListItemAtSatisfiesAssertion( AssertionContext context, int index, @@ -669,6 +676,13 @@ public class ReadOnlyListLastItemSatisfiesAssertion : ReadOnlyList { private readonly Func _assertion; + /// + /// Backward-compatible constructor preserved for direct consumers from before the + /// internal refactor to a raw-item delegate. Wraps the supplied factory in a + /// — the unspecialised path. Internal callers should + /// use the + /// entry point to get specialised assertion sources. + /// public ReadOnlyListLastItemSatisfiesAssertion( AssertionContext context, Func, Assertion?> assertion) diff --git a/TUnit.Assertions/Core/IAssertionSourceFor.cs b/TUnit.Assertions/Core/IAssertionSourceFor.cs index 0580d1acfb..fff3664693 100644 --- a/TUnit.Assertions/Core/IAssertionSourceFor.cs +++ b/TUnit.Assertions/Core/IAssertionSourceFor.cs @@ -18,6 +18,17 @@ namespace TUnit.Assertions.Core; public interface IAssertionSourceFor where TSelf : IAssertionSourceFor { + /// + /// Constructs the specialised assertion source wrapping with the + /// given . + /// + /// + /// Implementations may receive a when the + /// caller selects from a collection that allows nulls. Whether the resulting assertion + /// surfaces a graceful failure or throws on a subsequent operation is the implementer's + /// responsibility — the contract is "construct a source that callers can run assertions + /// against", not "validate non-null up front". + /// static abstract TSelf Create(TItem item, string label); } #endif diff --git a/TUnit.Assertions/Extensions/CollectionItemSatisfiesExtensions.cs b/TUnit.Assertions/Extensions/CollectionItemSatisfiesExtensions.cs index 82d2041b0e..3bb52670fc 100644 --- a/TUnit.Assertions/Extensions/CollectionItemSatisfiesExtensions.cs +++ b/TUnit.Assertions/Extensions/CollectionItemSatisfiesExtensions.cs @@ -63,11 +63,13 @@ public static TResult Satisfies( [CallerArgumentExpression(nameof(assertion))] string? expression = null) => source.Satisfies>(assertion, expression); +#if NET5_0_OR_GREATER public static TResult Satisfies( this IItemSatisfiesSource, TResult> source, Func, IAssertion?> assertion, [CallerArgumentExpression(nameof(assertion))] string? expression = null) => source.Satisfies>(assertion, expression); +#endif public static TResult Satisfies( this IItemSatisfiesSource source,