Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Extend collection assertions with ContainInConsecutiveOrder and NotContainInConsecutiveOrder #1963

Merged
merged 19 commits into from Aug 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f4a8c68
Add ContainInConsecutiveOrder and NotContainInConsecutiveOrder
StacyCash Jul 19, 2022
4fad081
Optimise searching for expected list
StacyCash Jul 26, 2022
46a4135
Make NotContainsInConsecutiveOrder more efficient
StacyCash Jul 26, 2022
356f7c7
Update docs/_pages/releases.md
StacyCash Jul 26, 2022
fafd108
Rename Explicit to Consecutive
StacyCash Jul 26, 2022
f3e97c1
Format code based on PR comments
StacyCash Jul 26, 2022
9587001
Rename Explicit to Consecuritve for the test classes
StacyCash Jul 26, 2022
54790a0
Refactor/rewrite ContainInConsecutiveOrder
StacyCash Jul 28, 2022
7eef1ba
Merge branch 'fluentassertions:develop' into develop
StacyCash Jul 28, 2022
23e8059
Rewrote based on @jnyrup improvements
StacyCash Jul 28, 2022
2a940fe
Rewrite NotContainsInConsecuritveOrder
StacyCash Jul 28, 2022
a5be6cb
Add test for ContainsInConsecutiveOrder when expected is empty
StacyCash Jul 28, 2022
9313fe7
Update Src/FluentAssertions/Collections/GenericCollectionAssertions.cs
StacyCash Aug 1, 2022
b560ef6
Update Src/FluentAssertions/Collections/GenericCollectionAssertions.cs
StacyCash Aug 1, 2022
e58610e
Update Src/FluentAssertions/Collections/GenericCollectionAssertions.cs
StacyCash Aug 1, 2022
23c02fa
Cleanup and add SubjectIndexIsConsecutive helper function
StacyCash Aug 5, 2022
89c9100
Changes based on PR comments
StacyCash Aug 7, 2022
745cb81
Add int extension IsConsecutiveTo
StacyCash Aug 7, 2022
76de1b1
Rewrote (Not)ContainInConsecutiveOrder functions to remove index mani…
StacyCash Aug 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
StacyCash marked this conversation as resolved.
Show resolved Hide resolved

if (success)
{
IList<T> expectedItems = expected.ConvertOrCastToList();
StacyCash marked this conversation as resolved.
Show resolved Hide resolved

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