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.jspecify.annotations.NullMarked; 032import org.jspecify.annotations.Nullable; 033 034/** 035 * Tester for {@code Collector} implementations. 036 * 037 * <p>Example usage: 038 * 039 * <pre> 040 * CollectorTester.of(Collectors.summingInt(Integer::parseInt)) 041 * .expectCollects(3, "1", "2") 042 * .expectCollects(10, "1", "4", "3", "2") 043 * .expectCollects(5, "-3", "0", "8"); 044 * </pre> 045 * 046 * @author Louis Wasserman 047 * @since 21.0 048 */ 049@GwtCompatible 050@NullMarked 051public final class CollectorTester< 052 T extends @Nullable Object, A extends @Nullable Object, R extends @Nullable Object> { 053 /** 054 * Creates a {@code CollectorTester} for the specified {@code Collector}. The result of the {@code 055 * Collector} will be compared to the expected value using {@link Object#equals}. 056 */ 057 public static <T extends @Nullable Object, A extends @Nullable Object, R extends @Nullable Object> 058 CollectorTester<T, A, R> of(Collector<T, A, R> collector) { 059 return of(collector, Objects::equals); 060 } 061 062 /** 063 * Creates a {@code CollectorTester} for the specified {@code Collector}. The result of the {@code 064 * Collector} will be compared to the expected value using the specified {@code equivalence}. 065 */ 066 public static <T extends @Nullable Object, A extends @Nullable Object, R extends @Nullable Object> 067 CollectorTester<T, A, R> of( 068 Collector<T, A, R> collector, BiPredicate<? super R, ? super R> equivalence) { 069 return new CollectorTester<>(collector, equivalence); 070 } 071 072 private final Collector<T, A, R> collector; 073 private final BiPredicate<? super R, ? super R> equivalence; 074 075 private CollectorTester( 076 Collector<T, A, R> collector, BiPredicate<? super R, ? super R> equivalence) { 077 this.collector = checkNotNull(collector); 078 this.equivalence = checkNotNull(equivalence); 079 } 080 081 /** 082 * Different orderings for combining the elements of an input array, which must all produce the 083 * same result. 084 */ 085 enum CollectStrategy { 086 /** Get one accumulator and accumulate the elements into it sequentially. */ 087 SEQUENTIAL { 088 @Override 089 final <T extends @Nullable Object, A extends @Nullable Object, R extends @Nullable Object> 090 A result(Collector<T, A, R> collector, Iterable<T> inputs) { 091 A accum = collector.supplier().get(); 092 for (T input : inputs) { 093 collector.accumulator().accept(accum, input); 094 } 095 return accum; 096 } 097 }, 098 /** Get one accumulator for each element and merge the accumulators left-to-right. */ 099 MERGE_LEFT_ASSOCIATIVE { 100 @Override 101 final <T extends @Nullable Object, A extends @Nullable Object, R extends @Nullable Object> 102 A result(Collector<T, A, R> collector, Iterable<T> inputs) { 103 A accum = collector.supplier().get(); 104 for (T input : inputs) { 105 A newAccum = collector.supplier().get(); 106 collector.accumulator().accept(newAccum, input); 107 accum = collector.combiner().apply(accum, newAccum); 108 } 109 return accum; 110 } 111 }, 112 /** Get one accumulator for each element and merge the accumulators right-to-left. */ 113 MERGE_RIGHT_ASSOCIATIVE { 114 @Override 115 final <T extends @Nullable Object, A extends @Nullable Object, R extends @Nullable Object> 116 A result(Collector<T, A, R> collector, Iterable<T> inputs) { 117 List<A> stack = new ArrayList<>(); 118 for (T input : inputs) { 119 A newAccum = collector.supplier().get(); 120 collector.accumulator().accept(newAccum, input); 121 push(stack, newAccum); 122 } 123 push(stack, collector.supplier().get()); 124 while (stack.size() > 1) { 125 A right = pop(stack); 126 A left = pop(stack); 127 push(stack, collector.combiner().apply(left, right)); 128 } 129 return pop(stack); 130 } 131 132 <E extends @Nullable Object> void push(List<E> stack, E value) { 133 stack.add(value); 134 } 135 136 <E extends @Nullable Object> E pop(List<E> stack) { 137 return stack.remove(stack.size() - 1); 138 } 139 }; 140 141 abstract <T extends @Nullable Object, A extends @Nullable Object, R extends @Nullable Object> 142 A result(Collector<T, A, R> collector, Iterable<T> inputs); 143 } 144 145 /** 146 * Verifies that the specified expected result is always produced by collecting the specified 147 * inputs, regardless of how the elements are divided. 148 */ 149 @SafeVarargs 150 @CanIgnoreReturnValue 151 @SuppressWarnings("nullness") // TODO(cpovirk): Remove after we fix whatever the bug is. 152 public final CollectorTester<T, A, R> expectCollects(R expectedResult, T... inputs) { 153 List<T> list = Arrays.asList(inputs); 154 doExpectCollects(expectedResult, list); 155 if (collector.characteristics().contains(Collector.Characteristics.UNORDERED)) { 156 Collections.reverse(list); 157 doExpectCollects(expectedResult, list); 158 } 159 return this; 160 } 161 162 private void doExpectCollects(R expectedResult, List<T> inputs) { 163 for (CollectStrategy scheme : CollectStrategy.values()) { 164 A finalAccum = scheme.result(collector, inputs); 165 if (collector.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) { 166 @SuppressWarnings("unchecked") // `R` and `A` match for an `IDENTITY_FINISH` 167 R result = (R) finalAccum; 168 assertEquivalent(expectedResult, result); 169 } 170 assertEquivalent(expectedResult, collector.finisher().apply(finalAccum)); 171 } 172 } 173 174 private void assertEquivalent(R expected, R actual) { 175 assertTrue( 176 "Expected " + expected + " got " + actual + " modulo equivalence " + equivalence, 177 equivalence.test(expected, actual)); 178 } 179}