Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(assertions): address PR review for #5707
- Add Count(itemAssertion) overload for IReadOnlySet<TInner>
- Drop legacy constructor on CollectionCountEqualsAssertion (unused)
- Narrow bare catch to preserve OperationCanceledException semantics
  • Loading branch information
thomhurst committed Apr 25, 2026
commit 9eb3f343888b13838683a4c6e02451a647ab4f92
14 changes: 14 additions & 0 deletions TUnit.Assertions.Tests/Bugs/Issue5707Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,20 @@ public async Task Count_Set_Items_Reach_IsSubsetOf_On_Inner()
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<int> { 1, 2, 3, 4, 5 };
var sets = new List<IReadOnlySet<int>>
{
new HashSet<int> { 1, 2 },
new HashSet<int> { 6 },
new HashSet<int> { 3, 4 },
};

await Assert.That(sets).Count(s => s.IsSubsetOf(universe)).IsEqualTo(2);
}

[Test]
public async Task Count_Specialised_Source_Failure_Message_Mentions_Inner_Expectation()
{
Expand Down
27 changes: 5 additions & 22 deletions TUnit.Assertions/Conditions/CollectionCountSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ public class CollectionCountSource<TCollection, TItem>
public CollectionCountSource(
AssertionContext<TCollection> collectionContext,
Func<IAssertionSource<TItem>, Assertion<TItem>?>? assertion)
: this(collectionContext, WrapWithValueAssertion(assertion))
{
_collectionContext = collectionContext;
_itemAssertionFactory = assertion is null
? null
: (item, index) => assertion(new ValueAssertion<TItem>(item, $"item[{index}]"));
}

/// <summary>
Expand All @@ -35,17 +38,6 @@ internal CollectionCountSource(
_itemAssertionFactory = itemAssertionFactory;
}

internal static Func<TItem, int, IAssertion?>? WrapWithValueAssertion(
Func<IAssertionSource<TItem>, Assertion<TItem>?>? assertion)
{
if (assertion is null)
{
return null;
}

return (item, index) => assertion(new ValueAssertion<TItem>(item, $"item[{index}]"));
}

/// <summary>
/// Asserts that the count is equal to the expected value.
/// Returns a collection-aware assertion that allows further collection chaining.
Expand Down Expand Up @@ -169,15 +161,6 @@ public class CollectionCountEqualsAssertion<TCollection, TItem> : CollectionAsse
private readonly CountComparison _comparison;
private int _actualCount;

internal CollectionCountEqualsAssertion(
AssertionContext<TCollection> context,
Func<IAssertionSource<TItem>, Assertion<TItem>?>? itemAssertion,
int expected,
CountComparison comparison)
: this(context, CollectionCountSource<TCollection, TItem>.WrapWithValueAssertion(itemAssertion), expected, comparison)
{
}

internal CollectionCountEqualsAssertion(
AssertionContext<TCollection> context,
Func<TItem, int, IAssertion?>? itemAssertionFactory,
Expand Down Expand Up @@ -232,7 +215,7 @@ protected override async Task<AssertionResult> CheckAsync(EvaluationMetadata<TCo
await resultingAssertion.AssertAsync();
_actualCount++;
}
catch
catch (Exception ex) when (ex is not OperationCanceledException)
{
// Item did not satisfy the assertion, don't count it
}
Expand Down
20 changes: 20 additions & 0 deletions TUnit.Assertions/Extensions/AssertionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1968,6 +1968,26 @@ public static CollectionCountSource<TCollection, ISet<TInner>> Count<TCollection
expression);
}

#if NET5_0_OR_GREATER
/// <summary>
/// Counts items satisfying an assertion expressed against an <see cref="IReadOnlySet{TInner}"/>-typed source.
/// Use this overload when the collection's items are themselves read-only sets; the lambda
/// receives a <see cref="ReadOnlySetAssertion{TInner}"/> with set-specific assertions
/// (IsSubsetOf, IsSupersetOf, Overlaps, etc.) in addition to the standard collection surface.
/// </summary>
public static CollectionCountSource<TCollection, IReadOnlySet<TInner>> Count<TCollection, TInner>(
this CollectionAssertionBase<TCollection, IReadOnlySet<TInner>> source,
Func<ReadOnlySetAssertion<TInner>, IAssertion?> itemAssertion,
[CallerArgumentExpression(nameof(itemAssertion))] string? expression = null)
where TCollection : IEnumerable<IReadOnlySet<TInner>>
{
return CountSpecialised<TCollection, IReadOnlySet<TInner>>(
source,
(item, index) => itemAssertion(new ReadOnlySetAssertion<TInner>(item, $"item[{index}]")),
expression);
}
#endif

private static CollectionCountSource<TCollection, TItem> CountSpecialised<TCollection, TItem>(
CollectionAssertionBase<TCollection, TItem> source,
Func<TItem, int, IAssertion?> itemAssertionFactory,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2641,6 +2641,8 @@ namespace .Extensions
where TCollection : .<.<TInner>> { }
public static .<TCollection, .<TInner>> Count<TCollection, TInner>(this .<TCollection, .<TInner>> source, <.<TInner>, .?> itemAssertion, [.("itemAssertion")] string? expression = null)
where TCollection : .<.<TInner>> { }
public static .<TCollection, .<TInner>> Count<TCollection, TInner>(this .<TCollection, .<TInner>> source, <.<TInner>, .?> itemAssertion, [.("itemAssertion")] string? expression = null)
where TCollection : .<.<TInner>> { }
public static .<TCollection, .<TKey, TValue>> Count<TCollection, TKey, TValue>(this .<TCollection, .<TKey, TValue>> source, <.<TKey, TValue>, .?> itemAssertion, [.("itemAssertion")] string? expression = null)
where TCollection : .<.<TKey, TValue>>
where TKey : notnull { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2620,6 +2620,8 @@ namespace .Extensions
where TCollection : .<.<TInner>> { }
public static .<TCollection, .<TInner>> Count<TCollection, TInner>(this .<TCollection, .<TInner>> source, <.<TInner>, .?> itemAssertion, [.("itemAssertion")] string? expression = null)
where TCollection : .<.<TInner>> { }
public static .<TCollection, .<TInner>> Count<TCollection, TInner>(this .<TCollection, .<TInner>> source, <.<TInner>, .?> itemAssertion, [.("itemAssertion")] string? expression = null)
where TCollection : .<.<TInner>> { }
public static .<TCollection, .<TKey, TValue>> Count<TCollection, TKey, TValue>(this .<TCollection, .<TKey, TValue>> source, <.<TKey, TValue>, .?> itemAssertion, [.("itemAssertion")] string? expression = null)
where TCollection : .<.<TKey, TValue>>
where TKey : notnull { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2641,6 +2641,8 @@ namespace .Extensions
where TCollection : .<.<TInner>> { }
public static .<TCollection, .<TInner>> Count<TCollection, TInner>(this .<TCollection, .<TInner>> source, <.<TInner>, .?> itemAssertion, [.("itemAssertion")] string? expression = null)
where TCollection : .<.<TInner>> { }
public static .<TCollection, .<TInner>> Count<TCollection, TInner>(this .<TCollection, .<TInner>> source, <.<TInner>, .?> itemAssertion, [.("itemAssertion")] string? expression = null)
where TCollection : .<.<TInner>> { }
public static .<TCollection, .<TKey, TValue>> Count<TCollection, TKey, TValue>(this .<TCollection, .<TKey, TValue>> source, <.<TKey, TValue>, .?> itemAssertion, [.("itemAssertion")] string? expression = null)
where TCollection : .<.<TKey, TValue>>
where TKey : notnull { }
Expand Down
Loading