diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index c2a03c741f..12e9a60e5c 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -331,12 +331,22 @@ private bool IsValidProperty(Expression> propertyE /// Zero or more objects to format using the placeholders in . /// public void AllBeEquivalentTo(TExpectation expectation, - Func, EquivalencyAssertionOptions> config, string because = "", + Func, EquivalencyAssertionOptions> 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> forceStringOrderingConfig = + x => config(x).WithStrictOrderingFor(s => s.SelectedMemberPath == ""); + + BeEquivalentTo(repeatedExpectation, forceStringOrderingConfig, because, becauseArgs); } private static IEnumerable RepeatAsManyAs(TExpectation value, IEnumerable enumerable) diff --git a/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs b/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs index 880b5141b1..f8d5b52ce8 100644 --- a/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs +++ b/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs @@ -11,6 +11,8 @@ namespace FluentAssertions.Equivalency /// internal class EnumerableEquivalencyValidator { + private const int FailedItemsFastFailThreshold = 10; + #region Private Definitions private readonly IEquivalencyValidator parent; @@ -62,22 +64,61 @@ private void AssertElementGraphEquivalency(object[] subjects, T[] expectation unmatchedSubjectIndexes = new List(subjects.Length); unmatchedSubjectIndexes.AddRange(Enumerable.Range(0, subjects.Length)); + if (OrderingRules.IsOrderingStrictFor(context)) + { + AssertElementGraphEquivalencyWithStrictOrdering(subjects, expectations); + } + else + { + AssertElementGraphEquivalencyWithLooseOrdering(subjects, expectations); + } + } + + private void AssertElementGraphEquivalencyWithStrictOrdering(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(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; + } } } } @@ -85,12 +126,11 @@ private void AssertElementGraphEquivalency(object[] subjects, T[] expectation private List unmatchedSubjectIndexes; - private void LooselyMatchAgainst(IList subjects, T expectation, int expectationIndex) + private bool LooselyMatchAgainst(IList 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++) @@ -125,6 +165,8 @@ private void LooselyMatchAgainst(IList subjects, T expectation, int e { AssertionScope.Current.AddPreFormattedFailure(failure); } + + return indexToBeRemoved != -1; } private string[] TryToMatch(object subject, T expectation, int expectationIndex) @@ -137,9 +179,20 @@ private string[] TryToMatch(object subject, T expectation, int expectationInd } } - private void StrictlyMatchAgainst(object[] subjects, T expectation, int expectationIndex) + private bool StrictlyMatchAgainst(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; + } } } } diff --git a/Src/FluentAssertions/Execution/AssertionScope.cs b/Src/FluentAssertions/Execution/AssertionScope.cs index 6c5d11a782..89e4dda631 100644 --- a/Src/FluentAssertions/Execution/AssertionScope.cs +++ b/Src/FluentAssertions/Execution/AssertionScope.cs @@ -284,6 +284,12 @@ public string[] Discard() return assertionStrategy.DiscardFailures().ToArray(); } + public bool HasFailures() + { + return assertionStrategy.FailureMessages.Any(); + } + + /// /// Gets data associated with the current scope and identified by . /// diff --git a/Tests/Shared.Specs/CollectionEquivalencySpecs.cs b/Tests/Shared.Specs/CollectionEquivalencySpecs.cs index e34c9838e0..5ba64abf9a 100644 --- a/Tests/Shared.Specs/CollectionEquivalencySpecs.cs +++ b/Tests/Shared.Specs/CollectionEquivalencySpecs.cs @@ -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().Which + .Message.Should().Contain("[9]").And.NotContain("[10]"); + } + [Fact] public void When_a_nullable_collection_does_not_match_it_should_throw() { @@ -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 @@ -900,7 +923,7 @@ public void When_all_subject_items_are_not_equivalent_to_expectation_object_it_s // Act //----------------------------------------------------------------------------------------------------------- Action action = () => subject.Should().AllBeEquivalentTo(1); - + ////----------------------------------------------------------------------------------------------------------- //// Assert ////----------------------------------------------------------------------------------------------------------- @@ -908,6 +931,58 @@ public void When_all_subject_items_are_not_equivalent_to_expectation_object_it_s "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().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(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