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

Implement a matcher for parallel runs in iterables #383

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import java.util.ArrayList;
import java.util.List;
import org.hamcrest.collection.IsIterableContainingParallelRuns.MatchParallelRuns;

import static java.util.Arrays.asList;
import static org.hamcrest.core.IsEqual.equalTo;
Expand All @@ -19,54 +20,16 @@ public IsIterableContainingInRelativeOrder(List<Matcher<? super E>> matchers) {

@Override
protected boolean matchesSafely(Iterable<? extends E> iterable, Description mismatchDescription) {
MatchSeriesInRelativeOrder<E> matchSeriesInRelativeOrder = new MatchSeriesInRelativeOrder<>(matchers, mismatchDescription);
matchSeriesInRelativeOrder.processItems(iterable);
return matchSeriesInRelativeOrder.isFinished();
final MatchParallelRuns<E> matchParallelRuns =
new MatchParallelRuns<>(1, matchers, mismatchDescription);
matchParallelRuns.processItems(iterable);
return matchParallelRuns.isFinished();
}

public void describeTo(Description description) {
description.appendText("iterable containing ").appendList("[", ", ", "]", matchers).appendText(" in relative order");
}

private static class MatchSeriesInRelativeOrder<F> {
public final List<Matcher<? super F>> matchers;
private final Description mismatchDescription;
private int nextMatchIx = 0;
private F lastMatchedItem = null;

public MatchSeriesInRelativeOrder(List<Matcher<? super F>> matchers, Description mismatchDescription) {
this.mismatchDescription = mismatchDescription;
if (matchers.isEmpty()) {
throw new IllegalArgumentException("Should specify at least one expected element");
}
this.matchers = matchers;
}

public void processItems(Iterable<? extends F> iterable) {
for (F item : iterable) {
if (nextMatchIx < matchers.size()) {
Matcher<? super F> matcher = matchers.get(nextMatchIx);
if (matcher.matches(item)) {
lastMatchedItem = item;
nextMatchIx++;
}
}
}
}

public boolean isFinished() {
if (nextMatchIx < matchers.size()) {
mismatchDescription.appendDescriptionOf(matchers.get(nextMatchIx)).appendText(" was not found");
if (lastMatchedItem != null) {
mismatchDescription.appendText(" after ").appendValue(lastMatchedItem);
}
return false;
}
return true;
}

}

/**
* Creates a matcher for {@link Iterable}s that matches when a single pass over the
* examined {@link Iterable} yields a series of items, that contains items logically equal to the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package org.hamcrest.collection;

import static java.util.Arrays.asList;
import static org.hamcrest.core.IsEqual.equalTo;

import java.util.ArrayList;
import java.util.List;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;

/**
* A matcher like that can check an iterable for parallel runs of a list of matchers. It is similar
* to {@link org.hamcrest.collection.IsIterableContainingInRelativeOrder}, in fact behaving exactly
* the same when {@code numRuns = 1}.
*
* @param <E> Type of items to match.
* @see #containsParallelRunsOf(int, List)
*/
public class IsIterableContainingParallelRuns<E> extends TypeSafeDiagnosingMatcher<Iterable<E>> {

private final int numRuns;
private final List<Matcher<? super E>> matchers;

/**
* Construct a new matcher that will check for parallel runs.
*
* @see IsIterableContainingParallelRuns
* @see #containsParallelRunsOf(int, List)
*/
public IsIterableContainingParallelRuns(
final int numRuns, final List<Matcher<? super E>> matchers
) {
if (numRuns <= 0) {
throw new IllegalArgumentException("The number of parallel runs must be strictly positive");
}
this.numRuns = numRuns;
this.matchers = matchers;
}

@Override
protected boolean matchesSafely(
final Iterable<E> iterable, final Description mismatchDescription
) {
final MatchParallelRuns<E> matchParallelRuns =
new MatchParallelRuns<>(numRuns, matchers, mismatchDescription);
matchParallelRuns.processItems(iterable);
return matchParallelRuns.isFinished();
}

@Override
public void describeTo(Description description) {
description.appendText("iterable containing ");
if (numRuns > 1) {
description.appendValue(numRuns).appendText(" parallel runs of ");
}
description.appendList("[", ", ", "]", matchers)
.appendText(" in relative order");
}

static class MatchParallelRuns<F> {
private final int numRuns;
private final List<Matcher<? super F>> matchers;
private final Description mismatchDescription;
private final List<Integer> nextMatchIndexes;
private final List<F> lastMatchedItems;

public MatchParallelRuns(
final int numRuns,
final List<Matcher<? super F>> matchers,
final Description mismatchDescription
) {
this.numRuns = numRuns;
if (matchers.isEmpty()) {
throw new IllegalArgumentException("Should specify at least one expected element");
} else {
this.matchers = matchers;
}
this.mismatchDescription = mismatchDescription;
this.nextMatchIndexes = new ArrayList<>(numRuns);
this.lastMatchedItems = new ArrayList<>(numRuns);
for (int i = 0; i < numRuns; ++i) {
this.nextMatchIndexes.add(0);
this.lastMatchedItems.add(null);
}
}

public void processItems(Iterable<? extends F> iterable) {
for (final F item : iterable) {
for (int i = 0; i < numRuns; ++i) {
final int nextMatchIndex = nextMatchIndexes.get(i);
if (nextMatchIndex < matchers.size() && matchers.get(nextMatchIndex).matches(item)) {
lastMatchedItems.set(i, item);
nextMatchIndexes.set(i, nextMatchIndex + 1);
break;
}
}
}
}

public boolean isFinished() {
boolean isFinished = true;
for (int i = 0; i < numRuns; ++i) {
final int nextMatchIndex = nextMatchIndexes.get(i);
if (nextMatchIndex < matchers.size()) {
if (!isFinished) {
mismatchDescription.appendText("; and ");
}
isFinished = false;
mismatchDescription.appendDescriptionOf(matchers.get(nextMatchIndex))
.appendText(" was not found");
if (lastMatchedItems.get(i) != null) {
mismatchDescription.appendText(" after ").appendValue(lastMatchedItems.get(i));
}
if (numRuns > 1) {
mismatchDescription.appendText(" in run ").appendValue(i + 1);
}
}
}
return isFinished;
}
}

/**
* Creates a matcher for {@link Iterable Iterables} that matches when a single pass over the
* examined {@link Iterable} yields a series of items, that contains items logically equal to the
* corresponding item in the specified items, in the same relative order, with {@code numRuns}
* occurrences of the specified series of items being matched (possibly interspersed).
*/
@SafeVarargs
public static <E> Matcher<Iterable<E>> containsParallelRunsOf(
final int numRuns, final E... items
) {
final List<Matcher<? super E>> matchers = new ArrayList<>(items.length);
for (final Object item : items) {
matchers.add(equalTo(item));
}

return containsParallelRunsOf(numRuns, matchers);
}

@SafeVarargs
public static <E> Matcher<Iterable<E>> containsParallelRunsOf(
final int numRuns, final Matcher<? super E>... matchers
) {
return containsParallelRunsOf(numRuns, asList(matchers));
}

public static <E> Matcher<Iterable<E>> containsParallelRunsOf(
final int numRuns, final List<Matcher<? super E>> matchers
) {
return new IsIterableContainingParallelRuns<>(numRuns, matchers);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package org.hamcrest.collection;

import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.hamcrest.collection.IsIterableContainingParallelRuns.containsParallelRunsOf;
import static org.hamcrest.core.IsEqual.equalTo;

import java.util.List;
import org.hamcrest.AbstractMatcherTest;
import org.hamcrest.FeatureMatcher;
import org.hamcrest.Matcher;

public class IsIterableContainingParallelRunsTest extends AbstractMatcherTest {

@Override
protected Matcher<?> createMatcher() {
return containsParallelRunsOf(1, 1, 2);
}

//
// ---- SINGLE RUN TESTS ---- (same tests cases as IsIterableContainingInRelativeOrderTest) ----
//

public void testMatchingSingleItemIterable() {
assertMatches("Single item iterable",
containsParallelRunsOf(1, 1), singletonList(1));
}

public void testMatchingMultipleItemIterable() {
assertMatches("Multiple item iterable",
containsParallelRunsOf(1, 1, 2, 3), asList(1, 2, 3));
}

public void testMatchesWithMoreElementsThanExpectedAtBeginning() {
assertMatches("More elements at beginning",
containsParallelRunsOf(1, 2, 3, 4), asList(1, 2, 3, 4));
}

public void testMatchesWithMoreElementsThanExpectedAtEnd() {
assertMatches("More elements at end",
containsParallelRunsOf(1, 1, 2, 3), asList(1, 2, 3, 4));
}

public void testMatchesWithMoreElementsThanExpectedInBetween() {
assertMatches("More elements in between",
containsParallelRunsOf(1, 1, 3), asList(1, 2, 3));
}

public void testMatchesSubSection() {
assertMatches("Sub section of iterable",
containsParallelRunsOf(1, 2, 3), asList(1, 2, 3, 4));
}

public void testMatchesWithSingleGapAndNotFirstOrLast() {
assertMatches("Sub section with single gaps without a first or last match",
containsParallelRunsOf(1, 2, 4), asList(1, 2, 3, 4, 5));
}

public void testMatchingSubSectionWithManyGaps() {
assertMatches("Sub section with many gaps iterable",
containsParallelRunsOf(1, 2, 4, 6), asList(1, 2, 3, 4, 5, 6, 7));
}

public void testDoesNotMatchWithFewerElementsThanExpected() {
List<WithValue> valueList = asList(make(1), make(2));
assertMismatchDescription("value with <3> was not found after <WithValue 2>",
containsParallelRunsOf(1, value(1), value(2), value(3)), valueList);
}

public void testDoesNotMatchIfSingleItemNotFound() {
assertMismatchDescription("value with <4> was not found",
containsParallelRunsOf(1, value(4)), singletonList(make(3)));
}

public void testDoesNotMatchIfOneOfMultipleItemsNotFound() {
assertMismatchDescription("value with <3> was not found after <WithValue 2>",
containsParallelRunsOf(1, value(1), value(2), value(3)),
asList(make(1), make(2), make(4)));
}

public void testDoesNotMatchEmptyIterable() {
assertMismatchDescription("value with <4> was not found",
containsParallelRunsOf(1, value(4)), emptyList());
}

public void testHasAReadableDescription() {
assertDescription(
"iterable containing [<1>, <2>] in relative order",
containsParallelRunsOf(1, 1, 2));
}

//
// ---- MULTIPLE PARALLEL RUN TESTS ------------------------------------------------------------
//

public void testMultiMatchesWithoutUnexpectedElements() {
assertMatches("Multiple runs without unexpected elements",
containsParallelRunsOf(2, 1, 2, 3), asList(1, 1, 2, 3, 2, 3));
}

public void testMultiMatchesWithRepeatedElements() {
assertMatches("Multiple runs with repeated elements",
containsParallelRunsOf(2, 1, 2, 1), asList(1, 2, 1, 1, 2, 1));
}

public void testMultiMatchesWithGaps() {
assertMatches("Multiple runs with gaps",
containsParallelRunsOf(4, 1), asList(2, 1, 2, 1, 1, 2, 1, 2));
}

public void testMultiDoesNotMatchIfSingleItemNotFound() {
assertMismatchDescription("value with <2> was not found after <WithValue 1> in run <2>",
containsParallelRunsOf(2, value(1), value(2)),
asList(make(1), make(2), make(1)));
}

public void testMultiDoesNotMatchIfSingleItemNotFoundAtStart() {
assertMismatchDescription("value with <1> was not found in run <2>",
containsParallelRunsOf(2, value(1), value(2)),
asList(make(1), make(2)));
}

public void testMultiDoesNotMatchAndReportsAllMismatchedRuns() {
assertMismatchDescription("<3> was not found after <2> in run <1>; and "
+ "<2> was not found after <1> in run <2>; and "
+ "<1> was not found in run <3>",
containsParallelRunsOf(3, 1, 2, 3),
asList(1, 1, 2));
}

public void testMultiDoesNotMatchEmptyIterable() {
assertMismatchDescription("value with <4> was not found in run <1>; and "
+ "value with <4> was not found in run <2>",
containsParallelRunsOf(2, value(4)), emptyList());
}

public void testMultiHasAReadableDescription() {
assertDescription(
"iterable containing <2> parallel runs of [<1>, <2>] in relative order",
containsParallelRunsOf(2, 1, 2));
assertDescription(
"iterable containing <901> parallel runs of [<1>, <2>] in relative order",
containsParallelRunsOf(901, 1, 2));
}

// ---- TEST UTILITIES -------------------------------------------------------------------------

public static class WithValue {
private final int value;
public WithValue(int value) { this.value = value; }
public int getValue() { return value; }
@Override public String toString() { return "WithValue " + value; }
}

public static WithValue make(int value) {
return new WithValue(value);
}

public static Matcher<WithValue> value(int value) {
return new FeatureMatcher<WithValue, Integer>(equalTo(value), "value with", "value") {
@Override
protected Integer featureValueOf(WithValue actual) {
return actual.getValue();
}
};
}

}