• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2014 Google, Inc.
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 package com.google.common.truth;
17 
18 import static com.google.common.base.Throwables.throwIfUnchecked;
19 import static com.google.common.truth.DiffUtils.generateUnifiedDiff;
20 import static com.google.common.truth.Fact.fact;
21 
22 import com.google.common.base.Joiner;
23 import com.google.common.base.Splitter;
24 import com.google.common.base.Throwables;
25 import com.google.common.collect.ImmutableList;
26 import java.lang.reflect.Constructor;
27 import java.lang.reflect.InvocationTargetException;
28 import java.lang.reflect.Method;
29 import java.util.List;
30 import java.util.regex.Pattern;
31 import org.checkerframework.checker.nullness.qual.Nullable;
32 import org.junit.ComparisonFailure;
33 import org.junit.rules.TestRule;
34 
35 /**
36  * Extracted routines that need to be swapped in for GWT, to allow for minimal deltas between the
37  * GWT and non-GWT version.
38  *
39  * @author Christian Gruber (cgruber@google.com)
40  */
41 final class Platform {
Platform()42   private Platform() {}
43 
44   /** Returns true if the instance is assignable to the type Clazz. */
isInstanceOfType(Object instance, Class<?> clazz)45   static boolean isInstanceOfType(Object instance, Class<?> clazz) {
46     return clazz.isInstance(instance);
47   }
48 
49   /** Determines if the given subject contains a match for the given regex. */
containsMatch(String actual, String regex)50   static boolean containsMatch(String actual, String regex) {
51     return Pattern.compile(regex).matcher(actual).find();
52   }
53 
54   /**
55    * Returns an array containing all of the exceptions that were suppressed to deliver the given
56    * exception. If suppressed exceptions are not supported (pre-Java 1.7), an empty array will be
57    * returned.
58    */
getSuppressed(Throwable throwable)59   static Throwable[] getSuppressed(Throwable throwable) {
60     try {
61       Method getSuppressed = throwable.getClass().getMethod("getSuppressed");
62       return (Throwable[]) getSuppressed.invoke(throwable);
63     } catch (NoSuchMethodException e) {
64       return new Throwable[0];
65     } catch (IllegalAccessException e) {
66       throw new RuntimeException(e);
67     } catch (InvocationTargetException e) {
68       throw new RuntimeException(e);
69     }
70   }
71 
cleanStackTrace(Throwable throwable)72   static void cleanStackTrace(Throwable throwable) {
73     StackTraceCleaner.cleanStackTrace(throwable);
74   }
75 
76   /**
77    * Tries to infer a name for the root actual value from the bytecode. The "root" actual value is
78    * the value passed to {@code assertThat} or {@code that}, as distinct from any later actual
79    * values produced by chaining calls like {@code hasMessageThat}.
80    */
inferDescription()81   static String inferDescription() {
82     if (isInferDescriptionDisabled()) {
83       return null;
84     }
85 
86     AssertionError stack = new AssertionError();
87     /*
88      * cleanStackTrace() lets users turn off cleaning, so it's possible that we'll end up operating
89      * on an uncleaned stack trace. That should be mostly harmless. We could try force-enabling
90      * cleaning for inferDescription() only, but if anyone is turning it off, it might be because of
91      * bugs or confusing stack traces. Force-enabling it here might trigger those same problems.
92      */
93     cleanStackTrace(stack);
94     if (stack.getStackTrace().length == 0) {
95       return null;
96     }
97     StackTraceElement top = stack.getStackTrace()[0];
98     try {
99       /*
100        * Invoke ActualValueInference reflectively so that Truth can be compiled and run without its
101        * dependency, ASM, on the classpath.
102        *
103        * Also, mildly obfuscate the class name that we're looking up. The obfuscation prevents R8
104        * from detecting the usage of ActualValueInference. That in turn lets users exclude it from
105        * the compile-time classpath if they want. (And then *that* probably makes it easier and/or
106        * safer for R8 users (i.e., Android users) to exclude it from the *runtime* classpath. It
107        * would do no good there, anyway, since ASM won't find any .class files to load under
108        * Android. Perhaps R8 will even omit ASM automatically once it detects that it's "unused?")
109        *
110        * TODO(cpovirk): Add a test that runs R8 without ASM present.
111        */
112       String clazz =
113           Joiner.on('.').join("com", "google", "common", "truth", "ActualValueInference");
114       return (String)
115           Class.forName(clazz)
116               .getDeclaredMethod("describeActualValue", String.class, String.class, int.class)
117               .invoke(null, top.getClassName(), top.getMethodName(), top.getLineNumber());
118     } catch (IllegalAccessException
119         | InvocationTargetException
120         | NoSuchMethodException
121         | ClassNotFoundException
122         | LinkageError
123         | RuntimeException e) {
124       // Some possible reasons:
125       // - Inside Google, we omit ActualValueInference entirely under Android.
126       // - Outside Google, someone is running without ASM on the classpath.
127       // - There's a bug.
128       // - We don't handle a new bytecode feature.
129       // TODO(cpovirk): Log a warning, at least for non-ClassNotFoundException, non-LinkageError?
130       return null;
131     }
132   }
133 
134   private static final String DIFF_KEY = "diff (-expected +actual)";
135 
makeDiff(String expected, String actual)136   static @Nullable ImmutableList<Fact> makeDiff(String expected, String actual) {
137     ImmutableList<String> expectedLines = splitLines(expected);
138     ImmutableList<String> actualLines = splitLines(actual);
139     List<String> unifiedDiff =
140         generateUnifiedDiff(expectedLines, actualLines, /* contextSize= */ 3);
141     if (unifiedDiff.isEmpty()) {
142       return ImmutableList.of(
143           fact(DIFF_KEY, "(line contents match, but line-break characters differ)"));
144       // TODO(cpovirk): Possibly include the expected/actual value, too?
145     }
146     String result = Joiner.on("\n").join(unifiedDiff);
147     if (result.length() > expected.length() && result.length() > actual.length()) {
148       return null;
149     }
150     return ImmutableList.of(fact(DIFF_KEY, result));
151   }
152 
splitLines(String s)153   private static ImmutableList<String> splitLines(String s) {
154     // splitToList is @Beta, so we avoid it.
155     return ImmutableList.copyOf(Splitter.onPattern("\r?\n").split(s));
156   }
157 
158   abstract static class PlatformComparisonFailure extends ComparisonFailure {
159     private final String message;
160 
161     /** Separate cause field, in case initCause() fails. */
162     private final @Nullable Throwable cause;
163 
PlatformComparisonFailure( String message, String expected, String actual, @Nullable Throwable cause)164     PlatformComparisonFailure(
165         String message, String expected, String actual, @Nullable Throwable cause) {
166       super(message, expected, actual);
167       this.message = message;
168       this.cause = cause;
169 
170       try {
171         initCause(cause);
172       } catch (IllegalStateException alreadyInitializedBecauseOfHarmonyBug) {
173         // See Truth.SimpleAssertionError.
174       }
175     }
176 
177     @Override
getMessage()178     public final String getMessage() {
179       return message;
180     }
181 
182     @Override
183     @SuppressWarnings("UnsynchronizedOverridesSynchronized")
getCause()184     public final Throwable getCause() {
185       return cause;
186     }
187 
188     // To avoid printing the class name before the message.
189     // TODO(cpovirk): Write a test that fails without this. Ditto for SimpleAssertionError.
190     @Override
toString()191     public final String toString() {
192       return getLocalizedMessage();
193     }
194   }
195 
doubleToString(double value)196   static String doubleToString(double value) {
197     return Double.toString(value);
198   }
199 
floatToString(float value)200   static String floatToString(float value) {
201     return Float.toString(value);
202   }
203 
204   /** Returns a human readable string representation of the throwable's stack trace. */
getStackTraceAsString(Throwable throwable)205   static String getStackTraceAsString(Throwable throwable) {
206     return Throwables.getStackTraceAsString(throwable);
207   }
208 
209   /** Tests if current platform is Android. */
isAndroid()210   static boolean isAndroid() {
211     return System.getProperty("java.runtime.name").contains("Android");
212   }
213 
214   /**
215    * Wrapping interface of {@link TestRule} to be used within truth.
216    *
217    * <p>Note that the sole purpose of this interface is to allow it to be swapped in GWT
218    * implementation.
219    */
220   interface JUnitTestRule extends TestRule {}
221 
222   static final String EXPECT_FAILURE_WARNING_IF_GWT = "";
223 
224   // TODO(cpovirk): Share code with StackTraceCleaner?
isInferDescriptionDisabled()225   private static boolean isInferDescriptionDisabled() {
226     // Reading system properties might be forbidden.
227     try {
228       return Boolean.parseBoolean(
229           System.getProperty("com.google.common.truth.disable_infer_description"));
230     } catch (SecurityException e) {
231       // Hope for the best.
232       return false;
233     }
234   }
235 
makeComparisonFailure( ImmutableList<String> messages, ImmutableList<Fact> facts, String expected, String actual, @Nullable Throwable cause)236   static AssertionError makeComparisonFailure(
237       ImmutableList<String> messages,
238       ImmutableList<Fact> facts,
239       String expected,
240       String actual,
241       @Nullable Throwable cause) {
242     Class<?> comparisonFailureClass;
243     try {
244       comparisonFailureClass = Class.forName("com.google.common.truth.ComparisonFailureWithFacts");
245     } catch (LinkageError | ClassNotFoundException probablyJunitNotOnClasspath) {
246       /*
247        * LinkageError makes sense, but ClassNotFoundException shouldn't happen:
248        * ComparisonFailureWithFacts should be there, even if its JUnit 4 dependency is not. But it's
249        * harmless to catch an "impossible" exception, and if someone decides to strip the class out
250        * (perhaps along with Platform.PlatformComparisonFailure, to satisfy a tool that is unhappy
251        * because it can't find the latter's superclass because JUnit 4 is also missing?), presumably
252        * we should still fall back to a plain AssertionError.
253        *
254        * TODO(cpovirk): Consider creating and using yet another class like AssertionErrorWithFacts,
255        * not actually extending ComparisonFailure but still exposing getExpected() and getActual()
256        * methods.
257        */
258       return new AssertionErrorWithFacts(messages, facts, cause);
259     }
260     Class<? extends AssertionError> asAssertionErrorSubclass =
261         comparisonFailureClass.asSubclass(AssertionError.class);
262 
263     Constructor<? extends AssertionError> constructor;
264     try {
265       constructor =
266           asAssertionErrorSubclass.getDeclaredConstructor(
267               ImmutableList.class,
268               ImmutableList.class,
269               String.class,
270               String.class,
271               Throwable.class);
272     } catch (NoSuchMethodException e) {
273       // That constructor exists.
274       throw newLinkageError(e);
275     }
276 
277     try {
278       return constructor.newInstance(messages, facts, expected, actual, cause);
279     } catch (InvocationTargetException e) {
280       throwIfUnchecked(e.getCause());
281       // That constructor has no `throws` clause.
282       throw newLinkageError(e);
283     } catch (InstantiationException e) {
284       // The class is a concrete class.
285       throw newLinkageError(e);
286     } catch (IllegalAccessException e) {
287       // We're accessing a class from within its package.
288       throw newLinkageError(e);
289     }
290   }
291 
newLinkageError(Throwable cause)292   private static LinkageError newLinkageError(Throwable cause) {
293     LinkageError error = new LinkageError(cause.toString());
294     error.initCause(cause);
295     return error;
296   }
297 }
298