diff --git a/hamcrest/src/main/java/org/hamcrest/collection/IsIterableContainingInRelativeOrder.java b/hamcrest/src/main/java/org/hamcrest/collection/IsIterableContainingInRelativeOrder.java index 34233cf9..09876011 100644 --- a/hamcrest/src/main/java/org/hamcrest/collection/IsIterableContainingInRelativeOrder.java +++ b/hamcrest/src/main/java/org/hamcrest/collection/IsIterableContainingInRelativeOrder.java @@ -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; @@ -19,54 +20,16 @@ public IsIterableContainingInRelativeOrder(List> matchers) { @Override protected boolean matchesSafely(Iterable iterable, Description mismatchDescription) { - MatchSeriesInRelativeOrder matchSeriesInRelativeOrder = new MatchSeriesInRelativeOrder<>(matchers, mismatchDescription); - matchSeriesInRelativeOrder.processItems(iterable); - return matchSeriesInRelativeOrder.isFinished(); + final MatchParallelRuns 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 { - public final List> matchers; - private final Description mismatchDescription; - private int nextMatchIx = 0; - private F lastMatchedItem = null; - - public MatchSeriesInRelativeOrder(List> 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 iterable) { - for (F item : iterable) { - if (nextMatchIx < matchers.size()) { - Matcher 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 diff --git a/hamcrest/src/main/java/org/hamcrest/collection/IsIterableContainingParallelRuns.java b/hamcrest/src/main/java/org/hamcrest/collection/IsIterableContainingParallelRuns.java new file mode 100644 index 00000000..1b3e9330 --- /dev/null +++ b/hamcrest/src/main/java/org/hamcrest/collection/IsIterableContainingParallelRuns.java @@ -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 Type of items to match. + * @see #containsParallelRunsOf(int, List) + */ +public class IsIterableContainingParallelRuns extends TypeSafeDiagnosingMatcher> { + + private final int numRuns; + private final List> matchers; + + /** + * Construct a new matcher that will check for parallel runs. + * + * @see IsIterableContainingParallelRuns + * @see #containsParallelRunsOf(int, List) + */ + public IsIterableContainingParallelRuns( + final int numRuns, final List> 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 iterable, final Description mismatchDescription + ) { + final MatchParallelRuns 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 { + private final int numRuns; + private final List> matchers; + private final Description mismatchDescription; + private final List nextMatchIndexes; + private final List lastMatchedItems; + + public MatchParallelRuns( + final int numRuns, + final List> 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 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 Matcher> containsParallelRunsOf( + final int numRuns, final E... items + ) { + final List> matchers = new ArrayList<>(items.length); + for (final Object item : items) { + matchers.add(equalTo(item)); + } + + return containsParallelRunsOf(numRuns, matchers); + } + + @SafeVarargs + public static Matcher> containsParallelRunsOf( + final int numRuns, final Matcher... matchers + ) { + return containsParallelRunsOf(numRuns, asList(matchers)); + } + + public static Matcher> containsParallelRunsOf( + final int numRuns, final List> matchers + ) { + return new IsIterableContainingParallelRuns<>(numRuns, matchers); + } + +} diff --git a/hamcrest/src/test/java/org/hamcrest/collection/IsIterableContainingParallelRunsTest.java b/hamcrest/src/test/java/org/hamcrest/collection/IsIterableContainingParallelRunsTest.java new file mode 100644 index 00000000..5730420e --- /dev/null +++ b/hamcrest/src/test/java/org/hamcrest/collection/IsIterableContainingParallelRunsTest.java @@ -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 valueList = asList(make(1), make(2)); + assertMismatchDescription("value with <3> was not found after ", + 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 ", + 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 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 value(int value) { + return new FeatureMatcher(equalTo(value), "value with", "value") { + @Override + protected Integer featureValueOf(WithValue actual) { + return actual.getValue(); + } + }; + } + +}