1 // Copyright 2017 The Bazel Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 package com.google.devtools.build.android.desugar; 15 16 import static com.google.common.truth.Truth.assertThat; 17 import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.getStrategyClassName; 18 import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.getTwrStrategyClassNameSpecifiedInSystemProperty; 19 import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.isMimicStrategy; 20 import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.isNullStrategy; 21 import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.isReuseStrategy; 22 import static org.junit.Assert.fail; 23 import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS; 24 import static org.objectweb.asm.Opcodes.ASM5; 25 import static org.objectweb.asm.Opcodes.INVOKESTATIC; 26 import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; 27 28 import com.google.devtools.build.android.desugar.runtime.ThrowableExtension; 29 import com.google.devtools.build.android.desugar.testdata.ClassUsingTryWithResources; 30 import java.io.IOException; 31 import java.lang.reflect.InvocationTargetException; 32 import java.util.HashMap; 33 import java.util.HashSet; 34 import java.util.Map; 35 import java.util.Set; 36 import java.util.concurrent.atomic.AtomicInteger; 37 import org.junit.Before; 38 import org.junit.Test; 39 import org.junit.runner.RunWith; 40 import org.junit.runners.JUnit4; 41 import org.objectweb.asm.ClassReader; 42 import org.objectweb.asm.ClassVisitor; 43 import org.objectweb.asm.ClassWriter; 44 import org.objectweb.asm.MethodVisitor; 45 import org.objectweb.asm.Opcodes; 46 import org.objectweb.asm.Type; 47 48 /** This is the unit test for {@link TryWithResourcesRewriter} */ 49 @RunWith(JUnit4.class) 50 public class TryWithResourcesRewriterTest { 51 52 private final DesugaringClassLoader classLoader = 53 new DesugaringClassLoader(ClassUsingTryWithResources.class.getName()); 54 private Class<?> desugaredClass; 55 56 @Before 57 public void setup() { 58 try { 59 desugaredClass = classLoader.findClass(ClassUsingTryWithResources.class.getName()); 60 } catch (ClassNotFoundException e) { 61 throw new AssertionError(e); 62 } 63 } 64 65 @Test 66 public void testMethodsAreDesugared() { 67 // verify whether the desugared class is indeed desugared. 68 DesugaredThrowableMethodCallCounter origCounter = 69 countDesugaredThrowableMethodCalls(ClassUsingTryWithResources.class); 70 DesugaredThrowableMethodCallCounter desugaredCounter = 71 countDesugaredThrowableMethodCalls(classLoader.classContent, classLoader); 72 /** 73 * In java9, javac creates a helper method {@code $closeResource(Throwable, AutoCloseable) 74 * to close resources. So, the following number 3 is highly dependant on the version of javac. 75 */ 76 assertThat(hasAutoCloseable(classLoader.classContent)).isFalse(); 77 assertThat(classLoader.numOfTryWithResourcesInvoked.intValue()).isAtLeast(2); 78 assertThat(classLoader.visitedExceptionTypes) 79 .containsExactly( 80 "java/lang/Exception", "java/lang/Throwable", "java/io/UnsupportedEncodingException"); 81 assertDesugaringBehavior(origCounter, desugaredCounter); 82 } 83 84 @Test 85 public void testCheckSuppressedExceptionsReturningEmptySuppressedExceptions() { 86 { 87 Throwable[] suppressed = ClassUsingTryWithResources.checkSuppressedExceptions(false); 88 assertThat(suppressed).isEmpty(); 89 } 90 try { 91 Throwable[] suppressed = 92 (Throwable[]) 93 desugaredClass 94 .getMethod("checkSuppressedExceptions", boolean.class) 95 .invoke(null, Boolean.FALSE); 96 assertThat(suppressed).isEmpty(); 97 } catch (Exception e) { 98 e.printStackTrace(); 99 throw new AssertionError(e); 100 } 101 } 102 103 @Test 104 public void testPrintStackTraceOfCaughtException() { 105 { 106 String trace = ClassUsingTryWithResources.printStackTraceOfCaughtException(); 107 assertThat(trace.toLowerCase()).contains("suppressed"); 108 } 109 try { 110 String trace = 111 (String) desugaredClass.getMethod("printStackTraceOfCaughtException").invoke(null); 112 113 if (isMimicStrategy()) { 114 assertThat(trace.toLowerCase()).contains("suppressed"); 115 } else if (isReuseStrategy()) { 116 assertThat(trace.toLowerCase()).contains("suppressed"); 117 } else if (isNullStrategy()) { 118 assertThat(trace.toLowerCase()).doesNotContain("suppressed"); 119 } else { 120 fail("unexpected desugaring strategy " + ThrowableExtension.getStrategy()); 121 } 122 } catch (Exception e) { 123 e.printStackTrace(); 124 throw new AssertionError(e); 125 } 126 } 127 128 @Test 129 public void testCheckSuppressedExceptionReturningOneSuppressedException() { 130 { 131 Throwable[] suppressed = ClassUsingTryWithResources.checkSuppressedExceptions(true); 132 assertThat(suppressed).hasLength(1); 133 } 134 try { 135 Throwable[] suppressed = 136 (Throwable[]) 137 desugaredClass 138 .getMethod("checkSuppressedExceptions", boolean.class) 139 .invoke(null, Boolean.TRUE); 140 141 if (isMimicStrategy()) { 142 assertThat(suppressed).hasLength(1); 143 } else if (isReuseStrategy()) { 144 assertThat(suppressed).hasLength(1); 145 } else if (isNullStrategy()) { 146 assertThat(suppressed).isEmpty(); 147 } else { 148 fail("unexpected desugaring strategy " + ThrowableExtension.getStrategy()); 149 } 150 } catch (Exception e) { 151 e.printStackTrace(); 152 throw new AssertionError(e); 153 } 154 } 155 156 @Test 157 public void testSimpleTryWithResources() throws Throwable { 158 { 159 try { 160 ClassUsingTryWithResources.simpleTryWithResources(); 161 fail("Expected RuntimeException"); 162 } catch (RuntimeException expected) { 163 assertThat(expected.getClass()).isEqualTo(RuntimeException.class); 164 assertThat(expected.getSuppressed()).hasLength(1); 165 assertThat(expected.getSuppressed()[0].getClass()).isEqualTo(IOException.class); 166 } 167 } 168 169 try { 170 try { 171 desugaredClass.getMethod("simpleTryWithResources").invoke(null); 172 fail("Expected RuntimeException"); 173 } catch (InvocationTargetException e) { 174 throw e.getCause(); 175 } 176 } catch (RuntimeException expected) { 177 String expectedStrategyName = getTwrStrategyClassNameSpecifiedInSystemProperty(); 178 assertThat(getStrategyClassName()).isEqualTo(expectedStrategyName); 179 if (isMimicStrategy()) { 180 assertThat(expected.getSuppressed()).isEmpty(); 181 assertThat(ThrowableExtension.getSuppressed(expected)).hasLength(1); 182 assertThat(ThrowableExtension.getSuppressed(expected)[0].getClass()) 183 .isEqualTo(IOException.class); 184 } else if (isReuseStrategy()) { 185 assertThat(expected.getSuppressed()).hasLength(1); 186 assertThat(expected.getSuppressed()[0].getClass()).isEqualTo(IOException.class); 187 assertThat(ThrowableExtension.getSuppressed(expected)[0].getClass()) 188 .isEqualTo(IOException.class); 189 } else if (isNullStrategy()) { 190 assertThat(expected.getSuppressed()).isEmpty(); 191 assertThat(ThrowableExtension.getSuppressed(expected)).isEmpty(); 192 } else { 193 fail("unexpected desugaring strategy " + ThrowableExtension.getStrategy()); 194 } 195 } 196 } 197 198 private static void assertDesugaringBehavior( 199 DesugaredThrowableMethodCallCounter orig, DesugaredThrowableMethodCallCounter desugared) { 200 assertThat(desugared.countThrowableGetSuppressed()).isEqualTo(orig.countExtGetSuppressed()); 201 assertThat(desugared.countThrowableAddSuppressed()).isEqualTo(orig.countExtAddSuppressed()); 202 assertThat(desugared.countThrowablePrintStackTrace()).isEqualTo(orig.countExtPrintStackTrace()); 203 assertThat(desugared.countThrowablePrintStackTracePrintStream()) 204 .isEqualTo(orig.countExtPrintStackTracePrintStream()); 205 assertThat(desugared.countThrowablePrintStackTracePrintWriter()) 206 .isEqualTo(orig.countExtPrintStackTracePrintWriter()); 207 208 assertThat(orig.countThrowableGetSuppressed()).isEqualTo(desugared.countExtGetSuppressed()); 209 // $closeResource may be specialized into multiple versions. 210 assertThat(orig.countThrowableAddSuppressed()).isAtMost(desugared.countExtAddSuppressed()); 211 assertThat(orig.countThrowablePrintStackTrace()).isEqualTo(desugared.countExtPrintStackTrace()); 212 assertThat(orig.countThrowablePrintStackTracePrintStream()) 213 .isEqualTo(desugared.countExtPrintStackTracePrintStream()); 214 assertThat(orig.countThrowablePrintStackTracePrintWriter()) 215 .isEqualTo(desugared.countExtPrintStackTracePrintWriter()); 216 217 if (orig.getSyntheticCloseResourceCount() > 0) { 218 // Depending on the specific javac version, $closeResource(Throwable, AutoCloseable) may not 219 // be there. 220 assertThat(orig.getSyntheticCloseResourceCount()).isEqualTo(1); 221 assertThat(desugared.getSyntheticCloseResourceCount()).isAtLeast(1); 222 } 223 assertThat(desugared.countThrowablePrintStackTracePrintStream()).isEqualTo(0); 224 assertThat(desugared.countThrowablePrintStackTracePrintStream()).isEqualTo(0); 225 assertThat(desugared.countThrowablePrintStackTracePrintWriter()).isEqualTo(0); 226 assertThat(desugared.countThrowableAddSuppressed()).isEqualTo(0); 227 assertThat(desugared.countThrowableGetSuppressed()).isEqualTo(0); 228 } 229 230 private static DesugaredThrowableMethodCallCounter countDesugaredThrowableMethodCalls( 231 Class<?> klass) { 232 try { 233 ClassReader reader = new ClassReader(klass.getName()); 234 DesugaredThrowableMethodCallCounter counter = 235 new DesugaredThrowableMethodCallCounter(klass.getClassLoader()); 236 reader.accept(counter, 0); 237 return counter; 238 } catch (IOException e) { 239 e.printStackTrace(); 240 fail(e.toString()); 241 return null; 242 } 243 } 244 245 private static DesugaredThrowableMethodCallCounter countDesugaredThrowableMethodCalls( 246 byte[] content, ClassLoader loader) { 247 ClassReader reader = new ClassReader(content); 248 DesugaredThrowableMethodCallCounter counter = new DesugaredThrowableMethodCallCounter(loader); 249 reader.accept(counter, 0); 250 return counter; 251 } 252 253 /** Check whether java.lang.AutoCloseable is used as arguments of any method. */ 254 private static boolean hasAutoCloseable(byte[] classContent) { 255 ClassReader reader = new ClassReader(classContent); 256 final AtomicInteger counter = new AtomicInteger(); 257 ClassVisitor visitor = 258 new ClassVisitor(Opcodes.ASM5) { 259 @Override 260 public MethodVisitor visitMethod( 261 int access, String name, String desc, String signature, String[] exceptions) { 262 for (Type argumentType : Type.getArgumentTypes(desc)) { 263 if ("Ljava/lang/AutoCloseable;".equals(argumentType.getDescriptor())) { 264 counter.incrementAndGet(); 265 } 266 } 267 return null; 268 } 269 }; 270 reader.accept(visitor, 0); 271 return counter.get() > 0; 272 } 273 274 private static class DesugaredThrowableMethodCallCounter extends ClassVisitor { 275 private final ClassLoader classLoader; 276 private final Map<String, AtomicInteger> counterMap; 277 private int syntheticCloseResourceCount; 278 279 public DesugaredThrowableMethodCallCounter(ClassLoader loader) { 280 super(ASM5); 281 classLoader = loader; 282 counterMap = new HashMap<>(); 283 TryWithResourcesRewriter.TARGET_METHODS 284 .entries() 285 .forEach(entry -> counterMap.put(entry.getKey() + entry.getValue(), new AtomicInteger())); 286 TryWithResourcesRewriter.TARGET_METHODS 287 .entries() 288 .forEach( 289 entry -> 290 counterMap.put( 291 entry.getKey() 292 + TryWithResourcesRewriter.METHOD_DESC_MAP.get(entry.getValue()), 293 new AtomicInteger())); 294 } 295 296 @Override 297 public MethodVisitor visitMethod( 298 int access, String name, String desc, String signature, String[] exceptions) { 299 if (BitFlags.isSet(access, Opcodes.ACC_SYNTHETIC | Opcodes.ACC_STATIC) 300 && name.equals("$closeResource") 301 && Type.getArgumentTypes(desc).length == 2 302 && Type.getArgumentTypes(desc)[0].getDescriptor().equals("Ljava/lang/Throwable;")) { 303 ++syntheticCloseResourceCount; 304 } 305 return new InvokeCounter(); 306 } 307 308 private class InvokeCounter extends MethodVisitor { 309 310 public InvokeCounter() { 311 super(ASM5); 312 } 313 314 private boolean isAssignableToThrowable(String owner) { 315 try { 316 Class<?> ownerClass = classLoader.loadClass(owner.replace('/', '.')); 317 return Throwable.class.isAssignableFrom(ownerClass); 318 } catch (ClassNotFoundException e) { 319 throw new AssertionError(e); 320 } 321 } 322 323 @Override 324 public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { 325 String signature = name + desc; 326 if ((opcode == INVOKEVIRTUAL && isAssignableToThrowable(owner)) 327 || (opcode == INVOKESTATIC 328 && Type.getInternalName(ThrowableExtension.class).equals(owner))) { 329 AtomicInteger counter = counterMap.get(signature); 330 if (counter == null) { 331 return; 332 } 333 counter.incrementAndGet(); 334 } 335 } 336 } 337 338 public int getSyntheticCloseResourceCount() { 339 return syntheticCloseResourceCount; 340 } 341 342 public int countThrowableAddSuppressed() { 343 return counterMap.get("addSuppressed(Ljava/lang/Throwable;)V").get(); 344 } 345 346 public int countThrowableGetSuppressed() { 347 return counterMap.get("getSuppressed()[Ljava/lang/Throwable;").get(); 348 } 349 350 public int countThrowablePrintStackTrace() { 351 return counterMap.get("printStackTrace()V").get(); 352 } 353 354 public int countThrowablePrintStackTracePrintStream() { 355 return counterMap.get("printStackTrace(Ljava/io/PrintStream;)V").get(); 356 } 357 358 public int countThrowablePrintStackTracePrintWriter() { 359 return counterMap.get("printStackTrace(Ljava/io/PrintWriter;)V").get(); 360 } 361 362 public int countExtAddSuppressed() { 363 return counterMap.get("addSuppressed(Ljava/lang/Throwable;Ljava/lang/Throwable;)V").get(); 364 } 365 366 public int countExtGetSuppressed() { 367 return counterMap.get("getSuppressed(Ljava/lang/Throwable;)[Ljava/lang/Throwable;").get(); 368 } 369 370 public int countExtPrintStackTrace() { 371 return counterMap.get("printStackTrace(Ljava/lang/Throwable;)V").get(); 372 } 373 374 public int countExtPrintStackTracePrintStream() { 375 return counterMap.get("printStackTrace(Ljava/lang/Throwable;Ljava/io/PrintStream;)V").get(); 376 } 377 378 public int countExtPrintStackTracePrintWriter() { 379 return counterMap.get("printStackTrace(Ljava/lang/Throwable;Ljava/io/PrintWriter;)V").get(); 380 } 381 } 382 383 private static class DesugaringClassLoader extends ClassLoader { 384 385 private final String targetedClassName; 386 private Class<?> klass; 387 private byte[] classContent; 388 private final Set<String> visitedExceptionTypes = new HashSet<>(); 389 private final AtomicInteger numOfTryWithResourcesInvoked = new AtomicInteger(); 390 391 public DesugaringClassLoader(String targetedClassName) { 392 super(DesugaringClassLoader.class.getClassLoader()); 393 this.targetedClassName = targetedClassName; 394 } 395 396 @Override 397 protected Class<?> findClass(String name) throws ClassNotFoundException { 398 if (name.equals(targetedClassName)) { 399 if (klass != null) { 400 return klass; 401 } 402 // desugar the class, and return the desugared one. 403 classContent = desugarTryWithResources(name); 404 klass = defineClass(name, classContent, 0, classContent.length); 405 return klass; 406 } else { 407 return super.findClass(name); 408 } 409 } 410 411 private byte[] desugarTryWithResources(String className) { 412 try { 413 ClassReader reader = new ClassReader(className); 414 CloseResourceMethodScanner scanner = new CloseResourceMethodScanner(); 415 reader.accept(scanner, ClassReader.SKIP_DEBUG); 416 ClassWriter writer = new ClassWriter(reader, COMPUTE_MAXS); 417 TryWithResourcesRewriter rewriter = 418 new TryWithResourcesRewriter( 419 writer, 420 TryWithResourcesRewriterTest.class.getClassLoader(), 421 visitedExceptionTypes, 422 numOfTryWithResourcesInvoked, 423 scanner.hasCloseResourceMethod()); 424 reader.accept(rewriter, 0); 425 return writer.toByteArray(); 426 } catch (IOException e) { 427 fail(e.toString()); 428 return null; // suppress compiler error. 429 } 430 } 431 } 432 } 433