001/*
002 * Copyright (C) 2015 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.google.common.testing;
018
019import static com.google.common.base.Preconditions.checkNotNull;
020import static junit.framework.Assert.assertTrue;
021
022import com.google.common.annotations.GwtCompatible;
023import com.google.errorprone.annotations.CanIgnoreReturnValue;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.Collections;
027import java.util.List;
028import java.util.Objects;
029import java.util.function.BiPredicate;
030import java.util.stream.Collector;
031import org.checkerframework.checker.nullness.qual.Nullable;
032
033/**
034 * Tester for {@code Collector} implementations.
035 *
036 * <p>Example usage:
037 *
038 * <pre>
039 * CollectorTester.of(Collectors.summingInt(Integer::parseInt))
040 *     .expectCollects(3, "1", "2")
041 *     .expectCollects(10, "1", "4", "3", "2")
042 *     .expectCollects(5, "-3", "0", "8");
043 * </pre>
044 *
045 * @author Louis Wasserman
046 * @since 21.0
047 */
048@GwtCompatible
049@ElementTypesAreNonnullByDefault
050public final class CollectorTester<
051    T extends @Nullable Object, A extends @Nullable Object, R extends @Nullable Object> {
052  /**
053   * Creates a {@code CollectorTester} for the specified {@code Collector}. The result of the {@code
054   * Collector} will be compared to the expected value using {@link Object#equals}.
055   */
056  public static <T extends @Nullable Object, A extends @Nullable Object, R extends @Nullable Object>
057      CollectorTester<T, A, R> of(Collector<T, A, R> collector) {
058    return of(collector, Objects::equals);
059  }
060
061  /**
062   * Creates a {@code CollectorTester} for the specified {@code Collector}. The result of the {@code
063   * Collector} will be compared to the expected value using the specified {@code equivalence}.
064   */
065  public static <T extends @Nullable Object, A extends @Nullable Object, R extends @Nullable Object>
066      CollectorTester<T, A, R> of(
067          Collector<T, A, R> collector, BiPredicate<? super R, ? super R> equivalence) {
068    return new CollectorTester<>(collector, equivalence);
069  }
070
071  private final Collector<T, A, R> collector;
072  private final BiPredicate<? super R, ? super R> equivalence;
073
074  private CollectorTester(
075      Collector<T, A, R> collector, BiPredicate<? super R, ? super R> equivalence) {
076    this.collector = checkNotNull(collector);
077    this.equivalence = checkNotNull(equivalence);
078  }
079
080  /**
081   * Different orderings for combining the elements of an input array, which must all produce the
082   * same result.
083   */
084  enum CollectStrategy {
085    /** Get one accumulator and accumulate the elements into it sequentially. */
086    SEQUENTIAL {
087      @Override
088      final <T extends @Nullable Object, A extends @Nullable Object, R extends @Nullable Object>
089          A result(Collector<T, A, R> collector, Iterable<T> inputs) {
090        A accum = collector.supplier().get();
091        for (T input : inputs) {
092          collector.accumulator().accept(accum, input);
093        }
094        return accum;
095      }
096    },
097    /** Get one accumulator for each element and merge the accumulators left-to-right. */
098    MERGE_LEFT_ASSOCIATIVE {
099      @Override
100      final <T extends @Nullable Object, A extends @Nullable Object, R extends @Nullable Object>
101          A result(Collector<T, A, R> collector, Iterable<T> inputs) {
102        A accum = collector.supplier().get();
103        for (T input : inputs) {
104          A newAccum = collector.supplier().get();
105          collector.accumulator().accept(newAccum, input);
106          accum = collector.combiner().apply(accum, newAccum);
107        }
108        return accum;
109      }
110    },
111    /** Get one accumulator for each element and merge the accumulators right-to-left. */
112    MERGE_RIGHT_ASSOCIATIVE {
113      @Override
114      final <T extends @Nullable Object, A extends @Nullable Object, R extends @Nullable Object>
115          A result(Collector<T, A, R> collector, Iterable<T> inputs) {
116        List<A> stack = new ArrayList<>();
117        for (T input : inputs) {
118          A newAccum = collector.supplier().get();
119          collector.accumulator().accept(newAccum, input);
120          push(stack, newAccum);
121        }
122        push(stack, collector.supplier().get());
123        while (stack.size() > 1) {
124          A right = pop(stack);
125          A left = pop(stack);
126          push(stack, collector.combiner().apply(left, right));
127        }
128        return pop(stack);
129      }
130
131      <E extends @Nullable Object> void push(List<E> stack, E value) {
132        stack.add(value);
133      }
134
135      <E extends @Nullable Object> E pop(List<E> stack) {
136        return stack.remove(stack.size() - 1);
137      }
138    };
139
140    abstract <T extends @Nullable Object, A extends @Nullable Object, R extends @Nullable Object>
141        A result(Collector<T, A, R> collector, Iterable<T> inputs);
142  }
143
144  /**
145   * Verifies that the specified expected result is always produced by collecting the specified
146   * inputs, regardless of how the elements are divided.
147   */
148  @SafeVarargs
149  @CanIgnoreReturnValue
150  @SuppressWarnings("nullness") // TODO(cpovirk): Remove after we fix whatever the bug is.
151  public final CollectorTester<T, A, R> expectCollects(R expectedResult, T... inputs) {
152    List<T> list = Arrays.asList(inputs);
153    doExpectCollects(expectedResult, list);
154    if (collector.characteristics().contains(Collector.Characteristics.UNORDERED)) {
155      Collections.reverse(list);
156      doExpectCollects(expectedResult, list);
157    }
158    return this;
159  }
160
161  private void doExpectCollects(R expectedResult, List<T> inputs) {
162    for (CollectStrategy scheme : CollectStrategy.values()) {
163      A finalAccum = scheme.result(collector, inputs);
164      if (collector.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) {
165        @SuppressWarnings("unchecked") // `R` and `A` match for an `IDENTITY_FINISH`
166        R result = (R) finalAccum;
167        assertEquivalent(expectedResult, result);
168      }
169      assertEquivalent(expectedResult, collector.finisher().apply(finalAccum));
170    }
171  }
172
173  private void assertEquivalent(R expected, R actual) {
174    assertTrue(
175        "Expected " + expected + " got " + actual + " modulo equivalence " + equivalence,
176        equivalence.test(expected, actual));
177  }
178}