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 15 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
162 changes: 162 additions & 0 deletions Src/FluentAssertions/Collections/GenericCollectionAssertions.cs
Expand Up @@ -943,6 +943,79 @@ 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
IList<T> actualItems = Subject.ConvertOrCastToList();

int subjectIndex = 0;

Func<T, T, bool> areSameOrEqual = ObjectExtensions.GetComparer<T>();
int index;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ You can move the declaration in the for loop. It'll helps to make the method less long (and noisy).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what you mean here, the declarations here need to be outside of the for loop to work

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? It's only needed within the block scope created by the for loop.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compiler error AV1530 - if you declare the index variable in the loop you cannot update it in the loop itself. Which we need to do to reset it should we have to start searching from the start of the unexpected items again.

(Of course, that does raise the question of if the loop is the right way to solve the problem 🤔 - but I think that it is the cleanest)

The 'subjectIndexandhighestIndex` need to be outside of the loop as they shouldn't be recreated/reset each loop.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that wasn't obvious at all. Updating a loop variable is quite ugly indeed. But I see now how it works.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you rather that this is really obvious?

We could declare the index in the loop and disable VS1530 for the loop itself... I see that is done elsewhere for other reasons.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make it clearer that you're doing that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having that code to disable the rule annoyed me so I spent some time thinking about it and figured out how to rewrite it to remove the index manipulation. I'm on the fence as to which is easier to read, let me know which you prefer (I'll revert if you want 😅)

int highestIndex = 0;
for (index = 0; index < expectedItems.Count; index++)
{
T expectedItem = expectedItems[index];
int previousSubjectIndex = subjectIndex;
subjectIndex = IndexOf(actualItems, expectedItem, subjectIndex, areSameOrEqual);
highestIndex = Math.Max(index, highestIndex);
dennisdoomen marked this conversation as resolved.
Show resolved Hide resolved

if (subjectIndex == -1)
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.FailWith(
"Expected {context:collection} {0} to contain items {1} in order{reason}" +
StacyCash marked this conversation as resolved.
Show resolved Hide resolved
", but {2} (index {3}) did not appear (in the right consecutive order).",
Subject, expected, expectedItems[highestIndex], highestIndex);
}

if (index > 0 && subjectIndex != previousSubjectIndex + 1)
{
index = -1;
subjectIndex = previousSubjectIndex;
}
}
}

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 @@ -2287,6 +2360,95 @@ 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;

Func<T, T, bool> areSameOrEqual = ObjectExtensions.GetComparer<T>();
int index;
int highestIndex = 0;

for (index = 0; index < unexpectedItems.Count; index++)
{
T unexpectedItem = unexpectedItems[index];

int previousSubjectIndex = subjectIndex;
subjectIndex = IndexOf(actualItems, unexpectedItem, subjectIndex, areSameOrEqual);
StacyCash marked this conversation as resolved.
Show resolved Hide resolved
highestIndex = Math.Max(index, highestIndex);
StacyCash marked this conversation as resolved.
Show resolved Hide resolved

if (subjectIndex == -1)
{
return new AndConstraint<TAssertions>((TAssertions)this);
}

if (index > 0 && subjectIndex - previousSubjectIndex > 1)
StacyCash marked this conversation as resolved.
Show resolved Hide resolved
{
index = -1;
subjectIndex = previousSubjectIndex;
}
}

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

/// <summary>
/// Asserts that the collection does not contain any <c>null</c> items.
/// </summary>
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
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
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