1 /*
2  * Copyright (C) 2015 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.testing;
18 
19 import static com.google.common.base.Preconditions.checkNotNull;
20 import static junit.framework.Assert.assertTrue;
21 
22 import com.google.common.annotations.Beta;
23 import com.google.common.annotations.GwtCompatible;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collections;
27 import java.util.EnumSet;
28 import java.util.List;
29 import java.util.Objects;
30 import java.util.function.BiPredicate;
31 import java.util.stream.Collector;
32 import org.checkerframework.checker.nullness.qual.Nullable;
33 
34 /**
35  * Tester for {@code Collector} implementations.
36  *
37  * <p>Example usage:
38  *
39  * <pre>
40  * CollectorTester.of(Collectors.summingInt(Integer::parseInt))
41  *     .expectCollects(3, "1", "2")
42  *     .expectCollects(10, "1", "4", "3", "2")
43  *     .expectCollects(5, "-3", "0", "8");
44  * </pre>
45  *
46  * @author Louis Wasserman
47  * @since 21.0
48  */
49 @Beta
50 @GwtCompatible
51 public final class CollectorTester<T, A, R> {
52   /**
53    * Creates a {@code CollectorTester} for the specified {@code Collector}. The result of the {@code
54    * Collector} will be compared to the expected value using {@link Object.equals}.
55    */
of(Collector<T, A, R> collector)56   public static <T, A, R> CollectorTester<T, A, R> of(Collector<T, A, R> collector) {
57     return of(collector, Objects::equals);
58   }
59 
60   /**
61    * Creates a {@code CollectorTester} for the specified {@code Collector}. The result of the {@code
62    * Collector} will be compared to the expected value using the specified {@code equivalence}.
63    */
of( Collector<T, A, R> collector, BiPredicate<? super R, ? super R> equivalence)64   public static <T, A, R> CollectorTester<T, A, R> of(
65       Collector<T, A, R> collector, BiPredicate<? super R, ? super R> equivalence) {
66     return new CollectorTester<>(collector, equivalence);
67   }
68 
69   private final Collector<T, A, R> collector;
70   private final BiPredicate<? super R, ? super R> equivalence;
71 
CollectorTester( Collector<T, A, R> collector, BiPredicate<? super R, ? super R> equivalence)72   private CollectorTester(
73       Collector<T, A, R> collector, BiPredicate<? super R, ? super R> equivalence) {
74     this.collector = checkNotNull(collector);
75     this.equivalence = checkNotNull(equivalence);
76   }
77 
78   /**
79    * Different orderings for combining the elements of an input array, which must all produce the
80    * same result.
81    */
82   enum CollectStrategy {
83     /** Get one accumulator and accumulate the elements into it sequentially. */
84     SEQUENTIAL {
85       @Override
result(Collector<T, A, R> collector, Iterable<T> inputs)86       final <T, A, R> A result(Collector<T, A, R> collector, Iterable<T> inputs) {
87         A accum = collector.supplier().get();
88         for (T input : inputs) {
89           collector.accumulator().accept(accum, input);
90         }
91         return accum;
92       }
93     },
94     /** Get one accumulator for each element and merge the accumulators left-to-right. */
95     MERGE_LEFT_ASSOCIATIVE {
96       @Override
result(Collector<T, A, R> collector, Iterable<T> inputs)97       final <T, A, R> A result(Collector<T, A, R> collector, Iterable<T> inputs) {
98         A accum = collector.supplier().get();
99         for (T input : inputs) {
100           A newAccum = collector.supplier().get();
101           collector.accumulator().accept(newAccum, input);
102           accum = collector.combiner().apply(accum, newAccum);
103         }
104         return accum;
105       }
106     },
107     /** Get one accumulator for each element and merge the accumulators right-to-left. */
108     MERGE_RIGHT_ASSOCIATIVE {
109       @Override
result(Collector<T, A, R> collector, Iterable<T> inputs)110       final <T, A, R> A result(Collector<T, A, R> collector, Iterable<T> inputs) {
111         List<A> stack = new ArrayList<>();
112         for (T input : inputs) {
113           A newAccum = collector.supplier().get();
114           collector.accumulator().accept(newAccum, input);
115           push(stack, newAccum);
116         }
117         push(stack, collector.supplier().get());
118         while (stack.size() > 1) {
119           A right = pop(stack);
120           A left = pop(stack);
121           push(stack, collector.combiner().apply(left, right));
122         }
123         return pop(stack);
124       }
125 
push(List<E> stack, E value)126       <E> void push(List<E> stack, E value) {
127         stack.add(value);
128       }
129 
pop(List<E> stack)130       <E> E pop(List<E> stack) {
131         return stack.remove(stack.size() - 1);
132       }
133     };
134 
result(Collector<T, A, R> collector, Iterable<T> inputs)135     abstract <T, A, R> A result(Collector<T, A, R> collector, Iterable<T> inputs);
136   }
137 
138   /**
139    * Verifies that the specified expected result is always produced by collecting the specified
140    * inputs, regardless of how the elements are divided.
141    */
142   @SafeVarargs
expectCollects(@ullable R expectedResult, T... inputs)143   public final CollectorTester<T, A, R> expectCollects(@Nullable R expectedResult, T... inputs) {
144     List<T> list = Arrays.asList(inputs);
145     doExpectCollects(expectedResult, list);
146     if (collector.characteristics().contains(Collector.Characteristics.UNORDERED)) {
147       Collections.reverse(list);
148       doExpectCollects(expectedResult, list);
149     }
150     return this;
151   }
152 
doExpectCollects(@ullable R expectedResult, List<T> inputs)153   private void doExpectCollects(@Nullable R expectedResult, List<T> inputs) {
154     for (CollectStrategy scheme : EnumSet.allOf(CollectStrategy.class)) {
155       A finalAccum = scheme.result(collector, inputs);
156       if (collector.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) {
157         assertEquivalent(expectedResult, (R) finalAccum);
158       }
159       assertEquivalent(expectedResult, collector.finisher().apply(finalAccum));
160     }
161   }
162 
assertEquivalent(@ullable R expected, @Nullable R actual)163   private void assertEquivalent(@Nullable R expected, @Nullable R actual) {
164     assertTrue(
165         "Expected " + expected + " got " + actual + " modulo equivalence " + equivalence,
166         equivalence.test(expected, actual));
167   }
168 }
169