From f4a8c68bbb35634ae6b3d310fe83a9441a5287bc Mon Sep 17 00:00:00 2001 From: Stacy Cashmore <33732891+StacyCash@users.noreply.github.com> Date: Tue, 19 Jul 2022 21:29:47 +0200 Subject: [PATCH 01/18] Add ContainInConsecutiveOrder and NotContainInConsecutiveOrder Two functions to check that a cleections contains or doesn't contain the items from a second collections consecutively. --- .../GenericCollectionAssertions.cs | 187 +++++++++++ .../FluentAssertions/net47.verified.txt | 4 + .../FluentAssertions/net6.0.verified.txt | 4 + .../netcoreapp2.1.verified.txt | 4 + .../netcoreapp3.0.verified.txt | 4 + .../netstandard2.0.verified.txt | 4 + .../netstandard2.1.verified.txt | 4 + ...onAssertionSpecs.ContainInExplicitOrder.cs | 293 ++++++++++++++++++ docs/_pages/collections.md | 3 + docs/_pages/releases.md | 1 + 10 files changed, 508 insertions(+) create mode 100644 Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInExplicitOrder.cs diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index 2fa8ca7c7a..7b096aa076 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -944,6 +944,90 @@ public AndConstraint ContainInOrder(IEnumerable expected, string return new AndConstraint((TAssertions)this); } + /// + /// Expects the current collection to contain the specified elements in the exact same order, and to be consecutive. + /// using their implementation. + /// + /// An with the expected elements. + public AndConstraint ContainInConsecutiveOrder(params T[] expected) + { + return ContainInConsecutiveOrder(expected, string.Empty); + } + + /// + /// Expects the current collection to contain the specified elements in the exact same order, and to be consecutive. + /// + /// + /// Elements are compared using their implementation. + /// + /// An with the expected elements. + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + /// is null. + public AndConstraint ContainInConsecutiveOrder(IEnumerable expected, string because = "", + params object[] becauseArgs) + { + Guard.ThrowIfArgumentIsNull(expected, nameof(expected), "Cannot verify ordered containment against a collection."); + + bool success = Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(Subject is not null) + .FailWith("Expected {context:collection} to contain {0} in order{reason}, but found .", expected); + + if (success) + { + IList expectedItems = expected.ConvertOrCastToList(); + IList actualItems = Subject.ConvertOrCastToList(); + + int highestIndex = -1; + int index = 0; + + Func areSameOrEqual = ObjectExtensions.GetComparer(); + + for (index = 0; index < expectedItems.Count; index++) + { + T expectedItem = expectedItems[index]; + if (index == 0) + { + actualItems = actualItems.SkipWhile(actualItem => !areSameOrEqual(actualItem, expectedItem)).ToArray(); + } + else + { + if (actualItems.Any() && areSameOrEqual(actualItems.First(), expectedItem)) + { + highestIndex = index; + } + else + { + actualItems = actualItems.SkipWhile(actualItem => !areSameOrEqual(actualItem, expectedItems[0])).ToArray(); + index = 0; + } + } + + if (actualItems.Any()) + { + actualItems = actualItems.Skip(1).ToArray(); + } + else + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith( + "Expected {context:collection} {0} to contain items {1} in order{reason}" + + ", but {2} (index {3}) did not appear (in the right order).", + Subject, expected, expectedItem, highestIndex + 1); + } + } + } + + return new AndConstraint((TAssertions)this); + } + /// /// Asserts that the current collection contains at least one element that is assignable to the type . /// @@ -2303,6 +2387,109 @@ public AndConstraint NotContainInOrder(IEnumerable unexpected, s return new AndConstraint((TAssertions)this); } + /// + /// Asserts the current collection does not contain the specified elements in the exact same order and are consecutive. + /// + /// + /// Elements are compared using their implementation. + /// + /// A with the unexpected elements. + /// is null. + public AndConstraint NotContainInConsecutiveOrder(params T[] unexpected) + { + return NotContainInConsecutiveOrder(unexpected, string.Empty); + } + + /// + /// Asserts the current collection does not contain the specified elements in the exact same order and consecutively. + /// + /// + /// Elements are compared using their implementation. + /// + /// An with the unexpected elements. + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + /// is null. + public AndConstraint NotContainInConsecutiveOrder(IEnumerable unexpected, string because = "", + params object[] becauseArgs) + { + Guard.ThrowIfArgumentIsNull(unexpected, nameof(unexpected), "Cannot verify absence of ordered containment against a collection."); + + if (Subject is null) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith("Cannot verify absence of ordered containment in a collection."); + + return new AndConstraint((TAssertions)this); + } + + IList unexpectedItems = unexpected.ConvertOrCastToList(); + IList actualItems = Subject.ConvertOrCastToList(); + + if (unexpectedItems.Count > actualItems.Count) + { + return new AndConstraint((TAssertions)this); + } + + var actualItemsSkipped = 0; + int index; + Func areSameOrEqual = ObjectExtensions.GetComparer(); + + for (index = 0; index < unexpectedItems.Count; index++) + { + T currentUnexpectedItem = unexpectedItems[index]; + if (index == 0) + { + actualItems = actualItems.SkipWhile(actualItem => + { + actualItemsSkipped++; + return !areSameOrEqual(actualItem, currentUnexpectedItem); + }).ToArray(); + } + else + { + if (actualItems.Any() && !areSameOrEqual(actualItems.First(), currentUnexpectedItem)) + { + index = 0; + actualItemsSkipped--; + actualItems = actualItems.SkipWhile(actualItem => + { + actualItemsSkipped++; + return !areSameOrEqual(actualItem, unexpectedItems[index]); + }).ToArray(); + } + } + + if (actualItems.Any()) + { + if (index == unexpectedItems.Count - 1) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith( + "Expected {context:collection} {0} to not contain items {1} in order{reason}, " + + "but items appeared in order ending at index {2}.", + Subject, unexpected, actualItemsSkipped - 1); + } + + actualItems = actualItems.Skip(1).ToArray(); + actualItemsSkipped++; + } + else + { + return new AndConstraint((TAssertions)this); + } + } + + return new AndConstraint((TAssertions)this); + } + /// /// Asserts that the collection does not contain any null items. /// diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt index 8a2d1f8e14..44dcce33ce 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt @@ -450,6 +450,8 @@ namespace FluentAssertions.Collections public FluentAssertions.AndWhichConstraint Contain(T expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint ContainEquivalentOf(TExpectation expectation, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint ContainEquivalentOf(TExpectation expectation, System.Func, FluentAssertions.Equivalency.EquivalencyAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint ContainInConsecutiveOrder(params T[] expected) { } + public FluentAssertions.AndConstraint ContainInConsecutiveOrder(System.Collections.Generic.IEnumerable expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint ContainInOrder(params T[] expected) { } public FluentAssertions.AndConstraint ContainInOrder(System.Collections.Generic.IEnumerable expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint ContainItemsAssignableTo(string because = "", params object[] becauseArgs) { } @@ -494,6 +496,8 @@ namespace FluentAssertions.Collections public FluentAssertions.AndWhichConstraint NotContain(T unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainEquivalentOf(TExpectation unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainEquivalentOf(TExpectation unexpected, System.Func, FluentAssertions.Equivalency.EquivalencyAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotContainInConsecutiveOrder(params T[] unexpected) { } + public FluentAssertions.AndConstraint NotContainInConsecutiveOrder(System.Collections.Generic.IEnumerable unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainInOrder(params T[] unexpected) { } public FluentAssertions.AndConstraint NotContainInOrder(System.Collections.Generic.IEnumerable unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainNulls(string because = "", params object[] becauseArgs) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt index 1b6faed215..fbf2f744c5 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt @@ -462,6 +462,8 @@ namespace FluentAssertions.Collections public FluentAssertions.AndWhichConstraint Contain(T expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint ContainEquivalentOf(TExpectation expectation, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint ContainEquivalentOf(TExpectation expectation, System.Func, FluentAssertions.Equivalency.EquivalencyAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint ContainInConsecutiveOrder(params T[] expected) { } + public FluentAssertions.AndConstraint ContainInConsecutiveOrder(System.Collections.Generic.IEnumerable expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint ContainInOrder(params T[] expected) { } public FluentAssertions.AndConstraint ContainInOrder(System.Collections.Generic.IEnumerable expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint ContainItemsAssignableTo(string because = "", params object[] becauseArgs) { } @@ -506,6 +508,8 @@ namespace FluentAssertions.Collections public FluentAssertions.AndWhichConstraint NotContain(T unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainEquivalentOf(TExpectation unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainEquivalentOf(TExpectation unexpected, System.Func, FluentAssertions.Equivalency.EquivalencyAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotContainInConsecutiveOrder(params T[] unexpected) { } + public FluentAssertions.AndConstraint NotContainInConsecutiveOrder(System.Collections.Generic.IEnumerable unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainInOrder(params T[] unexpected) { } public FluentAssertions.AndConstraint NotContainInOrder(System.Collections.Generic.IEnumerable unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainNulls(string because = "", params object[] becauseArgs) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt index a9f68eb68d..71b8257042 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt @@ -450,6 +450,8 @@ namespace FluentAssertions.Collections public FluentAssertions.AndWhichConstraint Contain(T expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint ContainEquivalentOf(TExpectation expectation, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint ContainEquivalentOf(TExpectation expectation, System.Func, FluentAssertions.Equivalency.EquivalencyAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint ContainInConsecutiveOrder(params T[] expected) { } + public FluentAssertions.AndConstraint ContainInConsecutiveOrder(System.Collections.Generic.IEnumerable expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint ContainInOrder(params T[] expected) { } public FluentAssertions.AndConstraint ContainInOrder(System.Collections.Generic.IEnumerable expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint ContainItemsAssignableTo(string because = "", params object[] becauseArgs) { } @@ -494,6 +496,8 @@ namespace FluentAssertions.Collections public FluentAssertions.AndWhichConstraint NotContain(T unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainEquivalentOf(TExpectation unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainEquivalentOf(TExpectation unexpected, System.Func, FluentAssertions.Equivalency.EquivalencyAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotContainInConsecutiveOrder(params T[] unexpected) { } + public FluentAssertions.AndConstraint NotContainInConsecutiveOrder(System.Collections.Generic.IEnumerable unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainInOrder(params T[] unexpected) { } public FluentAssertions.AndConstraint NotContainInOrder(System.Collections.Generic.IEnumerable unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainNulls(string because = "", params object[] becauseArgs) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt index 8c7f8490bc..8c76d8743f 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt @@ -450,6 +450,8 @@ namespace FluentAssertions.Collections public FluentAssertions.AndWhichConstraint Contain(T expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint ContainEquivalentOf(TExpectation expectation, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint ContainEquivalentOf(TExpectation expectation, System.Func, FluentAssertions.Equivalency.EquivalencyAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint ContainInConsecutiveOrder(params T[] expected) { } + public FluentAssertions.AndConstraint ContainInConsecutiveOrder(System.Collections.Generic.IEnumerable expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint ContainInOrder(params T[] expected) { } public FluentAssertions.AndConstraint ContainInOrder(System.Collections.Generic.IEnumerable expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint ContainItemsAssignableTo(string because = "", params object[] becauseArgs) { } @@ -494,6 +496,8 @@ namespace FluentAssertions.Collections public FluentAssertions.AndWhichConstraint NotContain(T unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainEquivalentOf(TExpectation unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainEquivalentOf(TExpectation unexpected, System.Func, FluentAssertions.Equivalency.EquivalencyAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotContainInConsecutiveOrder(params T[] unexpected) { } + public FluentAssertions.AndConstraint NotContainInConsecutiveOrder(System.Collections.Generic.IEnumerable unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainInOrder(params T[] unexpected) { } public FluentAssertions.AndConstraint NotContainInOrder(System.Collections.Generic.IEnumerable unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainNulls(string because = "", params object[] becauseArgs) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt index 1fc893363c..d628c9549e 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt @@ -443,6 +443,8 @@ namespace FluentAssertions.Collections public FluentAssertions.AndWhichConstraint Contain(T expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint ContainEquivalentOf(TExpectation expectation, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint ContainEquivalentOf(TExpectation expectation, System.Func, FluentAssertions.Equivalency.EquivalencyAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint ContainInConsecutiveOrder(params T[] expected) { } + public FluentAssertions.AndConstraint ContainInConsecutiveOrder(System.Collections.Generic.IEnumerable expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint ContainInOrder(params T[] expected) { } public FluentAssertions.AndConstraint ContainInOrder(System.Collections.Generic.IEnumerable expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint ContainItemsAssignableTo(string because = "", params object[] becauseArgs) { } @@ -487,6 +489,8 @@ namespace FluentAssertions.Collections public FluentAssertions.AndWhichConstraint NotContain(T unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainEquivalentOf(TExpectation unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainEquivalentOf(TExpectation unexpected, System.Func, FluentAssertions.Equivalency.EquivalencyAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotContainInConsecutiveOrder(params T[] unexpected) { } + public FluentAssertions.AndConstraint NotContainInConsecutiveOrder(System.Collections.Generic.IEnumerable unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainInOrder(params T[] unexpected) { } public FluentAssertions.AndConstraint NotContainInOrder(System.Collections.Generic.IEnumerable unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainNulls(string because = "", params object[] becauseArgs) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt index dedd54bad1..5043b87f06 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt @@ -450,6 +450,8 @@ namespace FluentAssertions.Collections public FluentAssertions.AndWhichConstraint Contain(T expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint ContainEquivalentOf(TExpectation expectation, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint ContainEquivalentOf(TExpectation expectation, System.Func, FluentAssertions.Equivalency.EquivalencyAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint ContainInConsecutiveOrder(params T[] expected) { } + public FluentAssertions.AndConstraint ContainInConsecutiveOrder(System.Collections.Generic.IEnumerable expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint ContainInOrder(params T[] expected) { } public FluentAssertions.AndConstraint ContainInOrder(System.Collections.Generic.IEnumerable expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint ContainItemsAssignableTo(string because = "", params object[] becauseArgs) { } @@ -494,6 +496,8 @@ namespace FluentAssertions.Collections public FluentAssertions.AndWhichConstraint NotContain(T unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainEquivalentOf(TExpectation unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainEquivalentOf(TExpectation unexpected, System.Func, FluentAssertions.Equivalency.EquivalencyAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotContainInConsecutiveOrder(params T[] unexpected) { } + public FluentAssertions.AndConstraint NotContainInConsecutiveOrder(System.Collections.Generic.IEnumerable unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainInOrder(params T[] unexpected) { } public FluentAssertions.AndConstraint NotContainInOrder(System.Collections.Generic.IEnumerable unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotContainNulls(string because = "", params object[] becauseArgs) { } diff --git a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInExplicitOrder.cs b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInExplicitOrder.cs new file mode 100644 index 0000000000..f58e7fb048 --- /dev/null +++ b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInExplicitOrder.cs @@ -0,0 +1,293 @@ +using System; +using FluentAssertions.Execution; +using Xunit; +using Xunit.Sdk; + +namespace FluentAssertions.Specs.Collections; + +/// +/// The [Not]ContainInOrder specs. +/// +public partial class CollectionAssertionSpecs +{ + public class ContainInExplicitOrder + { + [Fact] + public void When_the_first_collection_contains_a_duplicate_item_without_affecting_the_explicit_order_it_should_not_throw() + { + // Arrange + var collection = new[] { 1, 2, 3, 2 }; + + // Act / Assert + collection.Should().ContainInConsecutiveOrder(1, 2, 3); + } + + [Fact] + public void When_the_first_collection_contains_a_partial_duplicate_sequence_at_the_start_without_affecting_the_explicit_order_it_should_not_throw() + { + // Arrange + var collection = new[] { 1, 2, 1, 2, 3, 2 }; + + // Act / Assert + collection.Should().ContainInConsecutiveOrder(1, 2, 3); + } + + [Fact] + public void When_two_collections_contain_the_same_duplicate_items_in_the_same_explicit_order_it_should_not_throw() + { + // Arrange + var collection = new[] { 1, 2, 1, 2, 12, 2, 2 }; + + // Act / Assert + collection.Should().ContainInConsecutiveOrder(1, 2, 1, 2, 12, 2, 2); + } + + [Fact] + public void When_collection_contains_null_value_it_should_not_throw() + { + // Arrange + var collection = new object[] { 1, null, 2, "string" }; + + // Act / Assert + collection.Should().ContainInConsecutiveOrder(new object[] { 1, null, 2, "string" }); + } + + [Fact] + public void When_two_collections_contain_the_same_items_but_not_in_the_same_explicit_order_it_should_throw() + { + // Arrange + var collection = new[] { 1, 2, 2, 3 }; + + // Act / Assert + Action act = () => collection.Should().ContainInConsecutiveOrder(1, 2, 3); + + // Assert + act.Should().Throw().WithMessage( + "Expected collection {1, 2, 2, 3} to contain items {1, 2, 3} in order, but 3 (index 2) did not appear (in the right order)."); + } + + [Fact] + public void When_a_collection_does_not_contain_a_range_twice_it_should_throw() + { + // Arrange + var collection = new[] { 1, 2, 1, 2, 3, 12, 2, 2 }; + + // Act + Action act = () => collection.Should().ContainInConsecutiveOrder(1, 2, 1, 1, 2); + + // Assert + act.Should().Throw().WithMessage( + "Expected collection {1, 2, 1, 2, 3, 12, 2, 2} to contain items {1, 2, 1, 1, 2} in order, but 1 (index 3) did not appear (in the right order)."); + } + + [Fact] + public void When_two_collections_contain_the_same_items_but_in_different_order_it_should_throw_with_a_clear_explanation() + { + // Act + Action act = () => new[] { 1, 2, 3 }.Should().ContainInConsecutiveOrder(new[] { 3, 1 }, "because we said so"); + + // Assert + act.Should().Throw().WithMessage( + "Expected collection {1, 2, 3} to contain items {3, 1} in order because we said so, but 1 (index 0) did not appear (in the right order)."); + } + + [Fact] + public void When_a_collection_does_not_contain_an_ordered_item_it_should_throw_with_a_clear_explanation() + { + // Act + Action act = () => new[] { 1, 2, 3 }.Should().ContainInConsecutiveOrder(new[] { 4, 1 }, "we failed"); + + // Assert + act.Should().Throw().WithMessage( + "Expected collection {1, 2, 3} to contain items {4, 1} in order because we failed, " + + "but 4 (index 0) did not appear (in the right order)."); + } + + [Fact] + public void When_passing_in_null_while_checking_for_ordered_containment_it_should_throw_with_a_clear_explanation() + { + // Act + Action act = () => new[] { 1, 2, 3 }.Should().ContainInConsecutiveOrder(null); + + // Assert + act.Should().Throw().WithMessage( + "Cannot verify ordered containment against a collection.*"); + } + + [Fact] + public void When_asserting_collection_contains_some_values_in_order_but_collection_is_null_it_should_throw() + { + // Arrange + int[] collection = null; + + // Act + Action act = () => + { + using var _ = new AssertionScope(); + collection.Should().ContainInConsecutiveOrder(new[] { 4 }, "because we're checking how it reacts to a null subject"); + }; + + // Assert + act.Should().Throw().WithMessage( + "Expected collection to contain {4} in order because we're checking how it reacts to a null subject, but found ."); + } + } + + public class NotContainInExplicitOrder + { + [Fact] + public void When_two_collections_contain_the_same_items_but_in_different_order_it_should_not_throw() + { + // Arrange + var collection = new[] { 1, 2, 3 }; + + // Act / Assert + collection.Should().NotContainInConsecutiveOrder(2, 1); + } + + [Fact] + public void When_a_collection_does_not_contain_an_ordered_item_it_should_not_throw() + { + // Arrange + var collection = new[] { 1, 2, 3 }; + + // Act / Assert + collection.Should().NotContainInConsecutiveOrder(4, 1); + } + + [Fact] + public void When_a_collection_contains_less_items_it_should_not_throw() + { + // Arrange + var collection = new[] { 1, 2 }; + + // Act / Assert + collection.Should().NotContainInConsecutiveOrder(1, 2, 3); + } + + [Fact] + public void When_a_collection_does_not_contain_a_range_twice_it_should_not_throw() + { + // Arrange + var collection = new[] { 1, 2, 1, 2, 3, 12, 2, 2 }; + + // Act / Assert + collection.Should().NotContainInConsecutiveOrder(1, 2, 1, 1, 2); + } + + [Fact] + public void When_two_collections_contain_the_same_items_not_in_the_same_explicit_order_it_should_not_throw() + { + // Arrange + var collection = new[] { 1, 2, 1, 2, 2, 3 }; + + // Act + collection.Should().NotContainInConsecutiveOrder(new[] { 1, 2, 3 }, "that's what we expect"); + } + + [Fact] + public void When_asserting_collection_does_not_contain_some_values_in_order_but_collection_is_null_it_should_throw() + { + // Arrange + int[] collection = null; + + // Act + Action act = () => collection.Should().NotContainInConsecutiveOrder(4); + + // Assert + act.Should().Throw().WithMessage("Cannot verify absence of ordered containment in a collection."); + } + + [Fact] + public void When_collection_is_null_then_not_contain_in_order_should_fail() + { + // Arrange + int[] collection = null; + + // Act + Action act = () => + { + using var _ = new AssertionScope(); + collection.Should().NotContainInConsecutiveOrder(new[] { 1, 2, 3 }, "we want to test the failure {0}", "message"); + }; + + // Assert + act.Should().Throw().WithMessage( + "Cannot verify absence of ordered containment in a collection."); + } + + [Fact] + public void When_collection_and_contains_contain_the_same_items_in_the_same_order_with_null_value_it_should_throw() + { + // Arrange + var collection = new object[] { 1, null, 2, "string" }; + + // Act + Action act = () => collection.Should().NotContainInConsecutiveOrder(1, null, 2, "string"); + + // Assert + act.Should().Throw().WithMessage( + "Expected collection {1, , 2, \"string\"} to not contain items {1, , 2, \"string\"} in order, " + + "but items appeared in order ending at index 3."); + } + + [Fact] + public void When_the_first_collection_contains_a_duplicate_item_without_affecting_the_order_it_should_throw() + { + // Arrange + var collection = new[] { 1, 2, 3, 2 }; + + // Act + Action act = () => collection.Should().NotContainInConsecutiveOrder(1, 2, 3); + + // Assert + act.Should().Throw().WithMessage( + "Expected collection {1, 2, 3, 2} to not contain items {1, 2, 3} in order, " + + "but items appeared in order ending at index 2."); + } + + [Fact] + public void When_the_first_collection_contains_a_duplicate_item_not_at_start_without_affecting_the_order_it_should_throw() + { + // Arrange + var collection = new[] { 1, 2, 1, 2, 3, 4, 5, 1, 2 }; + + // Act + Action act = () => collection.Should().NotContainInConsecutiveOrder(1, 2, 3); + + // Assert + act.Should().Throw().WithMessage( + "Expected collection {1, 2, 1, 2, 3, 4, 5, 1, 2} to not contain items {1, 2, 3} in order, " + + "but items appeared in order ending at index 4."); + } + + [Fact] + public void When_two_collections_contain_the_same_duplicate_items_in_the_same_order_it_should_throw() + { + // Arrange + var collection = new[] { 1, 2, 1, 2, 12, 2, 2 }; + + // Act + Action act = () => collection.Should().NotContainInConsecutiveOrder(1, 2, 1, 2, 12, 2, 2); + + // Assert + act.Should().Throw().WithMessage( + "Expected collection {1, 2, 1, 2, 12, 2, 2} to not contain items {1, 2, 1, 2, 12, 2, 2} in order, " + + "but items appeared in order ending at index 6."); + } + + [Fact] + public void When_passing_in_null_while_checking_for_absence_of_ordered_containment_it_should_throw() + { + // Arrange + var collection = new[] { 1, 2, 3 }; + + // Act + Action act = () => collection.Should().NotContainInConsecutiveOrder(null); + + // Assert + act.Should().Throw().WithMessage( + "Cannot verify absence of ordered containment against a collection.*"); + } + } +} diff --git a/docs/_pages/collections.md b/docs/_pages/collections.md index ed0df7a81a..dd77e36110 100644 --- a/docs/_pages/collections.md +++ b/docs/_pages/collections.md @@ -57,6 +57,9 @@ collection.Should().ContainItemsAssignableTo(); collection.Should().ContainInOrder(new[] { 1, 5, 8 }); collection.Should().NotContainInOrder(new[] { 5, 1, 2 }); +collection.Should().ContainInExplicitOrder(new[] { 2, 5, 8 }); +collection.Should().NotContainInExplicitOrder(new[] { 1, 5, 8}); + collection.Should().NotContain(82); collection.Should().NotContain(new[] { 82, 83 }); collection.Should().NotContainNulls(); diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index a8097f9da2..2bae9e26cc 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -10,6 +10,7 @@ sidebar: ## Unreleased ### What's new +* Add `ContainInExplicitOrder` and `NotContainInExplicitOrder` assertions to check if a collection contains or not items in a specific order and to be consecutive. - [#](https://github.com/fluentassertions/fluentassertions/pull/) ### Fixes * Fix `For`/`Exclude` not excluding properties in objects in a collection - [#1953](https://github.com/fluentassertions/fluentassertions/pull/1953) From 4fad081716d3fc729ec9ac3d37b16b423e905595 Mon Sep 17 00:00:00 2001 From: Stacy Cashmore <33732891+StacyCash@users.noreply.github.com> Date: Tue, 26 Jul 2022 11:41:04 +0200 Subject: [PATCH 02/18] Optimise searching for expected list --- .../Collections/GenericCollectionAssertions.cs | 10 +++++++++- ...lectionAssertionSpecs.ContainInConsecutiveOrder.cs} | 0 2 files changed, 9 insertions(+), 1 deletion(-) rename Tests/FluentAssertions.Specs/Collections/{CollectionAssertionSpecs.ContainInExplicitOrder.cs => CollectionAssertionSpecs.ContainInConsecutiveOrder.cs} (100%) diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index 7b096aa076..6854e11255 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -988,13 +988,21 @@ public AndConstraint ContainInConsecutiveOrder(IEnumerable expec int index = 0; Func areSameOrEqual = ObjectExtensions.GetComparer(); + int expectedCount = expectedItems.Count; - for (index = 0; index < expectedItems.Count; index++) + for (index = 0; index < expectedCount; index++) { T expectedItem = expectedItems[index]; if (index == 0) { actualItems = actualItems.SkipWhile(actualItem => !areSameOrEqual(actualItem, expectedItem)).ToArray(); + if (actualItems.Count >= expectedCount - 1) + { + if (actualItems.Take(expectedCount - 1).Equals(expectedItems)) + { + return new AndConstraint((TAssertions)this); + } + } } else { diff --git a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInExplicitOrder.cs b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs similarity index 100% rename from Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInExplicitOrder.cs rename to Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs From 46a4135f0ddf73fb033278f002a50aef115fba07 Mon Sep 17 00:00:00 2001 From: Stacy Cashmore <33732891+StacyCash@users.noreply.github.com> Date: Tue, 26 Jul 2022 14:35:37 +0200 Subject: [PATCH 03/18] Make NotContainsInConsecutiveOrder more efficient --- .../GenericCollectionAssertions.cs | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index 6854e11255..ba99328932 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -985,20 +985,20 @@ public AndConstraint ContainInConsecutiveOrder(IEnumerable expec IList actualItems = Subject.ConvertOrCastToList(); int highestIndex = -1; - int index = 0; + int index; Func areSameOrEqual = ObjectExtensions.GetComparer(); - int expectedCount = expectedItems.Count; + int expectedItemsCount = expectedItems.Count; - for (index = 0; index < expectedCount; index++) + for (index = 0; index < expectedItemsCount; index++) { T expectedItem = expectedItems[index]; if (index == 0) { actualItems = actualItems.SkipWhile(actualItem => !areSameOrEqual(actualItem, expectedItem)).ToArray(); - if (actualItems.Count >= expectedCount - 1) + if (actualItems.Count >= expectedItemsCount - 1) { - if (actualItems.Take(expectedCount - 1).Equals(expectedItems)) + if (actualItems.Take(expectedItemsCount - 1).Equals(expectedItems)) { return new AndConstraint((TAssertions)this); } @@ -2440,16 +2440,18 @@ public AndConstraint NotContainInConsecutiveOrder(IEnumerable un IList unexpectedItems = unexpected.ConvertOrCastToList(); IList actualItems = Subject.ConvertOrCastToList(); - if (unexpectedItems.Count > actualItems.Count) + var unexpectedItemsCount = unexpectedItems.Count; + if (unexpectedItemsCount > actualItems.Count) { return new AndConstraint((TAssertions)this); } var actualItemsSkipped = 0; int index; + Func areSameOrEqual = ObjectExtensions.GetComparer(); - for (index = 0; index < unexpectedItems.Count; index++) + for (index = 0; index < unexpectedItemsCount; index++) { T currentUnexpectedItem = unexpectedItems[index]; if (index == 0) @@ -2459,6 +2461,11 @@ public AndConstraint NotContainInConsecutiveOrder(IEnumerable un actualItemsSkipped++; return !areSameOrEqual(actualItem, currentUnexpectedItem); }).ToArray(); + + if (actualItems.Count <= unexpectedItemsCount - 1) + { + return new AndConstraint((TAssertions)this); + } } else { @@ -2476,7 +2483,7 @@ public AndConstraint NotContainInConsecutiveOrder(IEnumerable un if (actualItems.Any()) { - if (index == unexpectedItems.Count - 1) + if (index == unexpectedItemsCount - 1) { Execute.Assertion .BecauseOf(because, becauseArgs) From 356f7c771d99bcaa759ed041d20562bab7a3a624 Mon Sep 17 00:00:00 2001 From: Stacy Cashmore <33732891+StacyCash@users.noreply.github.com> Date: Tue, 26 Jul 2022 15:26:32 +0200 Subject: [PATCH 04/18] Update docs/_pages/releases.md Co-authored-by: Jonas Nyrup --- docs/_pages/releases.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index 2bae9e26cc..05d850808c 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -10,7 +10,7 @@ sidebar: ## Unreleased ### What's new -* Add `ContainInExplicitOrder` and `NotContainInExplicitOrder` assertions to check if a collection contains or not items in a specific order and to be consecutive. - [#](https://github.com/fluentassertions/fluentassertions/pull/) +* Added `ContainInConsecutiveOrder` and `NotContainInConsecutiveOrder` assertions to check if a collection contains items in a specific order and to be consecutive - [#1963](https://github.com/fluentassertions/fluentassertions/pull/1963) ### Fixes * Fix `For`/`Exclude` not excluding properties in objects in a collection - [#1953](https://github.com/fluentassertions/fluentassertions/pull/1953) From fafd108927160c85c2fa7d63f6c6e09f2c3f2e61 Mon Sep 17 00:00:00 2001 From: Stacy Cashmore <33732891+StacyCash@users.noreply.github.com> Date: Tue, 26 Jul 2022 15:34:47 +0200 Subject: [PATCH 05/18] Rename Explicit to Consecutive --- docs/_pages/collections.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_pages/collections.md b/docs/_pages/collections.md index dd77e36110..e977ae428f 100644 --- a/docs/_pages/collections.md +++ b/docs/_pages/collections.md @@ -57,8 +57,8 @@ collection.Should().ContainItemsAssignableTo(); collection.Should().ContainInOrder(new[] { 1, 5, 8 }); collection.Should().NotContainInOrder(new[] { 5, 1, 2 }); -collection.Should().ContainInExplicitOrder(new[] { 2, 5, 8 }); -collection.Should().NotContainInExplicitOrder(new[] { 1, 5, 8}); +collection.Should().ContainInConsecutiveOrder(new[] { 2, 5, 8 }); +collection.Should().NotContainInConsecutiveOrder(new[] { 1, 5, 8}); collection.Should().NotContain(82); collection.Should().NotContain(new[] { 82, 83 }); From f3e97c11bb056b4b58d31667d75aaacbea773f03 Mon Sep 17 00:00:00 2001 From: Stacy Cashmore <33732891+StacyCash@users.noreply.github.com> Date: Tue, 26 Jul 2022 15:55:34 +0200 Subject: [PATCH 06/18] Format code based on PR comments --- .../Collections/GenericCollectionAssertions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index ba99328932..8f10a1d376 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -1024,11 +1024,11 @@ public AndConstraint ContainInConsecutiveOrder(IEnumerable expec else { Execute.Assertion - .BecauseOf(because, becauseArgs) - .FailWith( - "Expected {context:collection} {0} to contain items {1} in order{reason}" + - ", but {2} (index {3}) did not appear (in the right order).", - Subject, expected, expectedItem, highestIndex + 1); + .BecauseOf(because, becauseArgs) + .FailWith( + "Expected {context:collection} {0} to contain items {1} in order{reason}" + + ", but {2} (index {3}) did not appear (in the right order).", + Subject, expected, expectedItem, highestIndex + 1); } } } From 9587001e947c0ad076270ea8278780b513ad96dc Mon Sep 17 00:00:00 2001 From: Stacy Cashmore <33732891+StacyCash@users.noreply.github.com> Date: Tue, 26 Jul 2022 15:56:58 +0200 Subject: [PATCH 07/18] Rename Explicit to Consecuritve for the test classes --- .../CollectionAssertionSpecs.ContainInConsecutiveOrder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs index f58e7fb048..1b6eeb01a6 100644 --- a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs +++ b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs @@ -10,7 +10,7 @@ namespace FluentAssertions.Specs.Collections; /// public partial class CollectionAssertionSpecs { - public class ContainInExplicitOrder + public class ContainInConsecutiveOrder { [Fact] public void When_the_first_collection_contains_a_duplicate_item_without_affecting_the_explicit_order_it_should_not_throw() @@ -133,7 +133,7 @@ public void When_asserting_collection_contains_some_values_in_order_but_collecti } } - public class NotContainInExplicitOrder + public class NotContainInConsecutiveOrder { [Fact] public void When_two_collections_contain_the_same_items_but_in_different_order_it_should_not_throw() From 54790a0bf680969c409ebdda69b6ea4821165e9f Mon Sep 17 00:00:00 2001 From: Stacy Cashmore <33732891+StacyCash@users.noreply.github.com> Date: Thu, 28 Jul 2022 14:56:34 +0200 Subject: [PATCH 08/18] Refactor/rewrite ContainInConsecutiveOrder --- .../GenericCollectionAssertions.cs | 71 ++++++++++--------- ...ssertionSpecs.ContainInConsecutiveOrder.cs | 16 ++++- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index 8f10a1d376..6d161d9afb 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -984,58 +984,59 @@ public AndConstraint ContainInConsecutiveOrder(IEnumerable expec IList expectedItems = expected.ConvertOrCastToList(); IList actualItems = Subject.ConvertOrCastToList(); - int highestIndex = -1; - int index; - Func areSameOrEqual = ObjectExtensions.GetComparer(); int expectedItemsCount = expectedItems.Count; + T firstExpectedItem = expectedItems[0]; + int testHighestMatchingIndex = 0; - for (index = 0; index < expectedItemsCount; index++) + while (actualItems.Any()) { - T expectedItem = expectedItems[index]; - if (index == 0) - { - actualItems = actualItems.SkipWhile(actualItem => !areSameOrEqual(actualItem, expectedItem)).ToArray(); - if (actualItems.Count >= expectedItemsCount - 1) - { - if (actualItems.Take(expectedItemsCount - 1).Equals(expectedItems)) - { - return new AndConstraint((TAssertions)this); - } - } - } - else + actualItems = actualItems.SkipWhile(actualItem => !areSameOrEqual(actualItem, firstExpectedItem)).ToArray(); + int foundItemsCount = Math.Min(actualItems.Count, expectedItemsCount); + int currentHighestMatchingIndex = FindHighestMatchingIndex(actualItems.Take(foundItemsCount).ToList(), expectedItems.Take(foundItemsCount).ToList()); + + if (currentHighestMatchingIndex == expectedItemsCount) { - if (actualItems.Any() && areSameOrEqual(actualItems.First(), expectedItem)) - { - highestIndex = index; - } - else - { - actualItems = actualItems.SkipWhile(actualItem => !areSameOrEqual(actualItem, expectedItems[0])).ToArray(); - index = 0; - } + return new AndConstraint((TAssertions)this); } + testHighestMatchingIndex = Math.Max(testHighestMatchingIndex, currentHighestMatchingIndex); + if (actualItems.Any()) { actualItems = actualItems.Skip(1).ToArray(); } - else - { - Execute.Assertion - .BecauseOf(because, becauseArgs) - .FailWith( - "Expected {context:collection} {0} to contain items {1} in order{reason}" + - ", but {2} (index {3}) did not appear (in the right order).", - Subject, expected, expectedItem, highestIndex + 1); - } } + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith( + "Expected {context:collection} {0} to contain items {1} in order{reason}" + + ", but {2} (index {3}) did not appear (in the right order).", + Subject, expected, expectedItems[testHighestMatchingIndex], testHighestMatchingIndex); } return new AndConstraint((TAssertions)this); } + private static int FindHighestMatchingIndex(IReadOnlyList list, IReadOnlyList subList) + { + Func areSameOrEqual = ObjectExtensions.GetComparer(); + int index = 0; + + foreach (T item in list) + { + if (!areSameOrEqual(item, subList[index])) + { + return index; + } + + index++; + } + + return index; + } + /// /// Asserts that the current collection contains at least one element that is assignable to the type . /// diff --git a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs index 1b6eeb01a6..7b7dc23fc0 100644 --- a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs +++ b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs @@ -66,6 +66,20 @@ public void When_two_collections_contain_the_same_items_but_not_in_the_same_expl "Expected collection {1, 2, 2, 3} to contain items {1, 2, 3} in order, but 3 (index 2) did not appear (in the right order)."); } + [Fact] + public void When_end_of_first_collection_is_a_partial_match_of_second_at_end_it_should_throw() + { + // Arrange + var collection = new[] { 1, 3, 1, 2 }; + + // Act / Assert + Action act = () => collection.Should().ContainInConsecutiveOrder(1, 2, 3); + + // Assert + act.Should().Throw().WithMessage( + "Expected collection {1, 3, 1, 2} to contain items {1, 2, 3} in order, but 3 (index 2) did not appear (in the right order)."); + } + [Fact] public void When_a_collection_does_not_contain_a_range_twice_it_should_throw() { @@ -88,7 +102,7 @@ public void When_two_collections_contain_the_same_items_but_in_different_order_i // Assert act.Should().Throw().WithMessage( - "Expected collection {1, 2, 3} to contain items {3, 1} in order because we said so, but 1 (index 0) did not appear (in the right order)."); + "Expected collection {1, 2, 3} to contain items {3, 1} in order because we said so, but 1 (index 1) did not appear (in the right order)."); } [Fact] From 23e8059aae8dc51793e37bcd9d97168bac21d07d Mon Sep 17 00:00:00 2001 From: Stacy Cashmore <33732891+StacyCash@users.noreply.github.com> Date: Thu, 28 Jul 2022 16:00:54 +0200 Subject: [PATCH 09/18] Rewrote based on @jnyrup improvements --- .../GenericCollectionAssertions.cs | 40 +++++++++---------- ...ssertionSpecs.ContainInConsecutiveOrder.cs | 10 ++--- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index 6b1d0730b7..fdfa4cced4 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -983,36 +983,34 @@ public AndConstraint ContainInConsecutiveOrder(IEnumerable expec IList expectedItems = expected.ConvertOrCastToList(); IList actualItems = Subject.ConvertOrCastToList(); - Func areSameOrEqual = ObjectExtensions.GetComparer(); - int expectedItemsCount = expectedItems.Count; - T firstExpectedItem = expectedItems[0]; - int testHighestMatchingIndex = 0; + int subjectIndex = 0; - while (actualItems.Any()) + Func areSameOrEqual = ObjectExtensions.GetComparer(); + int index; + int highestIndex = 0; + for (index = 0; index < expectedItems.Count; index++) { - actualItems = actualItems.SkipWhile(actualItem => !areSameOrEqual(actualItem, firstExpectedItem)).ToArray(); - int foundItemsCount = Math.Min(actualItems.Count, expectedItemsCount); - int currentHighestMatchingIndex = FindHighestMatchingIndex(actualItems.Take(foundItemsCount).ToList(), expectedItems.Take(foundItemsCount).ToList()); + T expectedItem = expectedItems[index]; + var previousSubjectIndex = subjectIndex; + subjectIndex = IndexOf(actualItems, expectedItem, subjectIndex, areSameOrEqual); + highestIndex = Math.Max(index, highestIndex); - if (currentHighestMatchingIndex == expectedItemsCount) + if (subjectIndex == -1) { - return new AndConstraint((TAssertions)this); + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith( + "Expected {context:collection} {0} to contain items {1} in order{reason}" + + ", but {2} (index {3}) did not appear (in the right consecutive order).", + Subject, expected, expectedItems[highestIndex], highestIndex); } - testHighestMatchingIndex = Math.Max(testHighestMatchingIndex, currentHighestMatchingIndex); - - if (actualItems.Any()) + if (index > 0 && subjectIndex - previousSubjectIndex > 1) { - actualItems = actualItems.Skip(1).ToArray(); + index = -1; + subjectIndex = previousSubjectIndex; } } - - Execute.Assertion - .BecauseOf(because, becauseArgs) - .FailWith( - "Expected {context:collection} {0} to contain items {1} in order{reason}" + - ", but {2} (index {3}) did not appear (in the right order).", - Subject, expected, expectedItems[testHighestMatchingIndex], testHighestMatchingIndex); } return new AndConstraint((TAssertions)this); diff --git a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs index 7b7dc23fc0..6e709392bb 100644 --- a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs +++ b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs @@ -63,7 +63,7 @@ public void When_two_collections_contain_the_same_items_but_not_in_the_same_expl // Assert act.Should().Throw().WithMessage( - "Expected collection {1, 2, 2, 3} to contain items {1, 2, 3} in order, but 3 (index 2) did not appear (in the right order)."); + "Expected collection {1, 2, 2, 3} to contain items {1, 2, 3} in order, but 3 (index 2) did not appear (in the right consecutive order)."); } [Fact] @@ -77,7 +77,7 @@ public void When_end_of_first_collection_is_a_partial_match_of_second_at_end_it_ // Assert act.Should().Throw().WithMessage( - "Expected collection {1, 3, 1, 2} to contain items {1, 2, 3} in order, but 3 (index 2) did not appear (in the right order)."); + "Expected collection {1, 3, 1, 2} to contain items {1, 2, 3} in order, but 3 (index 2) did not appear (in the right consecutive order)."); } [Fact] @@ -91,7 +91,7 @@ public void When_a_collection_does_not_contain_a_range_twice_it_should_throw() // Assert act.Should().Throw().WithMessage( - "Expected collection {1, 2, 1, 2, 3, 12, 2, 2} to contain items {1, 2, 1, 1, 2} in order, but 1 (index 3) did not appear (in the right order)."); + "Expected collection {1, 2, 1, 2, 3, 12, 2, 2} to contain items {1, 2, 1, 1, 2} in order, but 1 (index 3) did not appear (in the right consecutive order)."); } [Fact] @@ -102,7 +102,7 @@ public void When_two_collections_contain_the_same_items_but_in_different_order_i // Assert act.Should().Throw().WithMessage( - "Expected collection {1, 2, 3} to contain items {3, 1} in order because we said so, but 1 (index 1) did not appear (in the right order)."); + "Expected collection {1, 2, 3} to contain items {3, 1} in order because we said so, but 1 (index 1) did not appear (in the right consecutive order)."); } [Fact] @@ -114,7 +114,7 @@ public void When_a_collection_does_not_contain_an_ordered_item_it_should_throw_w // Assert act.Should().Throw().WithMessage( "Expected collection {1, 2, 3} to contain items {4, 1} in order because we failed, " + - "but 4 (index 0) did not appear (in the right order)."); + "but 4 (index 0) did not appear (in the right consecutive order)."); } [Fact] From 2a940fe6a67ba6685c8eb6306177c13a94a803ff Mon Sep 17 00:00:00 2001 From: Stacy Cashmore <33732891+StacyCash@users.noreply.github.com> Date: Thu, 28 Jul 2022 16:40:50 +0200 Subject: [PATCH 10/18] Rewrite NotContainsInConsecuritveOrder --- .../GenericCollectionAssertions.cs | 95 ++++++------------- ...ssertionSpecs.ContainInConsecutiveOrder.cs | 18 +++- 2 files changed, 42 insertions(+), 71 deletions(-) diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index fdfa4cced4..1050d3c15b 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -1016,24 +1016,6 @@ public AndConstraint ContainInConsecutiveOrder(IEnumerable expec return new AndConstraint((TAssertions)this); } - private static int FindHighestMatchingIndex(IReadOnlyList list, IReadOnlyList subList) - { - Func areSameOrEqual = ObjectExtensions.GetComparer(); - int index = 0; - - foreach (T item in list) - { - if (!areSameOrEqual(item, subList[index])) - { - return index; - } - - index++; - } - - return index; - } - /// /// Asserts that the current collection contains at least one element that is assignable to the type . /// @@ -2421,68 +2403,47 @@ public AndConstraint NotContainInConsecutiveOrder(IEnumerable un } IList unexpectedItems = unexpected.ConvertOrCastToList(); - IList actualItems = Subject.ConvertOrCastToList(); - - var unexpectedItemsCount = unexpectedItems.Count; - if (unexpectedItemsCount > actualItems.Count) + if (unexpectedItems.Any()) { - return new AndConstraint((TAssertions)this); - } + IList actualItems = Subject.ConvertOrCastToList(); - var actualItemsSkipped = 0; - int index; + if (unexpectedItems.Count > actualItems.Count) + { + return new AndConstraint((TAssertions)this); + } - Func areSameOrEqual = ObjectExtensions.GetComparer(); + int subjectIndex = 0; - for (index = 0; index < unexpectedItemsCount; index++) - { - T currentUnexpectedItem = unexpectedItems[index]; - if (index == 0) + Func areSameOrEqual = ObjectExtensions.GetComparer(); + int index; + int highestIndex = 0; + + for (index = 0; index < unexpectedItems.Count; index++) { - actualItems = actualItems.SkipWhile(actualItem => - { - actualItemsSkipped++; - return !areSameOrEqual(actualItem, currentUnexpectedItem); - }).ToArray(); + T unexpectedItem = unexpectedItems[index]; - if (actualItems.Count <= unexpectedItemsCount - 1) + var previousSubjectIndex = subjectIndex; + subjectIndex = IndexOf(actualItems, unexpectedItem, subjectIndex, areSameOrEqual); + highestIndex = Math.Max(index, highestIndex); + + if (subjectIndex == -1) { return new AndConstraint((TAssertions)this); } - } - else - { - if (actualItems.Any() && !areSameOrEqual(actualItems.First(), currentUnexpectedItem)) - { - index = 0; - actualItemsSkipped--; - actualItems = actualItems.SkipWhile(actualItem => - { - actualItemsSkipped++; - return !areSameOrEqual(actualItem, unexpectedItems[index]); - }).ToArray(); - } - } - if (actualItems.Any()) - { - if (index == unexpectedItemsCount - 1) + if (index > 0 && subjectIndex - previousSubjectIndex > 1) { - Execute.Assertion - .BecauseOf(because, becauseArgs) - .FailWith( - "Expected {context:collection} {0} to not contain items {1} in order{reason}, " + - "but items appeared in order ending at index {2}.", - Subject, unexpected, actualItemsSkipped - 1); + index = -1; + subjectIndex = previousSubjectIndex; } - - actualItems = actualItems.Skip(1).ToArray(); - actualItemsSkipped++; - } - else - { - return new AndConstraint((TAssertions)this); } + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith( + "Expected {context:collection} {0} to not contain items {1} in consecutive order{reason}, " + + "but items appeared in order ending at index {2}.", + Subject, unexpectedItems, subjectIndex - 1); } return new AndConstraint((TAssertions)this); diff --git a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs index 6e709392bb..98381b05cf 100644 --- a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs +++ b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs @@ -169,6 +169,16 @@ public void When_a_collection_does_not_contain_an_ordered_item_it_should_not_thr collection.Should().NotContainInConsecutiveOrder(4, 1); } + [Fact] + public void When_checking_for_an_empty_list_it_should_not_throw() + { + // Arrange + var collection = new[] { 1, 2, 3 }; + + // Act / Assert + collection.Should().NotContainInConsecutiveOrder(); + } + [Fact] public void When_a_collection_contains_less_items_it_should_not_throw() { @@ -241,7 +251,7 @@ public void When_collection_and_contains_contain_the_same_items_in_the_same_orde // Assert act.Should().Throw().WithMessage( - "Expected collection {1, , 2, \"string\"} to not contain items {1, , 2, \"string\"} in order, " + + "Expected collection {1, , 2, \"string\"} to not contain items {1, , 2, \"string\"} in consecutive order, " + "but items appeared in order ending at index 3."); } @@ -256,7 +266,7 @@ public void When_the_first_collection_contains_a_duplicate_item_without_affectin // Assert act.Should().Throw().WithMessage( - "Expected collection {1, 2, 3, 2} to not contain items {1, 2, 3} in order, " + + "Expected collection {1, 2, 3, 2} to not contain items {1, 2, 3} in consecutive order, " + "but items appeared in order ending at index 2."); } @@ -271,7 +281,7 @@ public void When_the_first_collection_contains_a_duplicate_item_not_at_start_wit // Assert act.Should().Throw().WithMessage( - "Expected collection {1, 2, 1, 2, 3, 4, 5, 1, 2} to not contain items {1, 2, 3} in order, " + + "Expected collection {1, 2, 1, 2, 3, 4, 5, 1, 2} to not contain items {1, 2, 3} in consecutive order, " + "but items appeared in order ending at index 4."); } @@ -286,7 +296,7 @@ public void When_two_collections_contain_the_same_duplicate_items_in_the_same_or // Assert act.Should().Throw().WithMessage( - "Expected collection {1, 2, 1, 2, 12, 2, 2} to not contain items {1, 2, 1, 2, 12, 2, 2} in order, " + + "Expected collection {1, 2, 1, 2, 12, 2, 2} to not contain items {1, 2, 1, 2, 12, 2, 2} in consecutive order, " + "but items appeared in order ending at index 6."); } From a5be6cb3d791c022e18b74dd13b4aa136dafac23 Mon Sep 17 00:00:00 2001 From: Stacy Cashmore <33732891+StacyCash@users.noreply.github.com> Date: Thu, 28 Jul 2022 21:26:46 +0200 Subject: [PATCH 11/18] Add test for ContainsInConsecutiveOrder when expected is empty --- ...llectionAssertionSpecs.ContainInConsecutiveOrder.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs index 98381b05cf..89ebc9c5da 100644 --- a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs +++ b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs @@ -42,6 +42,16 @@ public void When_two_collections_contain_the_same_duplicate_items_in_the_same_ex collection.Should().ContainInConsecutiveOrder(1, 2, 1, 2, 12, 2, 2); } + [Fact] + public void When_checking_for_an_empty_list_it_should_not_throw() + { + // Arrange + var collection = new[] { 1, 2, 1, 2, 12, 2, 2 }; + + // Act / Assert + collection.Should().ContainInConsecutiveOrder(); + } + [Fact] public void When_collection_contains_null_value_it_should_not_throw() { From 9313fe70d93320f3fa7f18cd7d08a89f1b9f9360 Mon Sep 17 00:00:00 2001 From: Stacy Cashmore <33732891+StacyCash@users.noreply.github.com> Date: Mon, 1 Aug 2022 11:54:41 +0200 Subject: [PATCH 12/18] Update Src/FluentAssertions/Collections/GenericCollectionAssertions.cs Co-authored-by: Jonas Nyrup --- Src/FluentAssertions/Collections/GenericCollectionAssertions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index 1050d3c15b..69ab462809 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -991,7 +991,7 @@ public AndConstraint ContainInConsecutiveOrder(IEnumerable expec for (index = 0; index < expectedItems.Count; index++) { T expectedItem = expectedItems[index]; - var previousSubjectIndex = subjectIndex; + int previousSubjectIndex = subjectIndex; subjectIndex = IndexOf(actualItems, expectedItem, subjectIndex, areSameOrEqual); highestIndex = Math.Max(index, highestIndex); From b560ef6c1f81635af15b875377c32e8c91006081 Mon Sep 17 00:00:00 2001 From: Stacy Cashmore <33732891+StacyCash@users.noreply.github.com> Date: Mon, 1 Aug 2022 11:54:48 +0200 Subject: [PATCH 13/18] Update Src/FluentAssertions/Collections/GenericCollectionAssertions.cs Co-authored-by: Jonas Nyrup --- Src/FluentAssertions/Collections/GenericCollectionAssertions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index 69ab462809..be20cc68c3 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -2422,7 +2422,7 @@ public AndConstraint NotContainInConsecutiveOrder(IEnumerable un { T unexpectedItem = unexpectedItems[index]; - var previousSubjectIndex = subjectIndex; + int previousSubjectIndex = subjectIndex; subjectIndex = IndexOf(actualItems, unexpectedItem, subjectIndex, areSameOrEqual); highestIndex = Math.Max(index, highestIndex); From e58610e17cf07fb166bb536b4eca98cdeb76cd98 Mon Sep 17 00:00:00 2001 From: Stacy Cashmore <33732891+StacyCash@users.noreply.github.com> Date: Mon, 1 Aug 2022 11:55:07 +0200 Subject: [PATCH 14/18] Update Src/FluentAssertions/Collections/GenericCollectionAssertions.cs Co-authored-by: Jonas Nyrup --- Src/FluentAssertions/Collections/GenericCollectionAssertions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index be20cc68c3..de9a3b127d 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -1005,7 +1005,7 @@ public AndConstraint ContainInConsecutiveOrder(IEnumerable expec Subject, expected, expectedItems[highestIndex], highestIndex); } - if (index > 0 && subjectIndex - previousSubjectIndex > 1) + if (index > 0 && subjectIndex != previousSubjectIndex + 1) { index = -1; subjectIndex = previousSubjectIndex; From 23c02faba6d27e12001fc4b6947c26dc5c19c70c Mon Sep 17 00:00:00 2001 From: Stacy Cashmore <33732891+StacyCash@users.noreply.github.com> Date: Fri, 5 Aug 2022 17:18:20 +0200 Subject: [PATCH 15/18] Cleanup and add SubjectIndexIsConsecutive helper function --- .../Collections/GenericCollectionAssertions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index de9a3b127d..2b277bda2c 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -1005,7 +1005,7 @@ public AndConstraint ContainInConsecutiveOrder(IEnumerable expec Subject, expected, expectedItems[highestIndex], highestIndex); } - if (index > 0 && subjectIndex != previousSubjectIndex + 1) + if (index > 0 && SubjectIndexIsConsecutive(subjectIndex, previousSubjectIndex)) { index = -1; subjectIndex = previousSubjectIndex; @@ -2416,7 +2416,6 @@ public AndConstraint NotContainInConsecutiveOrder(IEnumerable un Func areSameOrEqual = ObjectExtensions.GetComparer(); int index; - int highestIndex = 0; for (index = 0; index < unexpectedItems.Count; index++) { @@ -2424,14 +2423,13 @@ public AndConstraint NotContainInConsecutiveOrder(IEnumerable un int previousSubjectIndex = subjectIndex; subjectIndex = IndexOf(actualItems, unexpectedItem, subjectIndex, areSameOrEqual); - highestIndex = Math.Max(index, highestIndex); if (subjectIndex == -1) { return new AndConstraint((TAssertions)this); } - if (index > 0 && subjectIndex - previousSubjectIndex > 1) + if (index > 0 && SubjectIndexIsConsecutive(subjectIndex, previousSubjectIndex)) { index = -1; subjectIndex = previousSubjectIndex; @@ -3470,4 +3468,6 @@ private static int IndexOf(IList items, T item, int index, Func c return -1; } + + private static bool SubjectIndexIsConsecutive(int subjectIndex, int previousSubjectIndex) => subjectIndex != previousSubjectIndex + 1; } From 89c910084c89eedc1200b053d0fb3f90b6d249b9 Mon Sep 17 00:00:00 2001 From: Stacy Cashmore <33732891+StacyCash@users.noreply.github.com> Date: Sun, 7 Aug 2022 11:20:09 +0200 Subject: [PATCH 16/18] Changes based on PR comments --- .../GenericCollectionAssertions.cs | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index 2b277bda2c..3246d98688 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -921,12 +921,11 @@ public AndConstraint ContainInOrder(IEnumerable expected, string IList actualItems = Subject.ConvertOrCastToList(); int subjectIndex = 0; - - Func areSameOrEqual = ObjectExtensions.GetComparer(); + for (int index = 0; index < expectedItems.Count; index++) { T expectedItem = expectedItems[index]; - subjectIndex = IndexOf(actualItems, expectedItem, subjectIndex, areSameOrEqual); + subjectIndex = IndexOf(actualItems, expectedItem, startIndex: subjectIndex); if (subjectIndex == -1) { @@ -984,15 +983,14 @@ public AndConstraint ContainInConsecutiveOrder(IEnumerable expec IList actualItems = Subject.ConvertOrCastToList(); int subjectIndex = 0; + int highestIndex = 0; - Func areSameOrEqual = ObjectExtensions.GetComparer(); int index; - int highestIndex = 0; for (index = 0; index < expectedItems.Count; index++) { T expectedItem = expectedItems[index]; int previousSubjectIndex = subjectIndex; - subjectIndex = IndexOf(actualItems, expectedItem, subjectIndex, areSameOrEqual); + subjectIndex = IndexOf(actualItems, expectedItem, startIndex: subjectIndex); highestIndex = Math.Max(index, highestIndex); if (subjectIndex == -1) @@ -1005,7 +1003,7 @@ public AndConstraint ContainInConsecutiveOrder(IEnumerable expec Subject, expected, expectedItems[highestIndex], highestIndex); } - if (index > 0 && SubjectIndexIsConsecutive(subjectIndex, previousSubjectIndex)) + if (index > 0 && !SubjectIndexIsConsecutive(subjectIndex, previousSubjectIndex)) { index = -1; subjectIndex = previousSubjectIndex; @@ -2337,11 +2335,10 @@ public AndConstraint NotContainInOrder(IEnumerable unexpected, s { IList actualItems = Subject.ConvertOrCastToList(); int subjectIndex = 0; - - Func areSameOrEqual = ObjectExtensions.GetComparer(); + foreach (var unexpectedItem in unexpectedItems) { - subjectIndex = IndexOf(actualItems, unexpectedItem, subjectIndex, areSameOrEqual); + subjectIndex = IndexOf(actualItems, unexpectedItem, startIndex: subjectIndex); if (subjectIndex == -1) { @@ -2413,23 +2410,20 @@ public AndConstraint NotContainInConsecutiveOrder(IEnumerable un } int subjectIndex = 0; - - Func areSameOrEqual = ObjectExtensions.GetComparer(); int index; - for (index = 0; index < unexpectedItems.Count; index++) { T unexpectedItem = unexpectedItems[index]; int previousSubjectIndex = subjectIndex; - subjectIndex = IndexOf(actualItems, unexpectedItem, subjectIndex, areSameOrEqual); + subjectIndex = IndexOf(actualItems, unexpectedItem, startIndex: subjectIndex); if (subjectIndex == -1) { return new AndConstraint((TAssertions)this); } - if (index > 0 && SubjectIndexIsConsecutive(subjectIndex, previousSubjectIndex)) + if (index > 0 && !SubjectIndexIsConsecutive(subjectIndex, previousSubjectIndex)) { index = -1; subjectIndex = previousSubjectIndex; @@ -3455,19 +3449,20 @@ private AndConstraint NotBeInOrder(IComparer comparer, SortOrder return new AndConstraint((TAssertions)this); } - private static int IndexOf(IList items, T item, int index, Func comparer) + private static int IndexOf(IList items, T item, int startIndex) { - for (; index < items.Count; index++) + Func comparer = ObjectExtensions.GetComparer(); + for (; startIndex < items.Count; startIndex++) { - if (comparer(items[index], item)) + if (comparer(items[startIndex], item)) { - index++; - return index; + startIndex++; + return startIndex; } } return -1; } - private static bool SubjectIndexIsConsecutive(int subjectIndex, int previousSubjectIndex) => subjectIndex != previousSubjectIndex + 1; + private static bool SubjectIndexIsConsecutive(int subjectIndex, int previousSubjectIndex) => subjectIndex == previousSubjectIndex + 1; } From 745cb812fc58331efd7d714c64616759e2ffb6e7 Mon Sep 17 00:00:00 2001 From: Stacy Cashmore <33732891+StacyCash@users.noreply.github.com> Date: Sun, 7 Aug 2022 21:08:47 +0200 Subject: [PATCH 17/18] Add int extension IsConsecutiveTo --- .../GenericCollectionAssertions.cs | 19 ++++++++++--------- .../Common/IntegerExtensions.cs | 2 ++ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index 3246d98688..65ec43716d 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -984,9 +984,9 @@ public AndConstraint ContainInConsecutiveOrder(IEnumerable expec int subjectIndex = 0; int highestIndex = 0; - - int index; - for (index = 0; index < expectedItems.Count; index++) + +#pragma warning disable AV1530 // If we need to restart our search then we need to reset the index + for (var index = 0; index < expectedItems.Count; index++) { T expectedItem = expectedItems[index]; int previousSubjectIndex = subjectIndex; @@ -1003,12 +1003,13 @@ public AndConstraint ContainInConsecutiveOrder(IEnumerable expec Subject, expected, expectedItems[highestIndex], highestIndex); } - if (index > 0 && !SubjectIndexIsConsecutive(subjectIndex, previousSubjectIndex)) + if (index > 0 && !previousSubjectIndex.IsConsecutiveTo(subjectIndex)) { index = -1; subjectIndex = previousSubjectIndex; } } +#pragma warning restore AV1530 } return new AndConstraint((TAssertions)this); @@ -2410,8 +2411,9 @@ public AndConstraint NotContainInConsecutiveOrder(IEnumerable un } int subjectIndex = 0; - int index; - for (index = 0; index < unexpectedItems.Count; index++) + +#pragma warning disable AV1530 // If we need to restart our search then we need to reset the index + for (var index = 0; index < unexpectedItems.Count; index++) { T unexpectedItem = unexpectedItems[index]; @@ -2423,12 +2425,13 @@ public AndConstraint NotContainInConsecutiveOrder(IEnumerable un return new AndConstraint((TAssertions)this); } - if (index > 0 && !SubjectIndexIsConsecutive(subjectIndex, previousSubjectIndex)) + if (index > 0 && !previousSubjectIndex.IsConsecutiveTo(subjectIndex)) { index = -1; subjectIndex = previousSubjectIndex; } } +#pragma warning disable AV1530 Execute.Assertion .BecauseOf(because, becauseArgs) @@ -3463,6 +3466,4 @@ private static int IndexOf(IList items, T item, int startIndex) return -1; } - - private static bool SubjectIndexIsConsecutive(int subjectIndex, int previousSubjectIndex) => subjectIndex == previousSubjectIndex + 1; } diff --git a/Src/FluentAssertions/Common/IntegerExtensions.cs b/Src/FluentAssertions/Common/IntegerExtensions.cs index 4ced96ab3d..65cd1e47c5 100644 --- a/Src/FluentAssertions/Common/IntegerExtensions.cs +++ b/Src/FluentAssertions/Common/IntegerExtensions.cs @@ -3,4 +3,6 @@ internal static class IntegerExtensions { public static string Times(this int count) => count == 1 ? "1 time" : $"{count} times"; + + internal static bool IsConsecutiveTo(this int startNumber, int endNumber) => endNumber == startNumber + 1; } From 76de1b1f4158e5603abf39b53bcdb04bb57e4776 Mon Sep 17 00:00:00 2001 From: Stacy Cashmore <33732891+StacyCash@users.noreply.github.com> Date: Mon, 8 Aug 2022 20:37:24 +0200 Subject: [PATCH 18/18] Rewrote (Not)ContainInConsecutiveOrder functions to remove index manipulation --- .../GenericCollectionAssertions.cs | 99 +++++++++++-------- ...ssertionSpecs.ContainInConsecutiveOrder.cs | 49 +++++++++ 2 files changed, 107 insertions(+), 41 deletions(-) diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index 65ec43716d..f98af396ad 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -980,36 +980,41 @@ public AndConstraint ContainInConsecutiveOrder(IEnumerable expec if (success) { IList expectedItems = expected.ConvertOrCastToList(); + + if (expectedItems.Count == 0) + { + return new AndConstraint((TAssertions)this); + } + IList actualItems = Subject.ConvertOrCastToList(); int subjectIndex = 0; int highestIndex = 0; - -#pragma warning disable AV1530 // If we need to restart our search then we need to reset the index - for (var index = 0; index < expectedItems.Count; index++) + + while (subjectIndex != -1) { - T expectedItem = expectedItems[index]; - int previousSubjectIndex = subjectIndex; - subjectIndex = IndexOf(actualItems, expectedItem, startIndex: subjectIndex); - highestIndex = Math.Max(index, highestIndex); + subjectIndex = IndexOf(actualItems, expectedItems[0], startIndex: subjectIndex); - if (subjectIndex == -1) + if (subjectIndex != -1) { - Execute.Assertion - .BecauseOf(because, becauseArgs) - .FailWith( - "Expected {context:collection} {0} to contain items {1} in order{reason}" + - ", but {2} (index {3}) did not appear (in the right consecutive order).", - Subject, expected, expectedItems[highestIndex], highestIndex); - } + int consecutiveItems = ConsecutiveItemCount(actualItems, expectedItems, startIndex: subjectIndex); - if (index > 0 && !previousSubjectIndex.IsConsecutiveTo(subjectIndex)) - { - index = -1; - subjectIndex = previousSubjectIndex; + if (consecutiveItems == expectedItems.Count) + { + return new AndConstraint((TAssertions)this); + } + + highestIndex = Math.Max(highestIndex, consecutiveItems); + subjectIndex++; } } -#pragma warning restore AV1530 + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith( + "Expected {context:collection} {0} to contain items {1} in order{reason}" + + ", but {2} (index {3}) did not appear (in the right consecutive order).", + Subject, expected, expectedItems[highestIndex], highestIndex); } return new AndConstraint((TAssertions)this); @@ -2412,33 +2417,27 @@ public AndConstraint NotContainInConsecutiveOrder(IEnumerable un int subjectIndex = 0; -#pragma warning disable AV1530 // If we need to restart our search then we need to reset the index - for (var index = 0; index < unexpectedItems.Count; index++) + while (subjectIndex != -1) { - T unexpectedItem = unexpectedItems[index]; - - int previousSubjectIndex = subjectIndex; - subjectIndex = IndexOf(actualItems, unexpectedItem, startIndex: subjectIndex); + subjectIndex = IndexOf(actualItems, unexpectedItems[0], startIndex: subjectIndex); - if (subjectIndex == -1) + if (subjectIndex != -1) { - return new AndConstraint((TAssertions)this); - } + int consecutiveItems = ConsecutiveItemCount(actualItems, unexpectedItems, startIndex: subjectIndex); - if (index > 0 && !previousSubjectIndex.IsConsecutiveTo(subjectIndex)) - { - index = -1; - subjectIndex = previousSubjectIndex; + if (consecutiveItems == unexpectedItems.Count) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith( + "Expected {context:collection} {0} to not contain items {1} in consecutive order{reason}, " + + "but items appeared in order ending at index {2}.", + Subject, unexpectedItems, subjectIndex + consecutiveItems - 2); + } + + subjectIndex++; } } -#pragma warning disable AV1530 - - Execute.Assertion - .BecauseOf(because, becauseArgs) - .FailWith( - "Expected {context:collection} {0} to not contain items {1} in consecutive order{reason}, " + - "but items appeared in order ending at index {2}.", - Subject, unexpectedItems, subjectIndex - 1); } return new AndConstraint((TAssertions)this); @@ -3466,4 +3465,22 @@ private static int IndexOf(IList items, T item, int startIndex) return -1; } + + private static int ConsecutiveItemCount(IList actualItems, IList expectedItems, int startIndex) + { + for (var index = 1; index < expectedItems.Count; index++) + { + T unexpectedItem = expectedItems[index]; + + int previousSubjectIndex = startIndex; + startIndex = IndexOf(actualItems, unexpectedItem, startIndex: startIndex); + + if (startIndex == -1 || !previousSubjectIndex.IsConsecutiveTo(startIndex)) + { + return index; + } + } + + return expectedItems.Count; + } } diff --git a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs index 89ebc9c5da..362a0eda2d 100644 --- a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs +++ b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainInConsecutiveOrder.cs @@ -22,6 +22,16 @@ public void When_the_first_collection_contains_a_duplicate_item_without_affectin collection.Should().ContainInConsecutiveOrder(1, 2, 3); } + [Fact] + public void When_the_second_collection_contains_just_1_item_included_in_the_first_it_should_not_throw() + { + // Arrange + var collection = new[] { 1, 2, 3, 2 }; + + // Act / Assert + collection.Should().ContainInConsecutiveOrder(2); + } + [Fact] public void When_the_first_collection_contains_a_partial_duplicate_sequence_at_the_start_without_affecting_the_explicit_order_it_should_not_throw() { @@ -76,6 +86,20 @@ public void When_two_collections_contain_the_same_items_but_not_in_the_same_expl "Expected collection {1, 2, 2, 3} to contain items {1, 2, 3} in order, but 3 (index 2) did not appear (in the right consecutive order)."); } + [Fact] + public void When_the_second_collection_contains_just_1_item_not_included_in_the_first_it_should_throw() + { + // Arrange + var collection = new[] { 1, 2, 2, 3 }; + + // Act / Assert + Action act = () => collection.Should().ContainInConsecutiveOrder(4); + + // Assert + act.Should().Throw().WithMessage( + "Expected collection {1, 2, 2, 3} to contain items {4} in order, but 4 (index 0) did not appear (in the right consecutive order)."); + } + [Fact] public void When_end_of_first_collection_is_a_partial_match_of_second_at_end_it_should_throw() { @@ -169,6 +193,16 @@ public void When_two_collections_contain_the_same_items_but_in_different_order_i collection.Should().NotContainInConsecutiveOrder(2, 1); } + [Fact] + public void When_the_second_collection_contains_just_1_item_not_included_in_the_first_it_should_not_throw() + { + // Arrange + var collection = new[] { 1, 2, 3 }; + + // Act / Assert + collection.Should().NotContainInConsecutiveOrder(4); + } + [Fact] public void When_a_collection_does_not_contain_an_ordered_item_it_should_not_throw() { @@ -265,6 +299,21 @@ public void When_collection_and_contains_contain_the_same_items_in_the_same_orde "but items appeared in order ending at index 3."); } + [Fact] + public void When_the_second_collection_contains_just_1_item_included_in_the_first_it_should_throw() + { + // Arrange + var collection = new object[] { 1, null, 2, "string" }; + + // Act + Action act = () => collection.Should().NotContainInConsecutiveOrder(2); + + // Assert + act.Should().Throw().WithMessage( + "Expected collection {1, , 2, \"string\"} to not contain items {2} in consecutive order, " + + "but items appeared in order ending at index 2."); + } + [Fact] public void When_the_first_collection_contains_a_duplicate_item_without_affecting_the_order_it_should_throw() {