aboutsummaryrefslogtreecommitdiff
path: root/tests/common/src/com/android/tv/testing/ComparatorTester.java
blob: 01c3964de7214465235a2cd999fd6befd828f4ac (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.tv.testing;

import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.Truth.assert_;

import android.support.annotation.Nullable;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;

import java.util.Comparator;
import java.util.List;

/**
 * Tests that a given {@link Comparator} (or the implementation of {@link Comparable}) is correct.
 * To use, repeatedly call {@link #addEqualityGroup(Object...)} with sets of objects that should be
 * equal. The calls to {@link #addEqualityGroup(Object...)} must be made in sorted order. Then call
 * {@link #testCompare()} to test the comparison. For example:
 *
 * <pre>{@code
 * new ComparatorTester()
 *     .addEqualityGroup(1)
 *     .addEqualityGroup(2)
 *     .addEqualityGroup(3)
 *     .testCompare();
 * }</pre>
 *
 * <p>By default, a {@code Comparator} is not tested for compatibility with {@link
 * Object#equals(Object)}. If that is desired, use the {link #requireConsistencyWithEquals()} to
 * explicitly activate the check. For example:
 *
 * <pre>{@code
 * new ComparatorTester(Comparator.naturalOrder())
 *     .requireConsistencyWithEquals()
 *     .addEqualityGroup(1)
 *     .addEqualityGroup(2)
 *     .addEqualityGroup(3)
 *     .testCompare();
 * }</pre>
 *
 * <p>If for some reason you need to suppress the compatibility check when testing a {@code
 * Comparable}, use the {link #permitInconsistencyWithEquals()} to explicitly deactivate the check.
 * For example:
 *
 * <pre>{@code
 * new ComparatorTester()
 *     .permitInconsistencyWithEquals()
 *     .addEqualityGroup(1)
 *     .addEqualityGroup(2)
 *     .addEqualityGroup(3)
 *     .testCompare();
 * }</pre>
 */
public class ComparatorTester {
    @SuppressWarnings({"unchecked", "rawtypes"})
    @Nullable
    private final Comparator comparator;

    /** The items that we are checking, stored as a sorted set of equivalence classes. */
    private final List<List<Object>> equalityGroups;

    /** Whether to enforce a.equals(b) == (a.compareTo(b) == 0) */
    private boolean testForEqualsCompatibility;

    /**
     * Creates a new instance that tests the order of objects using the natural order (as defined by
     * {@link Comparable}).
     */
    public ComparatorTester() {
        this(null);
    }

    /**
     * Creates a new instance that tests the order of objects using the given comparator. Or, if the
     * comparator is {@code null}, the natural ordering (as defined by {@link Comparable})
     */
    public ComparatorTester(@Nullable Comparator<?> comparator) {
        this.equalityGroups = Lists.newArrayList();
        this.comparator = comparator;
        this.testForEqualsCompatibility = (this.comparator == null);
    }

    /**
     * Activates enforcement of {@code a.equals(b) == (a.compareTo(b) == 0)}. This is off by default
     * when testing {@link Comparator}s, but can be turned on if required.
     */
    public ComparatorTester requireConsistencyWithEquals() {
        testForEqualsCompatibility = true;
        return this;
    }

    /**
     * Deactivates enforcement of {@code a.equals(b) == (a.compareTo(b) == 0)}. This is on by
     * default when testing {@link Comparable}s, but can be turned off if required.
     */
    public ComparatorTester permitInconsistencyWithEquals() {
        testForEqualsCompatibility = false;
        return this;
    }

    /**
     * Adds a set of objects to the test which should all compare as equal. All of the elements in
     * {@code objects} must be greater than any element of {@code objects} in a previous call to
     * {@link #addEqualityGroup(Object...)}.
     *
     * @return {@code this} (to allow chaining of calls)
     */
    public ComparatorTester addEqualityGroup(Object... objects) {
        Preconditions.checkNotNull(objects);
        Preconditions.checkArgument(objects.length > 0, "Array must not be empty");
        equalityGroups.add(ImmutableList.copyOf(objects));
        return this;
    }

    @SuppressWarnings({"unchecked"})
    private int compare(Object a, Object b) {
        int compareValue;
        if (comparator == null) {
            compareValue = ((Comparable<Object>) a).compareTo(b);
        } else {
            compareValue = comparator.compare(a, b);
        }
        return compareValue;
    }

    public final void testCompare() {
        doTestEquivalanceGroupOrdering();
        if (testForEqualsCompatibility) {
            doTestEqualsCompatibility();
        }
    }

    private final void doTestEquivalanceGroupOrdering() {
        for (int referenceIndex = 0; referenceIndex < equalityGroups.size(); referenceIndex++) {
            for (Object reference : equalityGroups.get(referenceIndex)) {
                testNullCompare(reference);
                testClassCast(reference);
                for (int otherIndex = 0; otherIndex < equalityGroups.size(); otherIndex++) {
                    for (Object other : equalityGroups.get(otherIndex)) {
                        assertWithMessage("compare(%s, %s)", reference, other)
                                .that(Integer.signum(compare(reference, other)))
                                .isEqualTo(
                                        Integer.signum(Ints.compare(referenceIndex, otherIndex)));
                    }
                }
            }
        }
    }

    private final void doTestEqualsCompatibility() {
        for (List<Object> referenceGroup : equalityGroups) {
            for (Object reference : referenceGroup) {
                for (List<Object> otherGroup : equalityGroups) {
                    for (Object other : otherGroup) {
                        assertWithMessage(
                                        "Testing equals() for compatibility with"
                                            + " compare()/compareTo(), add a call to"
                                            + " doNotRequireEqualsCompatibility() if this is not"
                                            + " required")
                                .withMessage("%s.equals(%s)", reference, other)
                                .that(reference.equals(other))
                                .isEqualTo(compare(reference, other) == 0);
                    }
                }
            }
        }
    }

    private void testNullCompare(Object obj) {
        // Comparator does not require any specific behavior for null.
        if (comparator == null) {
            try {
                compare(obj, null);
                assert_().fail("Expected NullPointerException in %s.compare(null)", obj);
            } catch (NullPointerException expected) {
                // TODO(cpovirk): Consider accepting JavaScriptException under GWT
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void testClassCast(Object obj) {
        if (comparator == null) {
            try {
                compare(obj, ICanNotBeCompared.INSTANCE);
                assert_().fail("Expected ClassCastException in %s.compareTo(otherObject)", obj);
            } catch (ClassCastException expected) {
            }
        }
    }

    private static final class ICanNotBeCompared {
        static final ComparatorTester.ICanNotBeCompared INSTANCE = new ICanNotBeCompared();
    }
}