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