Include original index in extraneous item failure messages by dennisdoomen · Pull Request #3203 · fluentassertions/fluentassertions · GitHub
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ internal class LooselyOrderedEquivalencyStrategy<TExpectation>(
// Populated lazily during Phase 3 for the ~n selected pairs with full formatting.
private Dictionary<(object Subject, object Expectation, int ExpectationIndex), string[]> fullFailuresCache = new();

public void FindAndRemoveMatches(List<object> subjects, List<TExpectation> expectations)
public void FindAndRemoveMatches(List<IndexedItem<object>> subjects, List<TExpectation> expectations)
{
countCache = new(new ReferentialComparer());
fullFailuresCache = new(new ReferentialComparer());
Expand All @@ -45,7 +45,7 @@ public void FindAndRemoveMatches(List<object> subjects, List<TExpectation> expec
expectationsWithIndexes.RemoveMatchedItemFrom(expectations);
}

private void FindAndRemoveExactMatches(List<object> subjects, IndexedItemCollection<TExpectation> expectationsWithIndexes)
private void FindAndRemoveExactMatches(List<IndexedItem<object>> subjects, IndexedItemCollection<TExpectation> expectationsWithIndexes)
{
int expectationIndex = 0;
while (expectationsWithIndexes.Count > expectationIndex)
Expand All @@ -65,14 +65,14 @@ private void FindAndRemoveExactMatches(List<object> subjects, IndexedItemCollect
}

[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
private bool StrictlyMatchAgainst(List<object> remainingSubjects, TExpectation expectation, int expectationIndex)
private bool StrictlyMatchAgainst(List<IndexedItem<object>> remainingSubjects, TExpectation expectation, int expectationIndex)
{
foreach ((int index, object subject) in remainingSubjects.Index())
foreach ((int index, IndexedItem<object> subject) in remainingSubjects.Index())
{
using var _ = tracer.WriteBlock(member =>
$"Comparing subject at {member.Subject}[{index}] with the expectation at {member.Expectation}[{expectationIndex}]");

int failures = TryToMatchCount(expectation, subject, expectationIndex);
int failures = TryToMatchCount(expectation, subject.Item, expectationIndex);
if (failures == 0)
{
tracer.WriteLine(_ => "It's a match");
Expand All @@ -91,7 +91,7 @@ private bool StrictlyMatchAgainst(List<object> remainingSubjects, TExpectation e
/// that are most similar to remaining subjects first, the algorithm is more likely
/// to find the correct pairings earlier, leading to better error messages when mismatches occur.
/// </summary>
private IndexedItemCollection<TExpectation> SortExpectationsByMinDistance(List<object> remainingSubjects,
private IndexedItemCollection<TExpectation> SortExpectationsByMinDistance(List<IndexedItem<object>> remainingSubjects,
IndexedItemCollection<TExpectation> expectationsWithIndexes)
{
if (remainingSubjects.Count > 0)
Expand All @@ -100,7 +100,7 @@ private IndexedItemCollection<TExpectation> SortExpectationsByMinDistance(List<o
.Select(e => new
{
Expectation = e,
MinDistance = remainingSubjects.Min(a => TryToMatchCount(e.Item, a, e.Index))
MinDistance = remainingSubjects.Min(a => TryToMatchCount(e.Item, a.Item, e.Index))
})
.OrderBy(x => x.MinDistance)
.Select(x => x.Expectation)
Expand All @@ -112,13 +112,13 @@ private IndexedItemCollection<TExpectation> SortExpectationsByMinDistance(List<o
}
}

private void FindAndRemoveClosestMatches(List<object> remainingSubjects,
private void FindAndRemoveClosestMatches(List<IndexedItem<object>> remainingSubjects,
IndexedItemCollection<TExpectation> expectationsWithIndexes)
{
int nrFailures = 0;
if (expectationsWithIndexes.Count > 0 && remainingSubjects.Count > 0)
{
IReadOnlyList<(IndexedItem<TExpectation>, object, string[])> bestMatches =
IReadOnlyList<(IndexedItem<TExpectation>, IndexedItem<object>, string[])> bestMatches =
FindClosestMismatches(remainingSubjects, expectationsWithIndexes);

foreach (var (expectation, subject, failures) in bestMatches)
Expand All @@ -138,8 +138,8 @@ private void FindAndRemoveClosestMatches(List<object> remainingSubjects,
}
}

private IReadOnlyList<(IndexedItem<TExpectation> Expectation, object Actual, string[] Failures)> FindClosestMismatches(
List<object> remainingSubjects, IndexedItemCollection<TExpectation> expectationsWithIndexes)
private IReadOnlyList<(IndexedItem<TExpectation> Expectation, IndexedItem<object> Actual, string[] Failures)> FindClosestMismatches(
List<IndexedItem<object>> remainingSubjects, IndexedItemCollection<TExpectation> expectationsWithIndexes)
{
// For small collections, use exact permutation search to find the globally optimal assignment.
// factorial(8) = 40,320 which is well within reason.
Expand All @@ -155,21 +155,21 @@ private void FindAndRemoveClosestMatches(List<object> remainingSubjects,
/// Uses failure counts for scoring (no string formatting) and only fetches full failure strings for the
/// winning assignment.
/// </summary>
private IReadOnlyList<(IndexedItem<TExpectation> Expectation, object Actual, string[] Failures)> FindClosestMismatchesByPermutation(
List<object> remainingSubjects, IndexedItemCollection<TExpectation> expectationsWithIndexes)
private IReadOnlyList<(IndexedItem<TExpectation> Expectation, IndexedItem<object> Actual, string[] Failures)> FindClosestMismatchesByPermutation(
List<IndexedItem<object>> remainingSubjects, IndexedItemCollection<TExpectation> expectationsWithIndexes)
{
var bestScore = int.MaxValue;
IReadOnlyList<object> bestAssignment = null;
IReadOnlyList<IndexedItem<object>> bestAssignment = null;

foreach (IReadOnlyList<object> assignment in remainingSubjects.Permute())
foreach (IReadOnlyList<IndexedItem<object>> assignment in remainingSubjects.Permute())
{
int score = 0;
bool tooHigh = false;

for (int index = 0; index < expectationsWithIndexes.Count && index < assignment.Count; index++)
{
IndexedItem<TExpectation> expectationWithIndex = expectationsWithIndexes[index];
score += TryToMatchCount(expectationWithIndex.Item, assignment[index], expectationWithIndex.Index);
score += TryToMatchCount(expectationWithIndex.Item, assignment[index].Item, expectationWithIndex.Index);

if (score >= bestScore)
{
Expand All @@ -187,17 +187,17 @@ private void FindAndRemoveClosestMatches(List<object> remainingSubjects,

if (bestAssignment is null)
{
return Array.Empty<(IndexedItem<TExpectation>, object, string[])>();
return Array.Empty<(IndexedItem<TExpectation>, IndexedItem<object>, string[])>();
}

// Fetch full failure strings only for the winning assignment.
int pairCount = Math.Min(expectationsWithIndexes.Count, bestAssignment.Count);
var result = new List<(IndexedItem<TExpectation>, object, string[])>(pairCount);
var result = new List<(IndexedItem<TExpectation>, IndexedItem<object>, string[])>(pairCount);

for (int index = 0; index < pairCount; index++)
{
IndexedItem<TExpectation> expectationWithIndex = expectationsWithIndexes[index];
string[] failures = TryToMatch(expectationWithIndex.Item, bestAssignment[index], expectationWithIndex.Index);
string[] failures = TryToMatch(expectationWithIndex.Item, bestAssignment[index].Item, expectationWithIndex.Index);
result.Add((expectationWithIndex, bestAssignment[index], failures));
}

Expand All @@ -209,8 +209,8 @@ private void FindAndRemoveClosestMatches(List<object> remainingSubjects,
/// permutation search would be prohibitively expensive. All distances are already cached from Phase 1, so
/// this is O(n² log n) rather than O(n! × n).
/// </summary>
private IReadOnlyList<(IndexedItem<TExpectation> Expectation, object Actual, string[] Failures)> FindClosestMismatchesByGreedyAssignment(
List<object> remainingSubjects, IndexedItemCollection<TExpectation> expectationsWithIndexes)
private IReadOnlyList<(IndexedItem<TExpectation> Expectation, IndexedItem<object> Actual, string[] Failures)> FindClosestMismatchesByGreedyAssignment(
List<IndexedItem<object>> remainingSubjects, IndexedItemCollection<TExpectation> expectationsWithIndexes)
{
int subjectCount = remainingSubjects.Count;
int expectationCount = expectationsWithIndexes.Count;
Expand All @@ -225,7 +225,7 @@ private void FindAndRemoveClosestMatches(List<object> remainingSubjects,

for (int subjectIndex = 0; subjectIndex < subjectCount; subjectIndex++)
{
int count = TryToMatchCount(exp.Item, remainingSubjects[subjectIndex], exp.Index);
int count = TryToMatchCount(exp.Item, remainingSubjects[subjectIndex].Item, exp.Index);
allPairs.Add((expectationIndex, subjectIndex, count));
}
}
Expand All @@ -248,7 +248,7 @@ private void FindAndRemoveClosestMatches(List<object> remainingSubjects,
var assignedSubjectIndexes = new bool[subjectCount];
int totalToAssign = Math.Min(expectationCount, subjectCount);

var result = new List<(IndexedItem<TExpectation>, object, string[])>(totalToAssign);
var result = new List<(IndexedItem<TExpectation>, IndexedItem<object>, string[])>(totalToAssign);

// First checks candidate matches from best to worst, then picks the first unused expectation/subject pair it finds,
// computes detailed failures only for that chosen pair, and then repeats until all possible matches are assigned.
Expand All @@ -258,7 +258,7 @@ private void FindAndRemoveClosestMatches(List<object> remainingSubjects,
{
// Fetch full failure strings only for the selected pairs (~n total).
string[] failures = TryToMatch(expectationsWithIndexes[expectationIndex].Item,
remainingSubjects[subjectIndex], expectationsWithIndexes[expectationIndex].Index);
remainingSubjects[subjectIndex].Item, expectationsWithIndexes[expectationIndex].Index);

result.Add((expectationsWithIndexes[expectationIndex], remainingSubjects[subjectIndex], failures));
assignedExpectationIndexes[expectationIndex] = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ internal class StrictlyOrderedEquivalencyStrategy<TExpectation>(
private const int FailedItemsFastFailThreshold = 10;
private readonly Tracer tracer = context.Tracer;

public void FindAndRemoveMatches(List<object> subjects, List<TExpectation> expectations)
public void FindAndRemoveMatches(List<IndexedItem<object>> subjects, List<TExpectation> expectations)
{
int failedCount = 0;

Expand Down Expand Up @@ -44,11 +44,11 @@ public void FindAndRemoveMatches(List<object> subjects, List<TExpectation> expec
expectations.RemoveRange(0, index);
}

private bool StrictlyMatchAgainst<T>(List<object> subjects, T expectation, int expectationIndex)
private bool StrictlyMatchAgainst<T>(List<IndexedItem<object>> subjects, T expectation, int expectationIndex)
{
using var scope = new AssertionScope();

object subject = subjects[expectationIndex];
object subject = subjects[expectationIndex].Item;
IEquivalencyValidationContext equivalencyValidationContext = context.AsCollectionItem<T>(expectationIndex);

parent.AssertEquivalencyOf(new Comparands(subject, expectation, typeof(T)), equivalencyValidationContext);
Expand Down
4 changes: 2 additions & 2 deletions Tests/FluentAssertions.Equivalency.Specs/BasicSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,7 @@ public void Reports_all_relevant_details_to_understand_the_differences()
"""
Expected property actual[0].Name to be "Dennis" with a length of 6, but "Jits" has a length of 4, differs near "Jit" (index 0).
Expected property actual[0].Age to be 52, but found 13.
Expected actual to contain exactly one item, but found one extraneous item FluentAssertions.Equivalency.Specs.Customer
Expected actual to contain exactly one item, but found one extraneous item at index 1: FluentAssertions.Equivalency.Specs.Customer
{
Age = 16,
Birthdate = <0001-01-01 00:00:00.000>,
Expand Down Expand Up @@ -1006,7 +1006,7 @@ public void Reports_all_relevant_details_to_understand_the_differences_including
"""
Expected property actual[0].Name to be "Dennis" with a length of 6, but "Jits" has a length of 4, differs near "Jit" (index 0).
Expected property actual[0].Age to be 52, but found 13.
Expected actual to contain exactly one item, but found one extraneous item FluentAssertions.Equivalency.Specs.Customer
Expected actual to contain exactly one item, but found one extraneous item at index 1: FluentAssertions.Equivalency.Specs.Customer
{
Age = 16,
Birthdate = <0001-01-01 00:00:00.000>,
Expand Down
55 changes: 54 additions & 1 deletion Tests/FluentAssertions.Equivalency.Specs/CollectionSpecs.cs
Loading
Loading