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