Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance of AllBeEquivalentTo #920

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 string ordering, because ordering does not matter in terms
krajek marked this conversation as resolved.
Show resolved Hide resolved
// 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 succeed = StrictlyMatchAgainst(subjects, expectation, index);
krajek marked this conversation as resolved.
Show resolved Hide resolved
if (!succeed)
{
LooselyMatchAgainst(subjects, expectation, index);
failedCount++;
if (failedCount >= FailedItemsFastFailThreshold)
{
context.TraceSingle(path =>
$"Fail failing strict order comparison of collection after {FailedItemsFastFailThreshold} items failed at {path}");
krajek marked this conversation as resolved.
Show resolved Hide resolved
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 succeed = LooselyMatchAgainst(subjects, expectation, index);
krajek marked this conversation as resolved.
Show resolved Hide resolved
if (!succeed)
{
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();
var equivalencyValidationContext = context
krajek marked this conversation as resolved.
Show resolved Hide resolved
.CreateForCollectionItem(indexString, subject, expectation);

parent.AssertEqualityUsing(equivalencyValidationContext);

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

public bool AnyFailures()
krajek marked this conversation as resolved.
Show resolved Hide resolved
{
return assertionStrategy.FailureMessages.Any();
}


/// <summary>
/// Gets data associated with the current scope and identified by <paramref name="key"/>.
/// </summary>
Expand Down
78 changes: 76 additions & 2 deletions Tests/Shared.Specs/CollectionEquivalencySpecs.cs
Expand Up @@ -324,6 +324,28 @@ 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;
var subject = new int[commonLength] { 0, 1, 2, 2, 4, 5, 6, 7, 8, 9, 10 };
krajek marked this conversation as resolved.
Show resolved Hide resolved
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 +911,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 +922,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;
krajek marked this conversation as resolved.
Show resolved Hide resolved
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
krajek marked this conversation as resolved.
Show resolved Hide resolved
}
};

////-----------------------------------------------------------------------------------------------------------
krajek marked this conversation as resolved.
Show resolved Hide resolved
//// 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