• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2011 Google, Inc.
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 package com.google.common.truth;
17 
18 import static com.google.common.base.Strings.lenientFormat;
19 import static com.google.common.collect.Iterables.isEmpty;
20 import static com.google.common.collect.Iterables.transform;
21 import static com.google.common.collect.Multisets.immutableEntry;
22 
23 import com.google.common.base.Equivalence;
24 import com.google.common.base.Equivalence.Wrapper;
25 import com.google.common.base.Objects;
26 import com.google.common.base.Optional;
27 import com.google.common.collect.ArrayListMultimap;
28 import com.google.common.collect.ImmutableList;
29 import com.google.common.collect.Iterables;
30 import com.google.common.collect.LinkedHashMultiset;
31 import com.google.common.collect.ListMultimap;
32 import com.google.common.collect.Lists;
33 import com.google.common.collect.Multiset;
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.Collection;
37 import java.util.List;
38 import java.util.Map;
39 import org.checkerframework.checker.nullness.qual.Nullable;
40 
41 /**
42  * Utility methods used in {@code Subject} implementors.
43  *
44  * @author Christian Gruber
45  * @author Jens Nyman
46  */
47 final class SubjectUtils {
SubjectUtils()48   private SubjectUtils() {}
49 
50   static final String HUMAN_UNDERSTANDABLE_EMPTY_STRING = "\"\" (empty String)";
51 
accumulate(T first, T second, T @Nullable ... rest)52   static <T extends @Nullable Object> List<T> accumulate(T first, T second, T @Nullable ... rest) {
53     // rest should never be deliberately null, so assume that the caller passed null
54     // in the third position but intended it to be the third element in the array of values.
55     // Javac makes the opposite inference, so handle that here.
56     List<T> items = new ArrayList<>(2 + ((rest == null) ? 1 : rest.length));
57     items.add(first);
58     items.add(second);
59     if (rest == null) {
60       items.add((T) null);
61     } else {
62       items.addAll(Arrays.asList(rest));
63     }
64     return items;
65   }
66 
countOf(T t, Iterable<T> items)67   static <T> int countOf(T t, Iterable<T> items) {
68     int count = 0;
69     for (T item : items) {
70       if (t == null ? (item == null) : t.equals(item)) {
71         count++;
72       }
73     }
74     return count;
75   }
76 
countDuplicates(Iterable<?> items)77   static String countDuplicates(Iterable<?> items) {
78     /*
79      * TODO(cpovirk): Remove brackets after migrating all callers to the new message format. But
80      * will that look OK when we put the result next to a homogeneous type name? If not, maybe move
81      * the homogeneous type name to a separate Fact?
82      */
83     return countDuplicatesToMultiset(items).toStringWithBrackets();
84   }
85 
entryString(Multiset.Entry<?> entry)86   static String entryString(Multiset.Entry<?> entry) {
87     int count = entry.getCount();
88     String item = String.valueOf(entry.getElement());
89     return (count > 1) ? item + " [" + count + " copies]" : item;
90   }
91 
countDuplicatesToMultiset( Iterable<T> items)92   private static <T extends @Nullable Object> NonHashingMultiset<T> countDuplicatesToMultiset(
93       Iterable<T> items) {
94     // We use avoid hashing in case the elements don't have a proper
95     // .hashCode() method (e.g., MessageSet from old versions of protobuf).
96     NonHashingMultiset<T> multiset = new NonHashingMultiset<>();
97     for (T item : items) {
98       multiset.add(item);
99     }
100     return multiset;
101   }
102 
103   /**
104    * Makes a String representation of {@code items} with collapsed duplicates and additional class
105    * info.
106    *
107    * <p>Example: {@code countDuplicatesAndAddTypeInfo([1, 2, 2, 3]) == "[1, 2 [3 copies]]
108    * (java.lang.Integer)"} and {@code countDuplicatesAndAddTypeInfo([1, 2L]) == "[1
109    * (java.lang.Integer), 2 (java.lang.Long)]"}.
110    */
countDuplicatesAndAddTypeInfo(Iterable<?> itemsIterable)111   static String countDuplicatesAndAddTypeInfo(Iterable<?> itemsIterable) {
112     Collection<?> items = iterableToCollection(itemsIterable);
113     Optional<String> homogeneousTypeName = getHomogeneousTypeName(items);
114 
115     return homogeneousTypeName.isPresent()
116         ? lenientFormat("%s (%s)", countDuplicates(items), homogeneousTypeName.get())
117         : countDuplicates(addTypeInfoToEveryItem(items));
118   }
119 
120   /**
121    * Similar to {@link #countDuplicatesAndAddTypeInfo} and {@link #countDuplicates} but (a) only
122    * adds type info if requested and (b) returns a richer object containing the data.
123    */
countDuplicatesAndMaybeAddTypeInfoReturnObject( Iterable<?> itemsIterable, boolean addTypeInfo)124   static DuplicateGroupedAndTyped countDuplicatesAndMaybeAddTypeInfoReturnObject(
125       Iterable<?> itemsIterable, boolean addTypeInfo) {
126     if (addTypeInfo) {
127       Collection<?> items = iterableToCollection(itemsIterable);
128       Optional<String> homogeneousTypeName = getHomogeneousTypeName(items);
129 
130       NonHashingMultiset<?> valuesWithCountsAndMaybeTypes =
131           homogeneousTypeName.isPresent()
132               ? countDuplicatesToMultiset(items)
133               : countDuplicatesToMultiset(addTypeInfoToEveryItem(items));
134       return new DuplicateGroupedAndTyped(valuesWithCountsAndMaybeTypes, homogeneousTypeName);
135     } else {
136       return new DuplicateGroupedAndTyped(
137           countDuplicatesToMultiset(itemsIterable),
138           /* homogeneousTypeToDisplay= */ Optional.<String>absent());
139     }
140   }
141 
142   private static final class NonHashingMultiset<E extends @Nullable Object> {
143     /*
144      * This ought to be static, but the generics are easier when I can refer to <E>. We still want
145      * an Entry<?> so that entrySet() can return Iterable<Entry<?>> instead of Iterable<Entry<E>>.
146      * That way, it can be returned directly from DuplicateGroupedAndTyped.entrySet() without our
147      * having to generalize *its* return type to Iterable<? extends Entry<?>>.
148      */
unwrapKey(Multiset.Entry<Wrapper<E>> input)149     private Multiset.Entry<?> unwrapKey(Multiset.Entry<Wrapper<E>> input) {
150       return immutableEntry(input.getElement().get(), input.getCount());
151     }
152 
153     private final Multiset<Wrapper<E>> contents = LinkedHashMultiset.create();
154 
add(E element)155     void add(E element) {
156       contents.add(EQUALITY_WITHOUT_USING_HASH_CODE.wrap(element));
157     }
158 
totalCopies()159     int totalCopies() {
160       return contents.size();
161     }
162 
isEmpty()163     boolean isEmpty() {
164       return contents.isEmpty();
165     }
166 
entrySet()167     Iterable<Multiset.Entry<?>> entrySet() {
168       return transform(contents.entrySet(), this::unwrapKey);
169     }
170 
toStringWithBrackets()171     String toStringWithBrackets() {
172       List<String> parts = new ArrayList<>();
173       for (Multiset.Entry<?> entry : entrySet()) {
174         parts.add(entryString(entry));
175       }
176       return parts.toString();
177     }
178 
179     @Override
toString()180     public String toString() {
181       String withBrackets = toStringWithBrackets();
182       return withBrackets.substring(1, withBrackets.length() - 1);
183     }
184 
185     private static final Equivalence<Object> EQUALITY_WITHOUT_USING_HASH_CODE =
186         new Equivalence<Object>() {
187           @Override
188           protected boolean doEquivalent(Object a, Object b) {
189             return Objects.equal(a, b);
190           }
191 
192           @Override
193           protected int doHash(Object o) {
194             return 0; // slow but hopefully not much worse than what we get with a flat list
195           }
196         };
197   }
198 
199   /**
200    * Missing or unexpected values from a collection assertion, with equal objects grouped together
201    * and, in some cases, type information added. If the type information is present, it is either
202    * present in {@code homogeneousTypeToDisplay} (if all objects have the same type) or appended to
203    * each individual element (if some elements have different types).
204    *
205    * <p>This allows collection assertions to the type information on a separate line from the
206    * elements and even to output different elements on different lines.
207    */
208   static final class DuplicateGroupedAndTyped {
209     final NonHashingMultiset<?> valuesAndMaybeTypes;
210     final Optional<String> homogeneousTypeToDisplay;
211 
DuplicateGroupedAndTyped( NonHashingMultiset<?> valuesAndMaybeTypes, Optional<String> homogeneousTypeToDisplay)212     DuplicateGroupedAndTyped(
213         NonHashingMultiset<?> valuesAndMaybeTypes, Optional<String> homogeneousTypeToDisplay) {
214       this.valuesAndMaybeTypes = valuesAndMaybeTypes;
215       this.homogeneousTypeToDisplay = homogeneousTypeToDisplay;
216     }
217 
totalCopies()218     int totalCopies() {
219       return valuesAndMaybeTypes.totalCopies();
220     }
221 
isEmpty()222     boolean isEmpty() {
223       return valuesAndMaybeTypes.isEmpty();
224     }
225 
entrySet()226     Iterable<Multiset.Entry<?>> entrySet() {
227       return valuesAndMaybeTypes.entrySet();
228     }
229 
230     @Override
toString()231     public String toString() {
232       return homogeneousTypeToDisplay.isPresent()
233           ? valuesAndMaybeTypes + " (" + homogeneousTypeToDisplay.get() + ")"
234           : valuesAndMaybeTypes.toString();
235     }
236   }
237 
238   /**
239    * Makes a String representation of {@code items} with additional class info.
240    *
241    * <p>Example: {@code iterableToStringWithTypeInfo([1, 2]) == "[1, 2] (java.lang.Integer)"} and
242    * {@code iterableToStringWithTypeInfo([1, 2L]) == "[1 (java.lang.Integer), 2 (java.lang.Long)]"}.
243    */
iterableToStringWithTypeInfo(Iterable<?> itemsIterable)244   static String iterableToStringWithTypeInfo(Iterable<?> itemsIterable) {
245     Collection<?> items = iterableToCollection(itemsIterable);
246     Optional<String> homogeneousTypeName = getHomogeneousTypeName(items);
247 
248     if (homogeneousTypeName.isPresent()) {
249       return lenientFormat("%s (%s)", items, homogeneousTypeName.get());
250     } else {
251       return addTypeInfoToEveryItem(items).toString();
252     }
253   }
254 
255   /**
256    * Returns a new collection containing all elements in {@code items} for which there exists at
257    * least one element in {@code itemsToCheck} that has the same {@code toString()} value without
258    * being equal.
259    *
260    * <p>Example: {@code retainMatchingToString([1L, 2L, 2L], [2, 3]) == [2L, 2L]}
261    */
retainMatchingToString( Iterable<?> items, Iterable<?> itemsToCheck)262   static List<@Nullable Object> retainMatchingToString(
263       Iterable<?> items, Iterable<?> itemsToCheck) {
264     ListMultimap<String, @Nullable Object> stringValueToItemsToCheck = ArrayListMultimap.create();
265     for (Object itemToCheck : itemsToCheck) {
266       stringValueToItemsToCheck.put(String.valueOf(itemToCheck), itemToCheck);
267     }
268 
269     List<@Nullable Object> result = Lists.newArrayList();
270     for (Object item : items) {
271       for (Object itemToCheck : stringValueToItemsToCheck.get(String.valueOf(item))) {
272         if (!Objects.equal(itemToCheck, item)) {
273           result.add(item);
274           break;
275         }
276       }
277     }
278     return result;
279   }
280 
281   /**
282    * Returns true if there is a pair of an item from {@code items1} and one in {@code items2} that
283    * has the same {@code toString()} value without being equal.
284    *
285    * <p>Example: {@code hasMatchingToStringPair([1L, 2L], [1]) == true}
286    */
hasMatchingToStringPair(Iterable<?> items1, Iterable<?> items2)287   static boolean hasMatchingToStringPair(Iterable<?> items1, Iterable<?> items2) {
288     if (isEmpty(items1) || isEmpty(items2)) {
289       return false; // Bail early to avoid calling hashCode() on the elements unnecessarily.
290     }
291     return !retainMatchingToString(items1, items2).isEmpty();
292   }
293 
objectToTypeName(@ullable Object item)294   static String objectToTypeName(@Nullable Object item) {
295     // TODO(cpovirk): Merge this with the code in Subject.failEqualityCheck().
296     if (item == null) {
297       // The name "null type" comes from the interface javax.lang.model.type.NullType.
298       return "null type";
299     } else if (item instanceof Map.Entry) {
300       Map.Entry<?, ?> entry = (Map.Entry<?, ?>) item;
301       // Fix for interesting bug when entry.getValue() returns itself b/170390717
302       String valueTypeName =
303           entry.getValue() == entry ? "Map.Entry" : objectToTypeName(entry.getValue());
304 
305       return lenientFormat("Map.Entry<%s, %s>", objectToTypeName(entry.getKey()), valueTypeName);
306     } else {
307       return item.getClass().getName();
308     }
309   }
310 
311   /**
312    * Returns the name of the single type of all given items or {@link Optional#absent()} if no such
313    * type exists.
314    */
getHomogeneousTypeName(Iterable<?> items)315   private static Optional<String> getHomogeneousTypeName(Iterable<?> items) {
316     Optional<String> homogeneousTypeName = Optional.absent();
317     for (Object item : items) {
318       if (item == null) {
319         /*
320          * TODO(cpovirk): Why? We could have multiple nulls, which would be homogeneous. More
321          * likely, we could have exactly one null, which is still homogeneous. Arguably it's weird
322          * to call a single element "homogeneous" at all, but that's not specific to null.
323          */
324         return Optional.absent();
325       } else if (!homogeneousTypeName.isPresent()) {
326         // This is the first item
327         homogeneousTypeName = Optional.of(objectToTypeName(item));
328       } else if (!objectToTypeName(item).equals(homogeneousTypeName.get())) {
329         // items is a heterogeneous collection
330         return Optional.absent();
331       }
332     }
333     return homogeneousTypeName;
334   }
335 
addTypeInfoToEveryItem(Iterable<?> items)336   private static List<String> addTypeInfoToEveryItem(Iterable<?> items) {
337     List<String> itemsWithTypeInfo = Lists.newArrayList();
338     for (Object item : items) {
339       itemsWithTypeInfo.add(lenientFormat("%s (%s)", item, objectToTypeName(item)));
340     }
341     return itemsWithTypeInfo;
342   }
343 
iterableToCollection(Iterable<T> iterable)344   static <T extends @Nullable Object> Collection<T> iterableToCollection(Iterable<T> iterable) {
345     if (iterable instanceof Collection) {
346       // Should be safe to assume that any Iterable implementing Collection isn't a one-shot
347       // iterable, right? I sure hope so.
348       return (Collection<T>) iterable;
349     } else {
350       return Lists.newArrayList(iterable);
351     }
352   }
353 
iterableToList(Iterable<T> iterable)354   static <T extends @Nullable Object> List<T> iterableToList(Iterable<T> iterable) {
355     if (iterable instanceof List) {
356       return (List<T>) iterable;
357     } else {
358       return Lists.newArrayList(iterable);
359     }
360   }
361 
362   /**
363    * Returns an iterable with all empty strings replaced by a non-empty human understandable
364    * indicator for an empty string.
365    *
366    * <p>Returns the given iterable if it contains no empty strings.
367    */
annotateEmptyStrings(Iterable<T> items)368   static <T extends @Nullable Object> Iterable<T> annotateEmptyStrings(Iterable<T> items) {
369     if (Iterables.contains(items, "")) {
370       List<T> annotatedItems = Lists.newArrayList();
371       for (T item : items) {
372         if (Objects.equal(item, "")) {
373           // This is a safe cast because know that at least one instance of T (this item) is a
374           // String.
375           @SuppressWarnings("unchecked")
376           T newItem = (T) HUMAN_UNDERSTANDABLE_EMPTY_STRING;
377           annotatedItems.add(newItem);
378         } else {
379           annotatedItems.add(item);
380         }
381       }
382       return annotatedItems;
383     } else {
384       return items;
385     }
386   }
387 
388   @SafeVarargs
concat(Iterable<? extends E>.... inputs)389   static <E> ImmutableList<E> concat(Iterable<? extends E>... inputs) {
390     return ImmutableList.copyOf(Iterables.concat(inputs));
391   }
392 
append(E[] array, E object)393   static <E> ImmutableList<E> append(E[] array, E object) {
394     return new ImmutableList.Builder<E>().add(array).add(object).build();
395   }
396 
append(ImmutableList<? extends E> list, E object)397   static <E> ImmutableList<E> append(ImmutableList<? extends E> list, E object) {
398     return new ImmutableList.Builder<E>().addAll(list).add(object).build();
399   }
400 
sandwich(E first, E[] array, E last)401   static <E> ImmutableList<E> sandwich(E first, E[] array, E last) {
402     return new ImmutableList.Builder<E>().add(first).add(array).add(last).build();
403   }
404 }
405