Skip to content

Commit

Permalink
Extend collection assertions with ContainInConsecutiveOrder and `No…
Browse files Browse the repository at this point in the history
…tContainInConsecutiveOrder` (#1963)

* Add ContainInConsecutiveOrder and NotContainInConsecutiveOrder

Two functions to check that a cleections contains or doesn't contain the items from a second collections consecutively.

* Optimise searching for expected list

* Make NotContainsInConsecutiveOrder more efficient

* Update docs/_pages/releases.md

Co-authored-by: Jonas Nyrup <jnyrup@users.noreply.github.com>

* Rename Explicit to Consecutive

* Format code based on PR comments

* Rename Explicit to Consecuritve for the test classes

* Refactor/rewrite ContainInConsecutiveOrder

* Rewrote based on @jnyrup improvements

* Rewrite NotContainsInConsecuritveOrder

* Add test for ContainsInConsecutiveOrder when expected is empty

* Update Src/FluentAssertions/Collections/GenericCollectionAssertions.cs

Co-authored-by: Jonas Nyrup <jnyrup@users.noreply.github.com>

* Update Src/FluentAssertions/Collections/GenericCollectionAssertions.cs

Co-authored-by: Jonas Nyrup <jnyrup@users.noreply.github.com>

* Update Src/FluentAssertions/Collections/GenericCollectionAssertions.cs

Co-authored-by: Jonas Nyrup <jnyrup@users.noreply.github.com>

* Cleanup and add SubjectIndexIsConsecutive helper function

* Changes based on PR comments

* Add int extension IsConsecutiveTo

* Rewrote (Not)ContainInConsecutiveOrder functions to remove index manipulation

Co-authored-by: Jonas Nyrup <jnyrup@users.noreply.github.com>
  • Loading branch information
StacyCash and jnyrup committed Aug 9, 2022
1 parent de9faf3 commit 69493a4
Show file tree
Hide file tree
Showing 11 changed files with 592 additions and 11 deletions.
197 changes: 186 additions & 11 deletions Src/FluentAssertions/Collections/GenericCollectionAssertions.cs
Expand Up @@ -921,12 +921,11 @@ public AndConstraint<TAssertions> ContainInOrder(params T[] expected)
IList<T> actualItems = Subject.ConvertOrCastToList();

int subjectIndex = 0;

Func<T, T, bool> areSameOrEqual = ObjectExtensions.GetComparer<T>();

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)
{
Expand All @@ -943,6 +942,84 @@ public AndConstraint<TAssertions> ContainInOrder(params T[] expected)
return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Expects the current collection to contain the specified elements in the exact same order, and to be consecutive.
/// using their <see cref="object.Equals(object)" /> implementation.
/// </summary>
/// <param name="expected">An <see cref="IEnumerable{T}"/> with the expected elements.</param>
public AndConstraint<TAssertions> ContainInConsecutiveOrder(params T[] expected)
{
return ContainInConsecutiveOrder(expected, string.Empty);
}

/// <summary>
/// Expects the current collection to contain the specified elements in the exact same order, and to be consecutive.
/// </summary>
/// <remarks>
/// Elements are compared using their <see cref="object.Equals(object)" /> implementation.
/// </remarks>
/// <param name="expected">An <see cref="IEnumerable{T}"/> with the expected elements.</param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="expected"/> is <c>null</c>.</exception>
public AndConstraint<TAssertions> ContainInConsecutiveOrder(IEnumerable<T> expected, string because = "",
params object[] becauseArgs)
{
Guard.ThrowIfArgumentIsNull(expected, nameof(expected), "Cannot verify ordered containment against a <null> 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 <null>.", expected);

if (success)
{
IList<T> expectedItems = expected.ConvertOrCastToList();

if (expectedItems.Count == 0)
{
return new AndConstraint<TAssertions>((TAssertions)this);
}

IList<T> actualItems = Subject.ConvertOrCastToList();

int subjectIndex = 0;
int highestIndex = 0;

while (subjectIndex != -1)
{
subjectIndex = IndexOf(actualItems, expectedItems[0], startIndex: subjectIndex);

if (subjectIndex != -1)
{
int consecutiveItems = ConsecutiveItemCount(actualItems, expectedItems, startIndex: subjectIndex);

if (consecutiveItems == expectedItems.Count)
{
return new AndConstraint<TAssertions>((TAssertions)this);
}

highestIndex = Math.Max(highestIndex, consecutiveItems);
subjectIndex++;
}
}

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>((TAssertions)this);
}

/// <summary>
/// Asserts that the current collection contains at least one element that is assignable to the type <typeparamref name="TExpectation" />.
/// </summary>
Expand Down Expand Up @@ -2264,11 +2341,10 @@ public AndConstraint<TAssertions> NotContainInOrder(params T[] unexpected)
{
IList<T> actualItems = Subject.ConvertOrCastToList();
int subjectIndex = 0;

Func<T, T, bool> areSameOrEqual = ObjectExtensions.GetComparer<T>();

foreach (var unexpectedItem in unexpectedItems)
{
subjectIndex = IndexOf(actualItems, unexpectedItem, subjectIndex, areSameOrEqual);
subjectIndex = IndexOf(actualItems, unexpectedItem, startIndex: subjectIndex);

if (subjectIndex == -1)
{
Expand All @@ -2287,6 +2363,86 @@ public AndConstraint<TAssertions> NotContainInOrder(params T[] unexpected)
return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts the current collection does not contain the specified elements in the exact same order and are consecutive.
/// </summary>
/// <remarks>
/// Elements are compared using their <see cref="object.Equals(object)" /> implementation.
/// </remarks>
/// <param name="unexpected">A <see cref="Array"/> with the unexpected elements.</param>
/// <exception cref="ArgumentNullException"><paramref name="unexpected"/> is <c>null</c>.</exception>
public AndConstraint<TAssertions> NotContainInConsecutiveOrder(params T[] unexpected)
{
return NotContainInConsecutiveOrder(unexpected, string.Empty);
}

/// <summary>
/// Asserts the current collection does not contain the specified elements in the exact same order and consecutively.
/// </summary>
/// <remarks>
/// Elements are compared using their <see cref="object.Equals(object)" /> implementation.
/// </remarks>
/// <param name="unexpected">An <see cref="IEnumerable{T}"/> with the unexpected elements.</param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="unexpected"/> is <c>null</c>.</exception>
public AndConstraint<TAssertions> NotContainInConsecutiveOrder(IEnumerable<T> unexpected, string because = "",
params object[] becauseArgs)
{
Guard.ThrowIfArgumentIsNull(unexpected, nameof(unexpected), "Cannot verify absence of ordered containment against a <null> collection.");

if (Subject is null)
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.FailWith("Cannot verify absence of ordered containment in a <null> collection.");

return new AndConstraint<TAssertions>((TAssertions)this);
}

IList<T> unexpectedItems = unexpected.ConvertOrCastToList();
if (unexpectedItems.Any())
{
IList<T> actualItems = Subject.ConvertOrCastToList();

if (unexpectedItems.Count > actualItems.Count)
{
return new AndConstraint<TAssertions>((TAssertions)this);
}

int subjectIndex = 0;

while (subjectIndex != -1)
{
subjectIndex = IndexOf(actualItems, unexpectedItems[0], startIndex: subjectIndex);

if (subjectIndex != -1)
{
int consecutiveItems = ConsecutiveItemCount(actualItems, unexpectedItems, startIndex: subjectIndex);

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++;
}
}
}

return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts that the collection does not contain any <c>null</c> items.
/// </summary>
Expand Down Expand Up @@ -3295,17 +3451,36 @@ private AndConstraint<TAssertions> NotBeInOrder(IComparer<T> comparer, SortOrder
return new AndConstraint<TAssertions>((TAssertions)this);
}

private static int IndexOf(IList<T> items, T item, int index, Func<T, T, bool> comparer)
private static int IndexOf(IList<T> items, T item, int startIndex)
{
for (; index < items.Count; index++)
Func<T, T, bool> comparer = ObjectExtensions.GetComparer<T>();
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 int ConsecutiveItemCount(IList<T> actualItems, IList<T> 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;
}
}
2 changes: 2 additions & 0 deletions Src/FluentAssertions/Common/IntegerExtensions.cs
Expand Up @@ -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;
}
Expand Up @@ -450,6 +450,8 @@ namespace FluentAssertions.Collections
public FluentAssertions.AndWhichConstraint<TAssertions, T> Contain(T expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<TAssertions, T> ContainEquivalentOf<TExpectation>(TExpectation expectation, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<TAssertions, T> ContainEquivalentOf<TExpectation>(TExpectation expectation, System.Func<FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>, FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>> config, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> ContainInConsecutiveOrder(params T[] expected) { }
public FluentAssertions.AndConstraint<TAssertions> ContainInConsecutiveOrder(System.Collections.Generic.IEnumerable<T> expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> ContainInOrder(params T[] expected) { }
public FluentAssertions.AndConstraint<TAssertions> ContainInOrder(System.Collections.Generic.IEnumerable<T> expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> ContainItemsAssignableTo<TExpectation>(string because = "", params object[] becauseArgs) { }
Expand Down Expand Up @@ -494,6 +496,8 @@ namespace FluentAssertions.Collections
public FluentAssertions.AndWhichConstraint<TAssertions, T> NotContain(T unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainEquivalentOf<TExpectation>(TExpectation unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainEquivalentOf<TExpectation>(TExpectation unexpected, System.Func<FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>, FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>> config, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainInConsecutiveOrder(params T[] unexpected) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainInConsecutiveOrder(System.Collections.Generic.IEnumerable<T> unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainInOrder(params T[] unexpected) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainInOrder(System.Collections.Generic.IEnumerable<T> unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainNulls(string because = "", params object[] becauseArgs) { }
Expand Down
Expand Up @@ -462,6 +462,8 @@ namespace FluentAssertions.Collections
public FluentAssertions.AndWhichConstraint<TAssertions, T> Contain(T expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<TAssertions, T> ContainEquivalentOf<TExpectation>(TExpectation expectation, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<TAssertions, T> ContainEquivalentOf<TExpectation>(TExpectation expectation, System.Func<FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>, FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>> config, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> ContainInConsecutiveOrder(params T[] expected) { }
public FluentAssertions.AndConstraint<TAssertions> ContainInConsecutiveOrder(System.Collections.Generic.IEnumerable<T> expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> ContainInOrder(params T[] expected) { }
public FluentAssertions.AndConstraint<TAssertions> ContainInOrder(System.Collections.Generic.IEnumerable<T> expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> ContainItemsAssignableTo<TExpectation>(string because = "", params object[] becauseArgs) { }
Expand Down Expand Up @@ -506,6 +508,8 @@ namespace FluentAssertions.Collections
public FluentAssertions.AndWhichConstraint<TAssertions, T> NotContain(T unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainEquivalentOf<TExpectation>(TExpectation unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainEquivalentOf<TExpectation>(TExpectation unexpected, System.Func<FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>, FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>> config, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainInConsecutiveOrder(params T[] unexpected) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainInConsecutiveOrder(System.Collections.Generic.IEnumerable<T> unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainInOrder(params T[] unexpected) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainInOrder(System.Collections.Generic.IEnumerable<T> unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainNulls(string because = "", params object[] becauseArgs) { }
Expand Down
Expand Up @@ -450,6 +450,8 @@ namespace FluentAssertions.Collections
public FluentAssertions.AndWhichConstraint<TAssertions, T> Contain(T expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<TAssertions, T> ContainEquivalentOf<TExpectation>(TExpectation expectation, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<TAssertions, T> ContainEquivalentOf<TExpectation>(TExpectation expectation, System.Func<FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>, FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>> config, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> ContainInConsecutiveOrder(params T[] expected) { }
public FluentAssertions.AndConstraint<TAssertions> ContainInConsecutiveOrder(System.Collections.Generic.IEnumerable<T> expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> ContainInOrder(params T[] expected) { }
public FluentAssertions.AndConstraint<TAssertions> ContainInOrder(System.Collections.Generic.IEnumerable<T> expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> ContainItemsAssignableTo<TExpectation>(string because = "", params object[] becauseArgs) { }
Expand Down Expand Up @@ -494,6 +496,8 @@ namespace FluentAssertions.Collections
public FluentAssertions.AndWhichConstraint<TAssertions, T> NotContain(T unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainEquivalentOf<TExpectation>(TExpectation unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainEquivalentOf<TExpectation>(TExpectation unexpected, System.Func<FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>, FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>> config, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainInConsecutiveOrder(params T[] unexpected) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainInConsecutiveOrder(System.Collections.Generic.IEnumerable<T> unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainInOrder(params T[] unexpected) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainInOrder(System.Collections.Generic.IEnumerable<T> unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainNulls(string because = "", params object[] becauseArgs) { }
Expand Down

0 comments on commit 69493a4

Please sign in to comment.