1 /* 2 * Copyright (C) 2015 The Android Open Source Project 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.android.tv.testing; 18 19 import static com.google.common.truth.Truth.assertWithMessage; 20 import static com.google.common.truth.Truth.assert_; 21 22 import android.support.annotation.Nullable; 23 24 import com.google.common.base.Preconditions; 25 import com.google.common.collect.ImmutableList; 26 import com.google.common.collect.Lists; 27 import com.google.common.primitives.Ints; 28 29 import java.util.Comparator; 30 import java.util.List; 31 32 /** 33 * Tests that a given {@link Comparator} (or the implementation of {@link Comparable}) is correct. 34 * To use, repeatedly call {@link #addEqualityGroup(Object...)} with sets of objects that should be 35 * equal. The calls to {@link #addEqualityGroup(Object...)} must be made in sorted order. Then call 36 * {@link #testCompare()} to test the comparison. For example: 37 * 38 * <pre>{@code 39 * new ComparatorTester() 40 * .addEqualityGroup(1) 41 * .addEqualityGroup(2) 42 * .addEqualityGroup(3) 43 * .testCompare(); 44 * }</pre> 45 * 46 * <p>By default, a {@code Comparator} is not tested for compatibility with {@link 47 * Object#equals(Object)}. If that is desired, use the {link #requireConsistencyWithEquals()} to 48 * explicitly activate the check. For example: 49 * 50 * <pre>{@code 51 * new ComparatorTester(Comparator.naturalOrder()) 52 * .requireConsistencyWithEquals() 53 * .addEqualityGroup(1) 54 * .addEqualityGroup(2) 55 * .addEqualityGroup(3) 56 * .testCompare(); 57 * }</pre> 58 * 59 * <p>If for some reason you need to suppress the compatibility check when testing a {@code 60 * Comparable}, use the {link #permitInconsistencyWithEquals()} to explicitly deactivate the check. 61 * For example: 62 * 63 * <pre>{@code 64 * new ComparatorTester() 65 * .permitInconsistencyWithEquals() 66 * .addEqualityGroup(1) 67 * .addEqualityGroup(2) 68 * .addEqualityGroup(3) 69 * .testCompare(); 70 * }</pre> 71 */ 72 public class ComparatorTester { 73 @SuppressWarnings({"unchecked", "rawtypes"}) 74 @Nullable 75 private final Comparator comparator; 76 77 /** The items that we are checking, stored as a sorted set of equivalence classes. */ 78 private final List<List<Object>> equalityGroups; 79 80 /** Whether to enforce a.equals(b) == (a.compareTo(b) == 0) */ 81 private boolean testForEqualsCompatibility; 82 83 /** 84 * Creates a new instance that tests the order of objects using the natural order (as defined by 85 * {@link Comparable}). 86 */ ComparatorTester()87 public ComparatorTester() { 88 this(null); 89 } 90 91 /** 92 * Creates a new instance that tests the order of objects using the given comparator. Or, if the 93 * comparator is {@code null}, the natural ordering (as defined by {@link Comparable}) 94 */ ComparatorTester(@ullable Comparator<?> comparator)95 public ComparatorTester(@Nullable Comparator<?> comparator) { 96 this.equalityGroups = Lists.newArrayList(); 97 this.comparator = comparator; 98 this.testForEqualsCompatibility = (this.comparator == null); 99 } 100 101 /** 102 * Activates enforcement of {@code a.equals(b) == (a.compareTo(b) == 0)}. This is off by default 103 * when testing {@link Comparator}s, but can be turned on if required. 104 */ requireConsistencyWithEquals()105 public ComparatorTester requireConsistencyWithEquals() { 106 testForEqualsCompatibility = true; 107 return this; 108 } 109 110 /** 111 * Deactivates enforcement of {@code a.equals(b) == (a.compareTo(b) == 0)}. This is on by 112 * default when testing {@link Comparable}s, but can be turned off if required. 113 */ permitInconsistencyWithEquals()114 public ComparatorTester permitInconsistencyWithEquals() { 115 testForEqualsCompatibility = false; 116 return this; 117 } 118 119 /** 120 * Adds a set of objects to the test which should all compare as equal. All of the elements in 121 * {@code objects} must be greater than any element of {@code objects} in a previous call to 122 * {@link #addEqualityGroup(Object...)}. 123 * 124 * @return {@code this} (to allow chaining of calls) 125 */ addEqualityGroup(Object... objects)126 public ComparatorTester addEqualityGroup(Object... objects) { 127 Preconditions.checkNotNull(objects); 128 Preconditions.checkArgument(objects.length > 0, "Array must not be empty"); 129 equalityGroups.add(ImmutableList.copyOf(objects)); 130 return this; 131 } 132 133 @SuppressWarnings({"unchecked"}) compare(Object a, Object b)134 private int compare(Object a, Object b) { 135 int compareValue; 136 if (comparator == null) { 137 compareValue = ((Comparable<Object>) a).compareTo(b); 138 } else { 139 compareValue = comparator.compare(a, b); 140 } 141 return compareValue; 142 } 143 testCompare()144 public final void testCompare() { 145 doTestEquivalanceGroupOrdering(); 146 if (testForEqualsCompatibility) { 147 doTestEqualsCompatibility(); 148 } 149 } 150 doTestEquivalanceGroupOrdering()151 private final void doTestEquivalanceGroupOrdering() { 152 for (int referenceIndex = 0; referenceIndex < equalityGroups.size(); referenceIndex++) { 153 for (Object reference : equalityGroups.get(referenceIndex)) { 154 testNullCompare(reference); 155 testClassCast(reference); 156 for (int otherIndex = 0; otherIndex < equalityGroups.size(); otherIndex++) { 157 for (Object other : equalityGroups.get(otherIndex)) { 158 assertWithMessage("compare(%s, %s)", reference, other) 159 .that(Integer.signum(compare(reference, other))) 160 .isEqualTo( 161 Integer.signum(Ints.compare(referenceIndex, otherIndex))); 162 } 163 } 164 } 165 } 166 } 167 doTestEqualsCompatibility()168 private final void doTestEqualsCompatibility() { 169 for (List<Object> referenceGroup : equalityGroups) { 170 for (Object reference : referenceGroup) { 171 for (List<Object> otherGroup : equalityGroups) { 172 for (Object other : otherGroup) { 173 assertWithMessage( 174 "Testing equals() for compatibility with" 175 + " compare()/compareTo(), add a call to" 176 + " doNotRequireEqualsCompatibility() if this is not" 177 + " required") 178 .withMessage("%s.equals(%s)", reference, other) 179 .that(reference.equals(other)) 180 .isEqualTo(compare(reference, other) == 0); 181 } 182 } 183 } 184 } 185 } 186 testNullCompare(Object obj)187 private void testNullCompare(Object obj) { 188 // Comparator does not require any specific behavior for null. 189 if (comparator == null) { 190 try { 191 compare(obj, null); 192 assert_().fail("Expected NullPointerException in %s.compare(null)", obj); 193 } catch (NullPointerException expected) { 194 // TODO(cpovirk): Consider accepting JavaScriptException under GWT 195 } 196 } 197 } 198 199 @SuppressWarnings("unchecked") testClassCast(Object obj)200 private void testClassCast(Object obj) { 201 if (comparator == null) { 202 try { 203 compare(obj, ICanNotBeCompared.INSTANCE); 204 assert_().fail("Expected ClassCastException in %s.compareTo(otherObject)", obj); 205 } catch (ClassCastException expected) { 206 } 207 } 208 } 209 210 private static final class ICanNotBeCompared { 211 static final ComparatorTester.ICanNotBeCompared INSTANCE = new ICanNotBeCompared(); 212 } 213 } 214