Skip to content

Commit

Permalink
Improve performance of AllBeEquivalentTo (#920)
Browse files Browse the repository at this point in the history
* Force strinct ordering for collection created by AllBeEquivalentTo.
* Improves algorithmic complexity from O(n^2) to O(n)
* Fail fast after 10 items are detected to be incorrect in `EnumerableEquivalencyValidator`
* Add trace message in case of fast fail comparing enumerable equivalency
  • Loading branch information
krajek authored and dennisdoomen committed Sep 26, 2018
1 parent b8fe272 commit 6c72744
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 14 deletions.
14 changes: 12 additions & 2 deletions Src/FluentAssertions/Collections/GenericCollectionAssertions.cs
Expand Up @@ -331,12 +331,22 @@ private bool IsValidProperty<TSelector>(Expression<Func<T, TSelector>> propertyE
/// Zero or more objects to format using the placeholders in <see cref="because" />.
/// </param>
public void AllBeEquivalentTo<TExpectation>(TExpectation expectation,
Func<EquivalencyAssertionOptions<TExpectation>, EquivalencyAssertionOptions<TExpectation>> config, string because = "",
Func<EquivalencyAssertionOptions<TExpectation>, EquivalencyAssertionOptions<TExpectation>> config,
string because = "",
params object[] becauseArgs)
{
TExpectation[] repeatedExpectation = RepeatAsManyAs(expectation, Subject).ToArray();

BeEquivalentTo(repeatedExpectation, config, because, becauseArgs);
// Because we have just manually created the collection based on single element
// we are sure that we can force strict ordering, because ordering does not matter in terms
// of correctness. On the other hand we do not want to change ordering rules for nested objects
// in case user needs to use them. Strict ordering improves algorithmic complexity
// from O(n^2) to O(n). For bigger tables it is necessary in order to achieve acceptable
// execution times.
Func<EquivalencyAssertionOptions<TExpectation>, EquivalencyAssertionOptions<TExpectation>> forceStringOrderingConfig =
x => config(x).WithStrictOrderingFor(s => s.SelectedMemberPath == "");

BeEquivalentTo(repeatedExpectation, forceStringOrderingConfig, because, becauseArgs);
}

private static IEnumerable<TExpectation> RepeatAsManyAs<TExpectation>(TExpectation value, IEnumerable<T> enumerable)
Expand Down
73 changes: 63 additions & 10 deletions Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs
Expand Up @@ -11,6 +11,8 @@ namespace FluentAssertions.Equivalency
/// </summary>
internal class EnumerableEquivalencyValidator
{
private const int FailedItemsFastFailThreshold = 10;

#region Private Definitions

private readonly IEquivalencyValidator parent;
Expand Down Expand Up @@ -62,35 +64,73 @@ private void AssertElementGraphEquivalency<T>(object[] subjects, T[] expectation
unmatchedSubjectIndexes = new List<int>(subjects.Length);
unmatchedSubjectIndexes.AddRange(Enumerable.Range(0, subjects.Length));

if (OrderingRules.IsOrderingStrictFor(context))
{
AssertElementGraphEquivalencyWithStrictOrdering(subjects, expectations);
}
else
{
AssertElementGraphEquivalencyWithLooseOrdering(subjects, expectations);
}
}

private void AssertElementGraphEquivalencyWithStrictOrdering<T>(object[] subjects, T[] expectations)
{
int failedCount = 0;
foreach (int index in Enumerable.Range(0, expectations.Length))
{
T expectation = expectations[index];

if (!OrderingRules.IsOrderingStrictFor(context))
using (context.TraceBlock(path =>
$"Strictly comparing expectation {expectation} at {path} to item with index {index} in {subjects}"))
{
using (context.TraceBlock(path => $"Finding the best match of {expectation} within all items in {subjects} at {path}[{index}]"))
bool succeeded = StrictlyMatchAgainst(subjects, expectation, index);
if (!succeeded)
{
LooselyMatchAgainst(subjects, expectation, index);
failedCount++;
if (failedCount >= FailedItemsFastFailThreshold)
{
context.TraceSingle(path =>
$"Aborting strict order comparison of collections after {FailedItemsFastFailThreshold} items failed at {path}");
break;
}
}
}
else
}
}

private void AssertElementGraphEquivalencyWithLooseOrdering<T>(object[] subjects, T[] expectations)
{
int failedCount = 0;
foreach (int index in Enumerable.Range(0, expectations.Length))
{
T expectation = expectations[index];

using (context.TraceBlock(path =>
$"Finding the best match of {expectation} within all items in {subjects} at {path}[{index}]"))
{
using (context.TraceBlock(path => $"Strictly comparing expectation {expectation} at {path} to item with index {index} in {subjects}"))
bool succeeded = LooselyMatchAgainst(subjects, expectation, index);
if (!succeeded)
{
StrictlyMatchAgainst(subjects, expectation, index);
failedCount++;
if (failedCount >= FailedItemsFastFailThreshold)
{
context.TraceSingle(path =>
$"Fail failing loose order comparison of collection after {FailedItemsFastFailThreshold} items failed at {path}");
break;
}
}
}
}
}

private List<int> unmatchedSubjectIndexes;

private void LooselyMatchAgainst<T>(IList<object> subjects, T expectation, int expectationIndex)
private bool LooselyMatchAgainst<T>(IList<object> subjects, T expectation, int expectationIndex)
{
var results = new AssertionResultSet();
int index = 0;
GetTraceMessage getMessage = path => $"Comparing subject at {path}[{index}] with the expectation at {path}[{expectationIndex}]";
int count = subjects.Count;
int indexToBeRemoved = -1;

for (var metaIndex = 0; metaIndex < unmatchedSubjectIndexes.Count; metaIndex++)
Expand Down Expand Up @@ -125,6 +165,8 @@ private void LooselyMatchAgainst<T>(IList<object> subjects, T expectation, int e
{
AssertionScope.Current.AddPreFormattedFailure(failure);
}

return indexToBeRemoved != -1;
}

private string[] TryToMatch<T>(object subject, T expectation, int expectationIndex)
Expand All @@ -137,9 +179,20 @@ private string[] TryToMatch<T>(object subject, T expectation, int expectationInd
}
}

private void StrictlyMatchAgainst<T>(object[] subjects, T expectation, int expectationIndex)
private bool StrictlyMatchAgainst<T>(object[] subjects, T expectation, int expectationIndex)
{
parent.AssertEqualityUsing(context.CreateForCollectionItem(expectationIndex.ToString(), subjects[expectationIndex], expectation));
using (var scope = new AssertionScope())
{
object subject = subjects[expectationIndex];
string indexString = expectationIndex.ToString();
IEquivalencyValidationContext equivalencyValidationContext =
context.CreateForCollectionItem(indexString, subject, expectation);

parent.AssertEqualityUsing(equivalencyValidationContext);

bool failed = scope.HasFailures();
return !failed;
}
}
}
}
6 changes: 6 additions & 0 deletions Src/FluentAssertions/Execution/AssertionScope.cs
Expand Up @@ -296,6 +296,12 @@ public string[] Discard()
return assertionStrategy.DiscardFailures().ToArray();
}

public bool HasFailures()
{
return assertionStrategy.FailureMessages.Any();
}


/// <summary>
/// Gets data associated with the current scope and identified by <paramref name="key"/>.
/// </summary>
Expand Down
79 changes: 77 additions & 2 deletions Tests/Shared.Specs/CollectionEquivalencySpecs.cs
Expand Up @@ -324,6 +324,29 @@ public void When_a_collection_does_not_match_it_should_include_items_in_message(
.WithMessage("Expected*but*{1, 2}*1 item(s) less than*{3, 2, 1}*");
}

[Fact]
public void When_collection_of_same_count_does_not_match_it_should_include_at_most_10_items_in_message()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange
//-----------------------------------------------------------------------------------------------------------
const int commonLength = 11;
// Subjects contains different values, because we want to distinguish them in the assertion message
var subject = new int[commonLength] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var expectation = Enumerable.Repeat(20, commonLength).ToArray();

//-----------------------------------------------------------------------------------------------------------
// Act
//-----------------------------------------------------------------------------------------------------------
Action action = () => subject.Should().BeEquivalentTo(expectation);

//-----------------------------------------------------------------------------------------------------------
// Assert
//-----------------------------------------------------------------------------------------------------------
action.Should().Throw<XunitException>().Which
.Message.Should().Contain("[9]").And.NotContain("[10]");
}

[Fact]
public void When_a_nullable_collection_does_not_match_it_should_throw()
{
Expand Down Expand Up @@ -889,7 +912,7 @@ public void When_all_subject_items_are_equivalent_to_expectation_object_it_shoul
}

[Fact]
public void When_all_subject_items_are_not_equivalent_to_expectation_object_it_should_throw()
public void When_some_subject_items_are_not_equivalent_to_expectation_object_it_should_throw()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange
Expand All @@ -900,14 +923,66 @@ public void When_all_subject_items_are_not_equivalent_to_expectation_object_it_s
// Act
//-----------------------------------------------------------------------------------------------------------
Action action = () => subject.Should().AllBeEquivalentTo(1);

////-----------------------------------------------------------------------------------------------------------
//// Assert
////-----------------------------------------------------------------------------------------------------------
action.Should().Throw<XunitException>().WithMessage(
"Expected item[1] to be 1, but found 2.*Expected item[2] to be 1, but found 3*");
}

[Fact]
public void When_more_than_10_subjects_items_are_not_equivalent_to_expectation_only_10_are_reported()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange
//-----------------------------------------------------------------------------------------------------------
var subject = Enumerable.Repeat(2, 11);

//-----------------------------------------------------------------------------------------------------------
// Act
//-----------------------------------------------------------------------------------------------------------
Action action = () => subject.Should().AllBeEquivalentTo(1);

////-----------------------------------------------------------------------------------------------------------
//// Assert
////-----------------------------------------------------------------------------------------------------------
action.Should().Throw<XunitException>().Which
.Message.Should().Contain("item[9] to be 1, but found 2")
.And.NotContain("item[10]");
}

[Fact]
public void When_some_subject_items_are_not_equivalent_to_expectation_for_huge_table_execution_time_should_still_be_short()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange
//-----------------------------------------------------------------------------------------------------------
const int N = 100000;
var subject = new List<int>(N) {1};
for (int i = 1; i < N; i++)
{
subject.Add(2);
}

//-----------------------------------------------------------------------------------------------------------
// Act
//-----------------------------------------------------------------------------------------------------------
Action action = () =>
{
try { subject.Should().AllBeEquivalentTo(1); }
catch (Exception)
{
// ignored, we only care about execution time
}
};

////-----------------------------------------------------------------------------------------------------------
//// Assert
////-----------------------------------------------------------------------------------------------------------
action.ExecutionTime().Should().BeLessThan(1.Seconds());
}

[Fact]
public void
When_an_object_implements_multiple_IEnumerable_interfaces_but_the_declared_type_is_assignable_to_only_one_it_should_respect_the_declared_type
Expand Down

0 comments on commit 6c72744

Please sign in to comment.