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