Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
298 changes: 298 additions & 0 deletions TUnit.Assertions.Tests/Bugs/Issue5707Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
using TUnit.Assertions.Exceptions;

namespace TUnit.Assertions.Tests.Bugs;

/// <summary>
/// Regression tests for GitHub issue #5707:
/// `.Count(itemAssertion)` per-item overload only exposed a generic
/// `IAssertionSource&lt;TItem&gt;` inside the lambda, so specialised
/// assertions defined on collection / dictionary / set / list bases
/// (e.g. <c>HasCount</c>, <c>ContainsKey</c>, <c>IsSubsetOf</c>,
/// <c>HasItemAt</c>) were unreachable when the items themselves were
/// collections, dictionaries or sets.
///
/// Specialised <c>Count</c> 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.
/// </summary>
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<TInner>` cannot bind to
// `string` via type inference (no concrete-to-interface unification),
// so no specialised string overload is needed.
var items = new List<string> { "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<IEnumerable<int>> listOfLists = new List<List<int>>
{
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<IList<int>>
{
new List<int> { 10, 20 },
new List<int> { 99 },
new List<int> { 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<IReadOnlyList<int>>
{
new List<int> { 10, 20 },
new List<int> { 99 },
new List<int> { 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<string, int> a = new Dictionary<string, int> { ["k"] = 1 };
IReadOnlyDictionary<string, int> b = new Dictionary<string, int> { ["other"] = 2 };
IReadOnlyDictionary<string, int> c = new Dictionary<string, int> { ["k"] = 5 };
var dicts = new List<IReadOnlyDictionary<string, int>> { 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<IDictionary<string, int>>
{
new Dictionary<string, int> { ["k"] = 1 },
new Dictionary<string, int> { ["other"] = 2 },
new Dictionary<string, int> { ["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<int> { 1, 2, 3, 4, 5 };
var sets = new List<ISet<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_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_Array_Items_Reach_IsSingleElement_On_Inner()
{
var listOfArrays = new List<int[]>
{
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<List<int>> items must bind to a
// List<TInner> overload, not the IList<TInner> overload above.

[Test]
public async Task Count_ConcreteList_Items_Reach_HasItemAt_On_Inner()
{
var listOfLists = new List<List<int>>
{
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<int> { 1, 2, 3, 4, 5 };
var sets = new List<HashSet<int>>
{
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<Dictionary<string, int>>
{
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<IEnumerable<int>> listOfLists = new List<List<int>>
{
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<AssertionException>();

// 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<IList<int>>
{
new List<int> { 99 },
new List<int> { 99 },
};

var ex = await Assert.That(async () =>
await Assert.That(listOfLists).Count(l => l.HasItemAt(0, 10)).IsEqualTo(5))
.Throws<AssertionException>();

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<IDictionary<string, int>>
{
new Dictionary<string, int> { ["other"] = 1 },
new Dictionary<string, int> { ["other"] = 2 },
};

var ex = await Assert.That(async () =>
await Assert.That(dicts).Count(d => d.ContainsKey("k")).IsEqualTo(5))
.Throws<AssertionException>();

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<int> { 1, 2, 3 };
var sets = new List<ISet<int>>
{
new HashSet<int> { 6 },
new HashSet<int> { 7 },
};

var ex = await Assert.That(async () =>
await Assert.That(sets).Count(s => s.IsSubsetOf(universe)).IsEqualTo(5))
.Throws<AssertionException>();

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<IReadOnlyList<int>>
{
new List<int> { 99 },
new List<int> { 99 },
};

var ex = await Assert.That(async () =>
await Assert.That(listOfLists).Count(l => l.HasItemAt(0, 10)).IsEqualTo(5))
.Throws<AssertionException>();

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<int> { 1, 2, 3 };
var sets = new List<IReadOnlySet<int>>
{
new HashSet<int> { 6 },
new HashSet<int> { 7 },
};

var ex = await Assert.That(async () =>
await Assert.That(sets).Count(s => s.IsSubsetOf(universe)).IsEqualTo(5))
.Throws<AssertionException>();

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<int[]>
{
new[] { 1, 2 },
new[] { 1, 2 },
};

var ex = await Assert.That(async () =>
await Assert.That(listOfArrays).Count(a => a.IsSingleElement()).IsEqualTo(5))
.Throws<AssertionException>();

await Assert.That(ex.Message).Contains(".Count(a => a.IsSingleElement())");
}
}
4 changes: 2 additions & 2 deletions TUnit.Assertions/Conditions/CollectionAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@ protected override async Task<AssertionResult> CheckAsync(EvaluationMetadata<TCo
await assertion.AssertAsync();
}
}
catch (Exception ex)
catch (Exception ex) when (ex is not OperationCanceledException)
{
return AssertionResult.Failed($"item at index {index} failed assertion: {ex.Message}", ex);
}
Expand Down Expand Up @@ -753,7 +753,7 @@ protected override async Task<AssertionResult> CheckAsync(EvaluationMetadata<TCo
await resultingAssertion.AssertAsync();
}
}
catch (Exception ex)
catch (Exception ex) when (ex is not OperationCanceledException)
{
return AssertionResult.Failed($"item at index {index} (mapped by {_mapperDescription}) failed assertion: {ex.Message}", ex);
}
Expand Down
Loading
Loading