aboutsummaryrefslogtreecommitdiff
path: root/core/src/test/java/com/google/common/truth/TestCorrespondences.java
blob: d9cca3e1bfc796a54d2e2ddc71387eae997b3a23 (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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
/*
 * Copyright (c) 2011 Google, Inc.
 *
 * 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.google.common.truth;

import static com.google.common.base.Preconditions.checkState;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Splitter;
import com.google.common.primitives.Ints;
import java.util.List;
import org.checkerframework.checker.nullness.qual.Nullable;

/** {@link Correspondence} implementations for testing purposes. */
final class TestCorrespondences {
  /**
   * A correspondence between strings and integers which tests whether the string parses as the
   * integer. Parsing is as specified by {@link Integer#decode(String)}. It considers null to
   * correspond to null only.
   */
  static final Correspondence<String, Integer> STRING_PARSES_TO_INTEGER_CORRESPONDENCE =
      Correspondence.from(
          // If we were allowed to use method references, this would be:
          // TestCorrespondences::stringParsesToInteger,
          new Correspondence.BinaryPredicate<String, Integer>() {
            @Override
            public boolean apply(@Nullable String actual, @Nullable Integer expected) {
              return stringParsesToInteger(actual, expected);
            }
          },
          "parses to");

  private static boolean stringParsesToInteger(
      @Nullable String actual, @Nullable Integer expected) {
    if (actual == null) {
      return expected == null;
    }
    try {
      // Older versions of Android reject leading plus signs, per the pre-Java-7 contract:
      // https://docs.oracle.com/javase/6/docs/api/java/lang/Integer.html#decode(java.lang.String)
      // https://docs.oracle.com/javase/7/docs/api/java/lang/Integer.html#decode(java.lang.String)
      if (actual.startsWith("+")) {
        actual = actual.substring(1);
      }
      return Integer.decode(actual).equals(expected);
    } catch (NumberFormatException e) {
      return false;
    }
  }

  /** A formatter for the diffs between integers. */
  static final Correspondence.DiffFormatter<Integer, Integer> INT_DIFF_FORMATTER =
      // If we were allowed to use lambdas, this would be:
      // (a, e) -> Integer.toString(a - e));
      new Correspondence.DiffFormatter<Integer, Integer>() {
        @Override
        public String formatDiff(Integer actual, Integer expected) {
          return Integer.toString(actual - expected);
        }
      };

  /**
   * A correspondence between integers which tests whether they are within 10 of each other. Smart
   * diffing is enabled, with a formatted diff showing the actual value less the expected value.
   * Does not support null values.
   */
  static final Correspondence<Integer, Integer> WITHIN_10_OF =
      Correspondence.from(
              // If we were allowed to use lambdas, this would be:
              // (Integer a, Integer e) -> Math.abs(a - e) <= 10,
              new Correspondence.BinaryPredicate<Integer, Integer>() {
                @Override
                public boolean apply(Integer actual, Integer expected) {
                  return Math.abs(actual - expected) <= 10;
                }
              },
              "is within 10 of")
          .formattingDiffsUsing(INT_DIFF_FORMATTER);

  /**
   * A correspondence between strings which tests for case-insensitive equality. Supports null
   * expected elements, but throws {@link NullPointerException} on null actual elements.
   */
  static final Correspondence<String, String> CASE_INSENSITIVE_EQUALITY =
      Correspondence.from(
          // If we were allowed to use method references, this would be String::equalsIgnoreCase.
          new Correspondence.BinaryPredicate<String, String>() {
            @Override
            public boolean apply(String actual, String expected) {
              return actual.equalsIgnoreCase(expected);
            }
          },
          "equals (ignoring case)");

  /**
   * A correspondence between strings which tests for case-insensitive equality, with a broken
   * attempt at null-safety. The {@link Correspondence#compare} implementation returns true for
   * (null, null) and false for (non-null, null), but throws {@link NullPointerException} for (null,
   * non-null).
   */
  static final Correspondence<String, String> CASE_INSENSITIVE_EQUALITY_HALF_NULL_SAFE =
      Correspondence.from(
          // If we were allowed to use method references, this would be:
          // TestCorrespondences::equalsIgnoreCaseHalfNullSafe,
          new Correspondence.BinaryPredicate<String, String>() {
            @Override
            public boolean apply(String actual, String expected) {
              return equalsIgnoreCaseHalfNullSafe(actual, expected);
            }
          },
          "equals (ignoring case)");

  private static boolean equalsIgnoreCaseHalfNullSafe(String actual, String expected) {
    if (actual == null && expected == null) {
      return true;
    }
    // Oops! We don't handle the case where actual == null but expected != null.
    return actual.equalsIgnoreCase(expected);
  }

  /**
   * An example value object. It has an optional {@code id} field and a required {@code score}
   * field, both positive integers.
   */
  static final class Record {
    private final int id;
    private final int score;

    static Record create(int id, int score) {
      checkState(id >= 0);
      checkState(score > 0);
      return new Record(id, score);
    }

    static Record createWithoutId(int score) {
      checkState(score >= 0);
      return new Record(-1, score);
    }

    Record(int id, int score) {
      this.id = id;
      this.score = score;
    }

    boolean hasId() {
      return id >= 0;
    }

    int getId() {
      checkState(hasId());
      return id;
    }

    int getScore() {
      return score;
    }

    boolean hasSameId(Record that) {
      return this.id == that.id;
    }

    @Override
    public boolean equals(@Nullable Object o) {
      if (o instanceof Record) {
        Record that = (Record) o;
        return this.id == that.id && this.score == that.score;
      }
      return false;
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(id, score);
    }

    /**
     * Returns the string form of the record, which is the {@code id} value or the literal {@code
     * none} if none, the literal {@code /}, and the {@code score} value concatenated.
     */
    @Override
    public String toString() {
      return Joiner.on('/').join(hasId() ? getId() : "none", getScore());
    }

    /**
     * If the argument is the string form of a record, returns that record; otherwise returns {@code
     * null}.
     */
    static @Nullable Record parse(String str) {
      List<String> parts = Splitter.on('/').splitToList(str);
      if (parts.size() != 2) {
        return null;
      }
      @Nullable Integer id = parts.get(0).equals("none") ? -1 : Ints.tryParse(parts.get(0));
      @Nullable Integer score = Ints.tryParse(parts.get(1));
      if (id == null || score == null) {
        return null;
      }
      return new Record(id, score);
    }
  }

  /**
   * A correspondence between {@link Record} instances which tests whether their {@code id} values
   * are equal and their {@code score} values are within 10 of each other. Smart diffing is not
   * supported.
   *
   * <p>The {@link Correspondence#compare} implementation support nulls, such that null corresponds
   * to null only. The {@link Correspondence#formatDiff} implementation does not support nulls.
   */
  static final Correspondence<Record, Record> RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10_NO_DIFF =
      Correspondence.from(
          // If we were allowed to use method references, this would be:
          // TestCorrespondences::recordsAreCloseEnough,
          new Correspondence.BinaryPredicate<Record, Record>() {
            @Override
            public boolean apply(Record actual, Record expected) {
              return recordsAreCloseEnough(actual, expected);
            }
          },
          "has the same id as and a score within 10 of");

  /**
   * A formatter for diffs between records. If the records have the same key, it gives a string of
   * the form {@code "score:<score_diff>"}. If they have different keys, it gives null.
   */
  static final Correspondence.DiffFormatter<Record, Record> RECORD_DIFF_FORMATTER =
      // If we were allowed to use method references, this would be:
      // TestCorrespondences::formatRecordDiff);
      new Correspondence.DiffFormatter<Record, Record>() {
        @Override
        public String formatDiff(Record actual, Record expected) {
          return formatRecordDiff(actual, expected);
        }
      };

  /**
   * A correspondence between {@link Record} instances which tests whether their {@code id} values
   * are equal and their {@code score} values are within 10 of each other. Smart diffing is enabled
   * for records with equal {@code id} values, with a formatted diff showing the actual {@code
   * score} value less the expected {@code score} value preceded by the literal {@code score:}.
   *
   * <p>The {@link Correspondence#compare} implementation support nulls, such that null corresponds
   * to null only. The {@link Correspondence#formatDiff} implementation does not support nulls.
   */
  static final Correspondence<Record, Record> RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10 =
      RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10_NO_DIFF.formattingDiffsUsing(RECORD_DIFF_FORMATTER);

  /**
   * A correspondence like {@link #RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10} except that the actual
   * values are strings which will be parsed before comparing. If the string does not parse to a
   * record then it does not correspond and is not diffed. Does not support null strings or records.
   */
  static final Correspondence<String, Record> PARSED_RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10 =
      Correspondence.from(
              // If we were allowed to use lambdas, this would be:
              // (String a, Record e) -> {
              //   @Nullable Record actualRecord = Record.parse(a);
              //   return actualRecord != null && recordsAreCloseEnough(actualRecord, e);
              // },
              new Correspondence.BinaryPredicate<String, Record>() {
                @Override
                public boolean apply(String actual, Record expected) {
                  @Nullable Record actualRecord = Record.parse(actual);
                  return actualRecord != null && recordsAreCloseEnough(actualRecord, expected);
                }
              },
              "parses to a record that " + RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
          .formattingDiffsUsing(
              // If we were allowe to use lambdas, this would be:
              // (a, e) -> {
              //   @Nullable Record actualRecord = Record.parse(a);
              //   return actualRecord != null ? formatRecordDiff(actualRecord, e) : null;
              // });
              new Correspondence.DiffFormatter<String, Record>() {
                @Override
                public String formatDiff(String actual, Record expected) {
                  @Nullable Record actualRecord = Record.parse(actual);
                  return actualRecord != null ? formatRecordDiff(actualRecord, expected) : null;
                }
              });

  private static boolean recordsAreCloseEnough(@Nullable Record actual, @Nullable Record expected) {
    if (actual == null) {
      return expected == null;
    }
    if (expected == null) {
      return false;
    }
    return actual.hasSameId(expected) && Math.abs(actual.getScore() - expected.getScore()) <= 10;
  }

  private static String formatRecordDiff(Record actual, Record expected) {
    if (actual.hasId() && expected.hasId() && actual.getId() == expected.getId()) {
      return "score:" + (actual.getScore() - expected.getScore());
    } else {
      return null;
    }
  }

  /**
   * A key function for {@link Record} instances that keys records by their {@code id} values. The
   * key is null if the record has no {@code id}. Does not support null records.
   */
  static final Function<Record, Integer> RECORD_ID =
      new Function<Record, Integer>() {

        @Override
        public @Nullable Integer apply(Record record) {
          return record.hasId() ? record.getId() : null;
        }
      };

  /**
   * A key function for {@link Record} instances that keys records by their {@code id} values. The
   * key is null if the record has no {@code id}. Does not support null records.
   */
  static final Function<Record, Integer> NULL_SAFE_RECORD_ID =
      new Function<Record, Integer>() {

        @Override
        public @Nullable Integer apply(Record record) {
          if (record == null) {
            return 0;
          }
          return record.hasId() ? record.getId() : null;
        }
      };

  /**
   * A key function for {@link String} instances that attempts to parse them as {@link Record}
   * instances and keys records by their {@code id} values. The key is null if the string does not
   * parse or the record has no {@code id}. Does not support null strings.
   */
  static final Function<String, Integer> PARSED_RECORD_ID =
      new Function<String, Integer>() {

        @Override
        public @Nullable Integer apply(String str) {
          Record record = Record.parse(str);
          return record != null ? RECORD_ID.apply(record) : null;
        }
      };

  private TestCorrespondences() {}
}