aboutsummaryrefslogtreecommitdiff
path: root/core/src/main/java/com/google/common/truth/Platform.java
blob: bc4301a2b654b78e238b6fe5a24a6d94f2071f06 (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
361
362
363
364
365
366
367
/*
 * Copyright (c) 2014 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.checkNotNull;
import static com.google.common.base.Suppliers.memoize;
import static com.google.common.base.Throwables.throwIfUnchecked;
import static com.google.common.truth.DiffUtils.generateUnifiedDiff;
import static com.google.common.truth.Fact.fact;

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.regex.Pattern;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.junit.ComparisonFailure;
import org.junit.rules.TestRule;

/**
 * Extracted routines that need to be swapped in for GWT, to allow for minimal deltas between the
 * GWT and non-GWT version.
 *
 * @author Christian Gruber (cgruber@google.com)
 */
final class Platform {
  private Platform() {}

  /** Returns true if the instance is assignable to the type Clazz. */
  static boolean isInstanceOfType(Object instance, Class<?> clazz) {
    return clazz.isInstance(instance);
  }

  /** Determines if the given subject contains a match for the given regex. */
  static boolean containsMatch(String actual, String regex) {
    return Pattern.compile(regex).matcher(actual).find();
  }

  /**
   * Returns an array containing all of the exceptions that were suppressed to deliver the given
   * exception. If suppressed exceptions are not supported (pre-Java 1.7), an empty array will be
   * returned.
   */
  static Throwable[] getSuppressed(Throwable throwable) {
    try {
      Method getSuppressed = throwable.getClass().getMethod("getSuppressed");
      return (Throwable[]) checkNotNull(getSuppressed.invoke(throwable));
    } catch (NoSuchMethodException e) {
      return new Throwable[0];
    } catch (IllegalAccessException e) {
      // We're calling a public method on a public class.
      throw newLinkageError(e);
    } catch (InvocationTargetException e) {
      throwIfUnchecked(e.getCause());
      // getSuppressed has no `throws` clause.
      throw newLinkageError(e);
    }
  }

  static void cleanStackTrace(Throwable throwable) {
    StackTraceCleaner.cleanStackTrace(throwable);
  }

  /**
   * Tries to infer a name for the root actual value from the bytecode. The "root" actual value is
   * the value passed to {@code assertThat} or {@code that}, as distinct from any later actual
   * values produced by chaining calls like {@code hasMessageThat}.
   */
  // Checker complains that first invoke argument is null.
  @SuppressWarnings("argument.type.incompatible")
  static @Nullable String inferDescription() {
    if (isInferDescriptionDisabled()) {
      return null;
    }

    AssertionError stack = new AssertionError();
    /*
     * cleanStackTrace() lets users turn off cleaning, so it's possible that we'll end up operating
     * on an uncleaned stack trace. That should be mostly harmless. We could try force-enabling
     * cleaning for inferDescription() only, but if anyone is turning it off, it might be because of
     * bugs or confusing stack traces. Force-enabling it here might trigger those same problems.
     */
    cleanStackTrace(stack);
    if (stack.getStackTrace().length == 0) {
      return null;
    }
    StackTraceElement top = stack.getStackTrace()[0];
    try {
      /*
       * Invoke ActualValueInference reflectively so that Truth can be compiled and run without its
       * dependency, ASM, on the classpath.
       *
       * Also, mildly obfuscate the class name that we're looking up. The obfuscation prevents R8
       * from detecting the usage of ActualValueInference. That in turn lets users exclude it from
       * the compile-time classpath if they want. (And then *that* probably makes it easier and/or
       * safer for R8 users (i.e., Android users) to exclude it from the *runtime* classpath. It
       * would do no good there, anyway, since ASM won't find any .class files to load under
       * Android. Perhaps R8 will even omit ASM automatically once it detects that it's "unused?")
       *
       * TODO(cpovirk): Add a test that runs R8 without ASM present.
       */
      String clazz =
          Joiner.on('.').join("com", "google", "common", "truth", "ActualValueInference");
      return (String)
          Class.forName(clazz)
              .getDeclaredMethod("describeActualValue", String.class, String.class, int.class)
              .invoke(null, top.getClassName(), top.getMethodName(), top.getLineNumber());
    } catch (IllegalAccessException
        | InvocationTargetException
        | NoSuchMethodException
        | ClassNotFoundException
        | LinkageError
        | RuntimeException e) {
      // Some possible reasons:
      // - Inside Google, we omit ActualValueInference entirely under Android.
      // - Outside Google, someone is running without ASM on the classpath.
      // - There's a bug.
      // - We don't handle a new bytecode feature.
      // TODO(cpovirk): Log a warning, at least for non-ClassNotFoundException, non-LinkageError?
      return null;
    }
  }

  private static final String DIFF_KEY = "diff (-expected +actual)";

  static @Nullable ImmutableList<Fact> makeDiff(String expected, String actual) {
    ImmutableList<String> expectedLines = splitLines(expected);
    ImmutableList<String> actualLines = splitLines(actual);
    List<String> unifiedDiff =
        generateUnifiedDiff(expectedLines, actualLines, /* contextSize= */ 3);
    if (unifiedDiff.isEmpty()) {
      return ImmutableList.of(
          fact(DIFF_KEY, "(line contents match, but line-break characters differ)"));
      // TODO(cpovirk): Possibly include the expected/actual value, too?
    }
    String result = Joiner.on("\n").join(unifiedDiff);
    if (result.length() > expected.length() && result.length() > actual.length()) {
      return null;
    }
    return ImmutableList.of(fact(DIFF_KEY, result));
  }

  private static ImmutableList<String> splitLines(String s) {
    // splitToList is @Beta, so we avoid it.
    return ImmutableList.copyOf(Splitter.onPattern("\r?\n").split(s));
  }

  abstract static class PlatformComparisonFailure extends ComparisonFailure {
    private final String message;

    /** Separate cause field, in case initCause() fails. */
    private final @Nullable Throwable cause;

    PlatformComparisonFailure(
        String message, String expected, String actual, @Nullable Throwable cause) {
      super(message, expected, actual);
      this.message = message;
      this.cause = cause;

      try {
        initCause(cause);
      } catch (IllegalStateException alreadyInitializedBecauseOfHarmonyBug) {
        // See Truth.SimpleAssertionError.
      }
    }

    @Override
    public final String getMessage() {
      return message;
    }

    @Override
    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
    public final @Nullable Throwable getCause() {
      return cause;
    }

    // To avoid printing the class name before the message.
    // TODO(cpovirk): Write a test that fails without this. Ditto for SimpleAssertionError.
    @Override
    public final String toString() {
      return checkNotNull(getLocalizedMessage());
    }
  }

  static String doubleToString(double value) {
    return Double.toString(value);
  }

  static String floatToString(float value) {
    return Float.toString(value);
  }

  /** Turns a non-double, non-float object into a string. */
  static String stringValueOfNonFloatingPoint(@Nullable Object o) {
    return String.valueOf(o);
  }

  /** Returns a human readable string representation of the throwable's stack trace. */
  static String getStackTraceAsString(Throwable throwable) {
    return Throwables.getStackTraceAsString(throwable);
  }

  /** Tests if current platform is Android. */
  static boolean isAndroid() {
    return checkNotNull(System.getProperty("java.runtime.name", "")).contains("Android");
  }

  /**
   * Wrapping interface of {@link TestRule} to be used within truth.
   *
   * <p>Note that the sole purpose of this interface is to allow it to be swapped in GWT
   * implementation.
   */
  interface JUnitTestRule extends TestRule {}

  static final String EXPECT_FAILURE_WARNING_IF_GWT = "";

  // TODO(cpovirk): Share code with StackTraceCleaner?
  private static boolean isInferDescriptionDisabled() {
    // Reading system properties might be forbidden.
    try {
      return Boolean.parseBoolean(
          System.getProperty("com.google.common.truth.disable_infer_description"));
    } catch (SecurityException e) {
      // Hope for the best.
      return false;
    }
  }

  static AssertionError makeComparisonFailure(
      ImmutableList<String> messages,
      ImmutableList<Fact> facts,
      String expected,
      String actual,
      @Nullable Throwable cause) {
    Class<?> comparisonFailureClass;
    try {
      comparisonFailureClass = Class.forName("com.google.common.truth.ComparisonFailureWithFacts");
    } catch (LinkageError | ClassNotFoundException probablyJunitNotOnClasspath) {
      /*
       * LinkageError makes sense, but ClassNotFoundException shouldn't happen:
       * ComparisonFailureWithFacts should be there, even if its JUnit 4 dependency is not. But it's
       * harmless to catch an "impossible" exception, and if someone decides to strip the class out
       * (perhaps along with Platform.PlatformComparisonFailure, to satisfy a tool that is unhappy
       * because it can't find the latter's superclass because JUnit 4 is also missing?), presumably
       * we should still fall back to a plain AssertionError.
       *
       * TODO(cpovirk): Consider creating and using yet another class like AssertionErrorWithFacts,
       * not actually extending ComparisonFailure but still exposing getExpected() and getActual()
       * methods.
       */
      return new AssertionErrorWithFacts(messages, facts, cause);
    }
    Class<? extends AssertionError> asAssertionErrorSubclass =
        comparisonFailureClass.asSubclass(AssertionError.class);

    Constructor<? extends AssertionError> constructor;
    try {
      constructor =
          asAssertionErrorSubclass.getDeclaredConstructor(
              ImmutableList.class,
              ImmutableList.class,
              String.class,
              String.class,
              Throwable.class);
    } catch (NoSuchMethodException e) {
      // That constructor exists.
      throw newLinkageError(e);
    }

    try {
      return constructor.newInstance(messages, facts, expected, actual, cause);
    } catch (InvocationTargetException e) {
      throwIfUnchecked(e.getCause());
      // That constructor has no `throws` clause.
      throw newLinkageError(e);
    } catch (InstantiationException e) {
      // The class is a concrete class.
      throw newLinkageError(e);
    } catch (IllegalAccessException e) {
      // We're accessing a class from within its package.
      throw newLinkageError(e);
    }
  }

  private static LinkageError newLinkageError(Throwable cause) {
    LinkageError error = new LinkageError(cause.toString());
    error.initCause(cause);
    return error;
  }

  static boolean isKotlinRange(Iterable<?> iterable) {
    return closedRangeClassIfAvailable.get() != null
        && closedRangeClassIfAvailable.get().isInstance(iterable);
    // (If the class isn't available, then nothing could be an instance of ClosedRange.)
  }

  // Not using lambda here because of wrong nullability type inference in this case.
  private static final Supplier<@Nullable Class<?>> closedRangeClassIfAvailable =
      Suppliers.<@Nullable Class<?>>memoize(
          () -> {
            try {
              return Class.forName("kotlin.ranges.ClosedRange");
              /*
               * TODO(cpovirk): Consider looking up the Method we'll need here, too: If it's not
               * present (maybe because Proguard stripped it, similar to cl/462826082), then we
               * don't want our caller to continue on to call kotlinRangeContains, since it won't
               * be able to give an answer about what ClosedRange.contains will return.
               * (Alternatively, we could make kotlinRangeContains contain its own fallback to
               * Iterables.contains. Conceivably its first fallback could even be to try reading
               * `start` and `endInclusive` from the ClosedRange instance, but even then, we'd
               * want to check in advance whether we're able to access those.)
               */
            } catch (ClassNotFoundException notAvailable) {
              return null;
            }
          });

  static boolean kotlinRangeContains(Iterable<?> haystack, @Nullable Object needle) {
    try {
      return (boolean) closedRangeContainsMethod.get().invoke(haystack, needle);
    } catch (InvocationTargetException e) {
      if (e.getCause() instanceof ClassCastException) {
        // icky but no worse than what we normally do for isIn(Iterable)
        return false;
      }
      throwIfUnchecked(e.getCause());
      // That method has no `throws` clause.
      throw newLinkageError(e.getCause());
    } catch (IllegalAccessException e) {
      // We're calling a public method on a public class.
      throw newLinkageError(e);
    }
  }

  private static final Supplier<Method> closedRangeContainsMethod =
      memoize(
          () -> {
            try {
              return checkNotNull(closedRangeClassIfAvailable.get())
                  .getMethod("contains", Comparable.class);
            } catch (NoSuchMethodException e) {
              // That method exists. (But see the discussion at closedRangeClassIfAvailable above.)
              throw newLinkageError(e);
            }
          });
}