diff --git a/TUnit.Assertions.Tests/Bugs/Issue5707Tests.cs b/TUnit.Assertions.Tests/Bugs/Issue5707Tests.cs new file mode 100644 index 0000000000..e4ba59f8b0 --- /dev/null +++ b/TUnit.Assertions.Tests/Bugs/Issue5707Tests.cs @@ -0,0 +1,298 @@ +using TUnit.Assertions.Exceptions; + +namespace TUnit.Assertions.Tests.Bugs; + +/// +/// Regression tests for GitHub issue #5707: +/// `.Count(itemAssertion)` per-item overload only exposed a generic +/// `IAssertionSource<TItem>` inside the lambda, so specialised +/// assertions defined on collection / dictionary / set / list bases +/// (e.g. HasCount, ContainsKey, IsSubsetOf, +/// HasItemAt) were unreachable when the items themselves were +/// collections, dictionaries or sets. +/// +/// Specialised Count overloads now hand the lambda a typed +/// source (CollectionAssertion, ListAssertion, DictionaryAssertion, +/// SetAssertion, etc.) so the failure message also keeps the +/// specialised assertion's expectation rather than a generic wrapper. +/// +public class Issue5707Tests +{ + [Test] + public async Task Count_String_Items_Bind_To_Generic_Instance_Method() + { + // Strings already resolve through the generic instance method on + // CollectionAssertionBase: `IEnumerable` cannot bind to + // `string` via type inference (no concrete-to-interface unification), + // so no specialised string overload is needed. + var items = new List { "apple", "banana", "apricot", "cherry" }; + + await Assert.That(items).Count(s => s.IsEqualTo("apple")).IsEqualTo(1); + } + + [Test] + public async Task Count_Enumerable_Items_Reach_HasCount_On_Inner() + { + IEnumerable> listOfLists = new List> + { + new() { 1, 2, 3 }, + new() { 1 }, + new() { 1, 2, 3 }, + }; + + await Assert.That(listOfLists).Count(l => l.Count().IsEqualTo(3)).IsEqualTo(2); + } + + [Test] + public async Task Count_List_Items_Reach_HasItemAt_On_Inner() + { + var listOfLists = new List> + { + new List { 10, 20 }, + new List { 99 }, + new List { 10, 30 }, + }; + + await Assert.That(listOfLists).Count(l => l.HasItemAt(0, 10)).IsEqualTo(2); + } + + [Test] + public async Task Count_ReadOnlyList_Items_Reach_HasItemAt_On_Inner() + { + var listOfLists = new List> + { + new List { 10, 20 }, + new List { 99 }, + new List { 10, 30 }, + }; + + await Assert.That(listOfLists).Count(l => l.HasItemAt(0, 10)).IsEqualTo(2); + } + + [Test] + public async Task Count_ReadOnlyDictionary_Items_Reach_ContainsKey_On_Inner() + { + IReadOnlyDictionary a = new Dictionary { ["k"] = 1 }; + IReadOnlyDictionary b = new Dictionary { ["other"] = 2 }; + IReadOnlyDictionary c = new Dictionary { ["k"] = 5 }; + var dicts = new List> { a, b, c }; + + await Assert.That(dicts).Count(d => d.ContainsKey("k")).IsEqualTo(2); + } + + [Test] + public async Task Count_Dictionary_Items_Reach_ContainsKey_On_Inner() + { + var dicts = new List> + { + new Dictionary { ["k"] = 1 }, + new Dictionary { ["other"] = 2 }, + new Dictionary { ["k"] = 5 }, + }; + + await Assert.That(dicts).Count(d => d.ContainsKey("k")).IsEqualTo(2); + } + + [Test] + public async Task Count_Set_Items_Reach_IsSubsetOf_On_Inner() + { + var universe = new HashSet { 1, 2, 3, 4, 5 }; + var sets = new List> + { + new HashSet { 1, 2 }, + new HashSet { 6 }, + new HashSet { 3, 4 }, + }; + + await Assert.That(sets).Count(s => s.IsSubsetOf(universe)).IsEqualTo(2); + } + + [Test] + public async Task Count_ReadOnlySet_Items_Reach_IsSubsetOf_On_Inner() + { + var universe = new HashSet { 1, 2, 3, 4, 5 }; + var sets = new List> + { + new HashSet { 1, 2 }, + new HashSet { 6 }, + new HashSet { 3, 4 }, + }; + + await Assert.That(sets).Count(s => s.IsSubsetOf(universe)).IsEqualTo(2); + } + + [Test] + public async Task Count_Array_Items_Reach_IsSingleElement_On_Inner() + { + var listOfArrays = new List + { + new[] { 10, 20 }, + new[] { 99 }, + new[] { 10 }, + }; + + await Assert.That(listOfArrays).Count(a => a.IsSingleElement()).IsEqualTo(2); + } + + // ---- Concrete-type item tests --------------------------------------- + // C# generic inference resolves TItem to the exact declared type, never + // to an interface, so e.g. List> items must bind to a + // List overload, not the IList overload above. + + [Test] + public async Task Count_ConcreteList_Items_Reach_HasItemAt_On_Inner() + { + var listOfLists = new List> + { + new() { 10, 20 }, + new() { 99 }, + new() { 10, 30 }, + }; + + await Assert.That(listOfLists).Count(l => l.HasItemAt(0, 10)).IsEqualTo(2); + } + + [Test] + public async Task Count_ConcreteHashSet_Items_Reach_IsSubsetOf_On_Inner() + { + var universe = new HashSet { 1, 2, 3, 4, 5 }; + var sets = new List> + { + new() { 1, 2 }, + new() { 6 }, + new() { 3, 4 }, + }; + + await Assert.That(sets).Count(s => s.IsSubsetOf(universe)).IsEqualTo(2); + } + + [Test] + public async Task Count_ConcreteDictionary_Items_Reach_ContainsKey_On_Inner() + { + var dicts = new List> + { + new() { ["k"] = 1 }, + new() { ["other"] = 2 }, + new() { ["k"] = 5 }, + }; + + await Assert.That(dicts).Count(d => d.ContainsKey("k")).IsEqualTo(2); + } + + [Test] + public async Task Count_Specialised_Source_Failure_Message_Mentions_Inner_Expectation() + { + IEnumerable> listOfLists = new List> + { + new() { 1 }, + new() { 1 }, + }; + + // Expect 5 items with inner-count==3 → there are 0; ensure failure message + // surfaces the specialised inner expectation rather than just "received 0". + var ex = await Assert.That(async () => + await Assert.That(listOfLists).Count(l => l.Count().IsEqualTo(3)).IsEqualTo(5)) + .Throws(); + + // The chained expression should include `.Count(...)` per-item filter. + await Assert.That(ex.Message).Contains(".Count(l => l.Count().IsEqualTo(3))"); + } + + [Test] + public async Task Count_List_Failure_Message_Mentions_Specialised_Inner_Expectation() + { + var listOfLists = new List> + { + new List { 99 }, + new List { 99 }, + }; + + var ex = await Assert.That(async () => + await Assert.That(listOfLists).Count(l => l.HasItemAt(0, 10)).IsEqualTo(5)) + .Throws(); + + await Assert.That(ex.Message).Contains(".Count(l => l.HasItemAt(0, 10))"); + } + + [Test] + public async Task Count_Dictionary_Failure_Message_Mentions_Specialised_Inner_Expectation() + { + var dicts = new List> + { + new Dictionary { ["other"] = 1 }, + new Dictionary { ["other"] = 2 }, + }; + + var ex = await Assert.That(async () => + await Assert.That(dicts).Count(d => d.ContainsKey("k")).IsEqualTo(5)) + .Throws(); + + await Assert.That(ex.Message).Contains(".Count(d => d.ContainsKey(\"k\"))"); + } + + [Test] + public async Task Count_Set_Failure_Message_Mentions_Specialised_Inner_Expectation() + { + var universe = new HashSet { 1, 2, 3 }; + var sets = new List> + { + new HashSet { 6 }, + new HashSet { 7 }, + }; + + var ex = await Assert.That(async () => + await Assert.That(sets).Count(s => s.IsSubsetOf(universe)).IsEqualTo(5)) + .Throws(); + + await Assert.That(ex.Message).Contains(".Count(s => s.IsSubsetOf(universe))"); + } + + [Test] + public async Task Count_ReadOnlyList_Failure_Message_Mentions_Specialised_Inner_Expectation() + { + var listOfLists = new List> + { + new List { 99 }, + new List { 99 }, + }; + + var ex = await Assert.That(async () => + await Assert.That(listOfLists).Count(l => l.HasItemAt(0, 10)).IsEqualTo(5)) + .Throws(); + + await Assert.That(ex.Message).Contains(".Count(l => l.HasItemAt(0, 10))"); + } + + [Test] + public async Task Count_ReadOnlySet_Failure_Message_Mentions_Specialised_Inner_Expectation() + { + var universe = new HashSet { 1, 2, 3 }; + var sets = new List> + { + new HashSet { 6 }, + new HashSet { 7 }, + }; + + var ex = await Assert.That(async () => + await Assert.That(sets).Count(s => s.IsSubsetOf(universe)).IsEqualTo(5)) + .Throws(); + + await Assert.That(ex.Message).Contains(".Count(s => s.IsSubsetOf(universe))"); + } + + [Test] + public async Task Count_Array_Failure_Message_Mentions_Specialised_Inner_Expectation() + { + var listOfArrays = new List + { + new[] { 1, 2 }, + new[] { 1, 2 }, + }; + + var ex = await Assert.That(async () => + await Assert.That(listOfArrays).Count(a => a.IsSingleElement()).IsEqualTo(5)) + .Throws(); + + await Assert.That(ex.Message).Contains(".Count(a => a.IsSingleElement())"); + } +} diff --git a/TUnit.Assertions/Conditions/CollectionAssertions.cs b/TUnit.Assertions/Conditions/CollectionAssertions.cs index 4344fe22e2..6ffa85d8d5 100644 --- a/TUnit.Assertions/Conditions/CollectionAssertions.cs +++ b/TUnit.Assertions/Conditions/CollectionAssertions.cs @@ -686,7 +686,7 @@ protected override async Task CheckAsync(EvaluationMetadata CheckAsync(EvaluationMetadata where TCollection : IEnumerable { private readonly AssertionContext _collectionContext; - private readonly Func, Assertion?>? _assertion; + private readonly Func? _itemAssertionFactory; - public CollectionCountSource( + /// + /// Constructor used by the generic + /// instance method: wraps each item with before + /// invoking the user-supplied lambda. Specialised Count(itemAssertion) + /// extension overloads use the per-item factory ctor below to preserve + /// item-shape-specific assertion sources (issue #5707). + /// + internal CollectionCountSource( + AssertionContext collectionContext, + Func, IAssertion?>? assertion) + { + _collectionContext = collectionContext; + _itemAssertionFactory = assertion is null + ? null + : (item, index) => assertion(new ValueAssertion(item, $"item[{index}]")); + } + + /// + /// Internal constructor that accepts a per-item assertion factory directly. + /// Used by specialised Count(itemAssertion) overloads that supply + /// item-shape-specific assertion sources (e.g. collection, dictionary, set) + /// instead of the generic . + /// + internal CollectionCountSource( AssertionContext collectionContext, - Func, Assertion?>? assertion) + Func? itemAssertionFactory) { _collectionContext = collectionContext; - _assertion = assertion; + _itemAssertionFactory = itemAssertionFactory; } /// @@ -32,7 +55,7 @@ public CollectionCountEqualsAssertion IsEqualTo( { _collectionContext.ExpressionBuilder.Append($".IsEqualTo({expression})"); return new CollectionCountEqualsAssertion( - _collectionContext, _assertion, expected, CountComparison.Equal); + _collectionContext, _itemAssertionFactory, expected, CountComparison.Equal); } /// @@ -45,7 +68,7 @@ public CollectionCountEqualsAssertion IsNotEqualTo( { _collectionContext.ExpressionBuilder.Append($".IsNotEqualTo({expression})"); return new CollectionCountEqualsAssertion( - _collectionContext, _assertion, expected, CountComparison.NotEqual); + _collectionContext, _itemAssertionFactory, expected, CountComparison.NotEqual); } /// @@ -58,7 +81,7 @@ public CollectionCountEqualsAssertion IsGreaterThan( { _collectionContext.ExpressionBuilder.Append($".IsGreaterThan({expression})"); return new CollectionCountEqualsAssertion( - _collectionContext, _assertion, expected, CountComparison.GreaterThan); + _collectionContext, _itemAssertionFactory, expected, CountComparison.GreaterThan); } /// @@ -71,7 +94,7 @@ public CollectionCountEqualsAssertion IsGreaterThanOrEqualTo { _collectionContext.ExpressionBuilder.Append($".IsGreaterThanOrEqualTo({expression})"); return new CollectionCountEqualsAssertion( - _collectionContext, _assertion, expected, CountComparison.GreaterThanOrEqual); + _collectionContext, _itemAssertionFactory, expected, CountComparison.GreaterThanOrEqual); } /// @@ -84,7 +107,7 @@ public CollectionCountEqualsAssertion IsLessThan( { _collectionContext.ExpressionBuilder.Append($".IsLessThan({expression})"); return new CollectionCountEqualsAssertion( - _collectionContext, _assertion, expected, CountComparison.LessThan); + _collectionContext, _itemAssertionFactory, expected, CountComparison.LessThan); } /// @@ -97,7 +120,7 @@ public CollectionCountEqualsAssertion IsLessThanOrEqualTo( { _collectionContext.ExpressionBuilder.Append($".IsLessThanOrEqualTo({expression})"); return new CollectionCountEqualsAssertion( - _collectionContext, _assertion, expected, CountComparison.LessThanOrEqual); + _collectionContext, _itemAssertionFactory, expected, CountComparison.LessThanOrEqual); } /// @@ -108,7 +131,7 @@ public CollectionCountEqualsAssertion IsZero() { _collectionContext.ExpressionBuilder.Append(".IsZero()"); return new CollectionCountEqualsAssertion( - _collectionContext, _assertion, 0, CountComparison.Equal); + _collectionContext, _itemAssertionFactory, 0, CountComparison.Equal); } /// @@ -119,7 +142,7 @@ public CollectionCountEqualsAssertion IsPositive() { _collectionContext.ExpressionBuilder.Append(".IsPositive()"); return new CollectionCountEqualsAssertion( - _collectionContext, _assertion, 0, CountComparison.GreaterThan); + _collectionContext, _itemAssertionFactory, 0, CountComparison.GreaterThan); } } @@ -140,19 +163,19 @@ internal enum CountComparison public class CollectionCountEqualsAssertion : CollectionAssertionBase where TCollection : IEnumerable { - private readonly Func, Assertion?>? _itemAssertion; + private readonly Func? _itemAssertionFactory; private readonly int _expected; private readonly CountComparison _comparison; private int _actualCount; internal CollectionCountEqualsAssertion( AssertionContext context, - Func, Assertion?>? itemAssertion, + Func? itemAssertionFactory, int expected, CountComparison comparison) : base(context) { - _itemAssertion = itemAssertion; + _itemAssertionFactory = itemAssertionFactory; _expected = expected; _comparison = comparison; } @@ -173,7 +196,7 @@ protected override async Task CheckAsync(EvaluationMetadata CheckAsync(EvaluationMetadata(item, $"item[{index}]"); - var resultingAssertion = _itemAssertion(itemAssertionSource); + var resultingAssertion = _itemAssertionFactory(item, index); if (resultingAssertion != null) { @@ -200,7 +222,7 @@ protected override async Task CheckAsync(EvaluationMetadata IsNotDefined( return new Assertions.Enums.IsNotDefinedAssertion(source.Context); } + // ======================================================================== + // Specialised Count(itemAssertion) overloads — issue #5707. + // + // The instance-method `Count(Func, Assertion?>, ...)` + // on CollectionAssertionBase exposes only the generic `IAssertionSource` + // inside the lambda. When TItem itself is a collection, dictionary or set, + // specialised assertions defined as instance methods on the matching + // assertion-source base (e.g. `HasCount`, `ContainsKey`, `IsSubsetOf`, + // `HasItemAt`) are unreachable. + // + // These extension methods preserve the specialised assertion source by + // matching on the closed shape of TItem and constructing the appropriate + // typed source per item. C# generic type inference requires an exact match + // on the receiver's TItem (it does not unify a concrete type with an + // interface), so we provide overloads for both the interface shapes + // (IList, IDictionary, ISet, ...) and the most common concrete + // shapes (List, Dictionary, HashSet, T[]). Items whose shape + // does not match a specialised overload still resolve to the generic + // instance method. + // + // No overload is needed for `string` items: the `IEnumerable` + // overload below cannot bind to a string item (C# inference does not + // unify `string` with `IEnumerable` for a generic type parameter), + // so the generic instance method already wraps each string in + // `ValueAssertion` — which is exactly what a dedicated overload + // would do. + // ======================================================================== + + /// + /// Counts items satisfying an assertion expressed against an -typed source. + /// Use this overload when the collection's items are themselves enumerables; the lambda + /// receives a with the full collection assertion + /// surface (Contains, IsInOrder, HasCount, etc.). + /// + public static CollectionCountSource> Count( + this CollectionAssertionBase> source, + Func, IAssertion?> itemAssertion, + [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null) + where TCollection : IEnumerable> + { + return CountSpecialised>( + source, + (item, index) => itemAssertion(new CollectionAssertion(item, $"item[{index}]")), + expression); + } + + /// + /// Counts items satisfying an assertion expressed against an -typed source. + /// Use this overload when the collection's items are themselves lists; the lambda + /// receives a with index-based assertions in addition + /// to the standard collection surface. + /// + public static CollectionCountSource> Count( + this CollectionAssertionBase> source, + Func, IAssertion?> itemAssertion, + [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null) + where TCollection : IEnumerable> + { + return CountSpecialised>( + source, + (item, index) => itemAssertion(new ListAssertion(item, $"item[{index}]")), + expression); + } + + /// + /// Counts items satisfying an assertion expressed against an -typed source. + /// Use this overload when the collection's items are themselves read-only lists; the lambda + /// receives a with index-based assertions in addition + /// to the standard collection surface. + /// + public static CollectionCountSource> Count( + this CollectionAssertionBase> source, + Func, IAssertion?> itemAssertion, + [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null) + where TCollection : IEnumerable> + { + return CountSpecialised>( + source, + (item, index) => itemAssertion(new ReadOnlyListAssertion(item, $"item[{index}]")), + expression); + } + + /// + /// Counts items satisfying an assertion expressed against an -typed source. + /// Use this overload when the collection's items are themselves read-only dictionaries; the lambda + /// receives a with dictionary-specific assertions + /// (ContainsKey, ContainsValue, etc.) in addition to the standard collection surface. + /// + public static CollectionCountSource> Count( + this CollectionAssertionBase> source, + Func, IAssertion?> itemAssertion, + [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null) + where TCollection : IEnumerable> + where TKey : notnull + { + return CountSpecialised>( + source, + (item, index) => itemAssertion(new DictionaryAssertion(item, $"item[{index}]")), + expression); + } + + /// + /// Counts items satisfying an assertion expressed against an -typed source. + /// Use this overload when the collection's items are themselves dictionaries; the lambda + /// receives a with dictionary-specific assertions + /// in addition to the standard collection surface. + /// + public static CollectionCountSource> Count( + this CollectionAssertionBase> source, + Func, IAssertion?> itemAssertion, + [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null) + where TCollection : IEnumerable> + where TKey : notnull + { + return CountSpecialised>( + source, + (item, index) => itemAssertion(new MutableDictionaryAssertion(item, $"item[{index}]")), + expression); + } + + /// + /// Counts items satisfying an assertion expressed against an -typed source. + /// Use this overload when the collection's items are themselves sets; the lambda + /// receives a with set-specific assertions + /// (IsSubsetOf, IsSupersetOf, Overlaps, etc.) in addition to the standard collection surface. + /// + public static CollectionCountSource> Count( + this CollectionAssertionBase> source, + Func, IAssertion?> itemAssertion, + [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null) + where TCollection : IEnumerable> + { + return CountSpecialised>( + source, + (item, index) => itemAssertion(new SetAssertion(item, $"item[{index}]")), + expression); + } + +#if NET5_0_OR_GREATER + /// + /// Counts items satisfying an assertion expressed against an -typed source. + /// Use this overload when the collection's items are themselves read-only sets; the lambda + /// receives a with set-specific assertions + /// (IsSubsetOf, IsSupersetOf, Overlaps, etc.) in addition to the standard collection surface. + /// + public static CollectionCountSource> Count( + this CollectionAssertionBase> source, + Func, IAssertion?> itemAssertion, + [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null) + where TCollection : IEnumerable> + { + return CountSpecialised>( + source, + (item, index) => itemAssertion(new ReadOnlySetAssertion(item, $"item[{index}]")), + expression); + } +#endif + + /// + /// Counts items satisfying an assertion expressed against a []-typed source. + /// Use this overload when the collection's items are themselves arrays; the lambda + /// receives an so array-specific assertions + /// (e.g. IsEmpty, IsSingleElement) defined on IAssertionSource<TInner[]> + /// are reachable in addition to the standard collection surface. + /// + public static CollectionCountSource Count( + this CollectionAssertionBase source, + Func, IAssertion?> itemAssertion, + [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null) + where TCollection : IEnumerable + { + return CountSpecialised( + source, + (item, index) => itemAssertion(new ArrayAssertion(item, $"item[{index}]")), + expression); + } + + // ----- Concrete-type overloads ----- + // C# generic inference resolves TItem to the exact declared type, never to + // an interface, so e.g. `List>` items would not match the + // `ISet` overload above. These cover the common BCL concrete types. + + /// + /// Counts items satisfying an assertion expressed against a -typed source. + /// Use this overload when the collection's items are themselves instances; + /// the lambda receives a with index-based assertions in addition + /// to the standard collection surface. Without this overload, C# inference would not unify + /// List<TInner> with the IList<TInner> overload. + /// + public static CollectionCountSource> Count( + this CollectionAssertionBase> source, + Func, IAssertion?> itemAssertion, + [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null) + where TCollection : IEnumerable> + { + return CountSpecialised>( + source, + (item, index) => itemAssertion(new ListAssertion(item, $"item[{index}]")), + expression); + } + + /// + /// Counts items satisfying an assertion expressed against a -typed source. + /// Use this overload when the collection's items are themselves instances; + /// the lambda receives a with set-specific assertions + /// (IsSubsetOf, IsSupersetOf, Overlaps, etc.) in addition to the standard collection surface. + /// Without this overload, C# inference would not unify HashSet<TInner> with the + /// ISet<TInner> overload. + /// + public static CollectionCountSource> Count( + this CollectionAssertionBase> source, + Func, IAssertion?> itemAssertion, + [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null) + where TCollection : IEnumerable> + { + return CountSpecialised>( + source, + (item, index) => itemAssertion(new HashSetAssertion(item, $"item[{index}]")), + expression); + } + + /// + /// Counts items satisfying an assertion expressed against a -typed source. + /// Use this overload when the collection's items are themselves instances; + /// the lambda receives a with dictionary-specific + /// assertions (ContainsKey, ContainsValue, etc.) in addition to the standard collection surface. + /// Without this overload, C# inference would not unify Dictionary<TKey, TValue> with the + /// IDictionary<TKey, TValue> overload. + /// + public static CollectionCountSource> Count( + this CollectionAssertionBase> source, + Func, IAssertion?> itemAssertion, + [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null) + where TCollection : IEnumerable> + where TKey : notnull + { + return CountSpecialised>( + source, + (item, index) => itemAssertion(new MutableDictionaryAssertion(item, $"item[{index}]")), + expression); + } + + private static CollectionCountSource CountSpecialised( + CollectionAssertionBase source, + Func itemAssertionFactory, + string? expression) + where TCollection : IEnumerable + { + var context = source.InternalContext; + context.ExpressionBuilder.Append($".Count({expression})"); + return new CollectionCountSource(context, itemAssertionFactory); + } } diff --git a/TUnit.Assertions/Sources/CollectionAssertionBase.cs b/TUnit.Assertions/Sources/CollectionAssertionBase.cs index 62447cf0ed..c5d7c24454 100644 --- a/TUnit.Assertions/Sources/CollectionAssertionBase.cs +++ b/TUnit.Assertions/Sources/CollectionAssertionBase.cs @@ -139,7 +139,8 @@ public CollectionContainsPredicateAssertion Contains( public CollectionCountSource Count() { Context.ExpressionBuilder.Append(".Count()"); - return new CollectionCountSource(Context, null); + return new CollectionCountSource( + Context, (Func?)null); } /// @@ -165,7 +166,7 @@ public CollectionCountWithInlineAssertionAssertion Count( /// Example: await Assert.That(list).Count(item => item.IsGreaterThan(10)).IsEqualTo(3).And.Contains(1); /// public CollectionCountSource Count( - Func, Assertion?> itemAssertion, + Func, IAssertion?> itemAssertion, [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null) { Context.ExpressionBuilder.Append($".Count({expression})"); diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt index 54fa8fd2df..51de4645a0 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 @@ -817,7 +817,6 @@ namespace .Conditions public class CollectionCountSource where TCollection : . { - public CollectionCountSource(. collectionContext, <., .?>? assertion) { } public . IsEqualTo(int expected, [.("expected")] string? expression = null) { } public . IsGreaterThan(int expected, [.("expected")] string? expression = null) { } public . IsGreaterThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } @@ -2631,6 +2630,31 @@ namespace .Extensions { public static . CompletesWithin(this . source, timeout, [.("timeout")] string? expression = null) { } public static . CompletesWithin(this . source, timeout, [.("timeout")] string? expression = null) { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static . Count(this . source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : . { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> + where TKey : notnull { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> + where TKey : notnull { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> + where TKey : notnull { } public static . EqualTo(this . source, TValue? expected, [.("expected")] string? expression = null) { } public static . Eventually(this . source, <., .> assertionBuilder, timeout, ? pollingInterval = default, [.("timeout")] string? timeoutExpression = null, [.("pollingInterval")] string? pollingIntervalExpression = null) { } [("Use Length() instead, which provides all numeric assertion methods. Example: Asse" + @@ -6155,7 +6179,7 @@ namespace .Sources public . Contains(TItem expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } public . ContainsOnly( predicate, [.("predicate")] string? expression = null) { } public . Count() { } - public . Count(<., .?> itemAssertion, [.("itemAssertion")] string? expression = null) { } + public . Count(<., .?> itemAssertion, [.("itemAssertion")] string? expression = null) { } [.(-1)] public . Count(<., .?> countAssertion, [.("countAssertion")] string? expression = null) { } public . DoesNotContain( predicate, [.("predicate")] string? expression = null) { } diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt index 4a85957834..0503d5c1c0 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 @@ -800,7 +800,6 @@ namespace .Conditions public class CollectionCountSource where TCollection : . { - public CollectionCountSource(. collectionContext, <., .?>? assertion) { } public . IsEqualTo(int expected, [.("expected")] string? expression = null) { } public . IsGreaterThan(int expected, [.("expected")] string? expression = null) { } public . IsGreaterThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } @@ -2610,6 +2609,31 @@ namespace .Extensions { public static . CompletesWithin(this . source, timeout, [.("timeout")] string? expression = null) { } public static . CompletesWithin(this . source, timeout, [.("timeout")] string? expression = null) { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static . Count(this . source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : . { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> + where TKey : notnull { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> + where TKey : notnull { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> + where TKey : notnull { } public static . EqualTo(this . source, TValue? expected, [.("expected")] string? expression = null) { } public static . Eventually(this . source, <., .> assertionBuilder, timeout, ? pollingInterval = default, [.("timeout")] string? timeoutExpression = null, [.("pollingInterval")] string? pollingIntervalExpression = null) { } [("Use Length() instead, which provides all numeric assertion methods. Example: Asse" + @@ -6086,7 +6110,7 @@ namespace .Sources public . Contains(TItem expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } public . ContainsOnly( predicate, [.("predicate")] string? expression = null) { } public . Count() { } - public . Count(<., .?> itemAssertion, [.("itemAssertion")] string? expression = null) { } + public . Count(<., .?> itemAssertion, [.("itemAssertion")] string? expression = null) { } public . Count(<., .?> countAssertion, [.("countAssertion")] string? expression = null) { } public . DoesNotContain( predicate, [.("predicate")] string? expression = null) { } public . DoesNotContain(TItem expected, [.("expected")] string? expression = null) { } diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt index c20c21a71f..218d6b9e32 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 @@ -817,7 +817,6 @@ namespace .Conditions public class CollectionCountSource where TCollection : . { - public CollectionCountSource(. collectionContext, <., .?>? assertion) { } public . IsEqualTo(int expected, [.("expected")] string? expression = null) { } public . IsGreaterThan(int expected, [.("expected")] string? expression = null) { } public . IsGreaterThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } @@ -2631,6 +2630,31 @@ namespace .Extensions { public static . CompletesWithin(this . source, timeout, [.("timeout")] string? expression = null) { } public static . CompletesWithin(this . source, timeout, [.("timeout")] string? expression = null) { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static . Count(this . source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : . { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> + where TKey : notnull { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> + where TKey : notnull { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> + where TKey : notnull { } public static . EqualTo(this . source, TValue? expected, [.("expected")] string? expression = null) { } public static . Eventually(this . source, <., .> assertionBuilder, timeout, ? pollingInterval = default, [.("timeout")] string? timeoutExpression = null, [.("pollingInterval")] string? pollingIntervalExpression = null) { } [("Use Length() instead, which provides all numeric assertion methods. Example: Asse" + @@ -6155,7 +6179,7 @@ namespace .Sources public . Contains(TItem expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } public . ContainsOnly( predicate, [.("predicate")] string? expression = null) { } public . Count() { } - public . Count(<., .?> itemAssertion, [.("itemAssertion")] string? expression = null) { } + public . Count(<., .?> itemAssertion, [.("itemAssertion")] string? expression = null) { } [.(-1)] public . Count(<., .?> countAssertion, [.("countAssertion")] string? expression = null) { } public . DoesNotContain( predicate, [.("predicate")] string? expression = null) { } diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt index a106aa161c..d4b8c212ba 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 @@ -657,7 +657,6 @@ namespace .Conditions public class CollectionCountSource where TCollection : . { - public CollectionCountSource(. collectionContext, <., .?>? assertion) { } public . IsEqualTo(int expected, [.("expected")] string? expression = null) { } public . IsGreaterThan(int expected, [.("expected")] string? expression = null) { } public . IsGreaterThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } @@ -2366,6 +2365,29 @@ namespace .Extensions { public static . CompletesWithin(this . source, timeout, [.("timeout")] string? expression = null) { } public static . CompletesWithin(this . source, timeout, [.("timeout")] string? expression = null) { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> { } + public static . Count(this . source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : . { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> + where TKey : notnull { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> + where TKey : notnull { } + public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null) + where TCollection : .<.> + where TKey : notnull { } public static . EqualTo(this . source, TValue? expected, [.("expected")] string? expression = null) { } public static . Eventually(this . source, <., .> assertionBuilder, timeout, ? pollingInterval = default, [.("timeout")] string? timeoutExpression = null, [.("pollingInterval")] string? pollingIntervalExpression = null) { } [("Use Length() instead, which provides all numeric assertion methods. Example: Asse" + @@ -5327,7 +5349,7 @@ namespace .Sources public . Contains(TItem expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } public . ContainsOnly( predicate, [.("predicate")] string? expression = null) { } public . Count() { } - public . Count(<., .?> itemAssertion, [.("itemAssertion")] string? expression = null) { } + public . Count(<., .?> itemAssertion, [.("itemAssertion")] string? expression = null) { } public . Count(<., .?> countAssertion, [.("countAssertion")] string? expression = null) { } public . DoesNotContain( predicate, [.("predicate")] string? expression = null) { } public . DoesNotContain(TItem expected, [.("expected")] string? expression = null) { }