// Copyright 2017 The Bazel Authors. All rights reserved. // // 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.devtools.build.android.desugar; import static com.google.common.truth.Truth.assertThat; import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.getStrategyClassName; import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.getTwrStrategyClassNameSpecifiedInSystemProperty; import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.isMimicStrategy; import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.isNullStrategy; import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.isReuseStrategy; import static org.junit.Assert.fail; import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS; import static org.objectweb.asm.Opcodes.ASM5; import static org.objectweb.asm.Opcodes.INVOKESTATIC; import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; import com.google.devtools.build.android.desugar.io.BitFlags; import com.google.devtools.build.android.desugar.runtime.ThrowableExtension; import com.google.devtools.build.android.desugar.testdata.ClassUsingTryWithResources; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; /** This is the unit test for {@link TryWithResourcesRewriter} */ @RunWith(JUnit4.class) public class TryWithResourcesRewriterTest { private final DesugaringClassLoader classLoader = new DesugaringClassLoader(ClassUsingTryWithResources.class.getName()); private Class desugaredClass; @Before public void setup() { try { desugaredClass = classLoader.findClass(ClassUsingTryWithResources.class.getName()); } catch (ClassNotFoundException e) { throw new AssertionError(e); } } @Test public void testMethodsAreDesugared() { // verify whether the desugared class is indeed desugared. DesugaredThrowableMethodCallCounter origCounter = countDesugaredThrowableMethodCalls(ClassUsingTryWithResources.class); DesugaredThrowableMethodCallCounter desugaredCounter = countDesugaredThrowableMethodCalls(classLoader.classContent, classLoader); /** * In java9, javac creates a helper method {@code $closeResource(Throwable, AutoCloseable) * to close resources. So, the following number 3 is highly dependant on the version of javac. */ assertThat(hasAutoCloseable(classLoader.classContent)).isFalse(); assertThat(classLoader.numOfTryWithResourcesInvoked.intValue()).isAtLeast(2); assertThat(classLoader.visitedExceptionTypes) .containsExactly( "java/lang/Exception", "java/lang/Throwable", "java/io/UnsupportedEncodingException"); assertDesugaringBehavior(origCounter, desugaredCounter); } @Test public void testCheckSuppressedExceptionsReturningEmptySuppressedExceptions() { { Throwable[] suppressed = ClassUsingTryWithResources.checkSuppressedExceptions(false); assertThat(suppressed).isEmpty(); } try { Throwable[] suppressed = (Throwable[]) desugaredClass .getMethod("checkSuppressedExceptions", boolean.class) .invoke(null, Boolean.FALSE); assertThat(suppressed).isEmpty(); } catch (Exception e) { e.printStackTrace(); throw new AssertionError(e); } } @Test public void testPrintStackTraceOfCaughtException() { { String trace = ClassUsingTryWithResources.printStackTraceOfCaughtException(); assertThat(trace.toLowerCase()).contains("suppressed"); } try { String trace = (String) desugaredClass.getMethod("printStackTraceOfCaughtException").invoke(null); if (isMimicStrategy()) { assertThat(trace.toLowerCase()).contains("suppressed"); } else if (isReuseStrategy()) { assertThat(trace.toLowerCase()).contains("suppressed"); } else if (isNullStrategy()) { assertThat(trace.toLowerCase()).doesNotContain("suppressed"); } else { fail("unexpected desugaring strategy " + ThrowableExtension.getStrategy()); } } catch (Exception e) { e.printStackTrace(); throw new AssertionError(e); } } @Test public void testCheckSuppressedExceptionReturningOneSuppressedException() { { Throwable[] suppressed = ClassUsingTryWithResources.checkSuppressedExceptions(true); assertThat(suppressed).hasLength(1); } try { Throwable[] suppressed = (Throwable[]) desugaredClass .getMethod("checkSuppressedExceptions", boolean.class) .invoke(null, Boolean.TRUE); if (isMimicStrategy()) { assertThat(suppressed).hasLength(1); } else if (isReuseStrategy()) { assertThat(suppressed).hasLength(1); } else if (isNullStrategy()) { assertThat(suppressed).isEmpty(); } else { fail("unexpected desugaring strategy " + ThrowableExtension.getStrategy()); } } catch (Exception e) { e.printStackTrace(); throw new AssertionError(e); } } @Test public void testSimpleTryWithResources() throws Throwable { { try { ClassUsingTryWithResources.simpleTryWithResources(); fail("Expected RuntimeException"); } catch (RuntimeException expected) { assertThat(expected.getClass()).isEqualTo(RuntimeException.class); assertThat(expected.getSuppressed()).hasLength(1); assertThat(expected.getSuppressed()[0].getClass()).isEqualTo(IOException.class); } } try { try { desugaredClass.getMethod("simpleTryWithResources").invoke(null); fail("Expected RuntimeException"); } catch (InvocationTargetException e) { throw e.getCause(); } } catch (RuntimeException expected) { String expectedStrategyName = getTwrStrategyClassNameSpecifiedInSystemProperty(); assertThat(getStrategyClassName()).isEqualTo(expectedStrategyName); if (isMimicStrategy()) { assertThat(expected.getSuppressed()).isEmpty(); assertThat(ThrowableExtension.getSuppressed(expected)).hasLength(1); assertThat(ThrowableExtension.getSuppressed(expected)[0].getClass()) .isEqualTo(IOException.class); } else if (isReuseStrategy()) { assertThat(expected.getSuppressed()).hasLength(1); assertThat(expected.getSuppressed()[0].getClass()).isEqualTo(IOException.class); assertThat(ThrowableExtension.getSuppressed(expected)[0].getClass()) .isEqualTo(IOException.class); } else if (isNullStrategy()) { assertThat(expected.getSuppressed()).isEmpty(); assertThat(ThrowableExtension.getSuppressed(expected)).isEmpty(); } else { fail("unexpected desugaring strategy " + ThrowableExtension.getStrategy()); } } } private static void assertDesugaringBehavior( DesugaredThrowableMethodCallCounter orig, DesugaredThrowableMethodCallCounter desugared) { assertThat(desugared.countThrowableGetSuppressed()).isEqualTo(orig.countExtGetSuppressed()); assertThat(desugared.countThrowableAddSuppressed()).isEqualTo(orig.countExtAddSuppressed()); assertThat(desugared.countThrowablePrintStackTrace()).isEqualTo(orig.countExtPrintStackTrace()); assertThat(desugared.countThrowablePrintStackTracePrintStream()) .isEqualTo(orig.countExtPrintStackTracePrintStream()); assertThat(desugared.countThrowablePrintStackTracePrintWriter()) .isEqualTo(orig.countExtPrintStackTracePrintWriter()); assertThat(orig.countThrowableGetSuppressed()).isEqualTo(desugared.countExtGetSuppressed()); // $closeResource may be specialized into multiple versions. assertThat(orig.countThrowableAddSuppressed()).isAtMost(desugared.countExtAddSuppressed()); assertThat(orig.countThrowablePrintStackTrace()).isEqualTo(desugared.countExtPrintStackTrace()); assertThat(orig.countThrowablePrintStackTracePrintStream()) .isEqualTo(desugared.countExtPrintStackTracePrintStream()); assertThat(orig.countThrowablePrintStackTracePrintWriter()) .isEqualTo(desugared.countExtPrintStackTracePrintWriter()); if (orig.getSyntheticCloseResourceCount() > 0) { // Depending on the specific javac version, $closeResource(Throwable, AutoCloseable) may not // be there. assertThat(orig.getSyntheticCloseResourceCount()).isEqualTo(1); assertThat(desugared.getSyntheticCloseResourceCount()).isAtLeast(1); } assertThat(desugared.countThrowablePrintStackTracePrintStream()).isEqualTo(0); assertThat(desugared.countThrowablePrintStackTracePrintStream()).isEqualTo(0); assertThat(desugared.countThrowablePrintStackTracePrintWriter()).isEqualTo(0); assertThat(desugared.countThrowableAddSuppressed()).isEqualTo(0); assertThat(desugared.countThrowableGetSuppressed()).isEqualTo(0); } private static DesugaredThrowableMethodCallCounter countDesugaredThrowableMethodCalls( Class klass) { try { ClassReader reader = new ClassReader(klass.getName()); DesugaredThrowableMethodCallCounter counter = new DesugaredThrowableMethodCallCounter(klass.getClassLoader()); reader.accept(counter, 0); return counter; } catch (IOException e) { e.printStackTrace(); fail(e.toString()); return null; } } private static DesugaredThrowableMethodCallCounter countDesugaredThrowableMethodCalls( byte[] content, ClassLoader loader) { ClassReader reader = new ClassReader(content); DesugaredThrowableMethodCallCounter counter = new DesugaredThrowableMethodCallCounter(loader); reader.accept(counter, 0); return counter; } /** Check whether java.lang.AutoCloseable is used as arguments of any method. */ private static boolean hasAutoCloseable(byte[] classContent) { ClassReader reader = new ClassReader(classContent); final AtomicInteger counter = new AtomicInteger(); ClassVisitor visitor = new ClassVisitor(Opcodes.ASM5) { @Override public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { for (Type argumentType : Type.getArgumentTypes(desc)) { if ("Ljava/lang/AutoCloseable;".equals(argumentType.getDescriptor())) { counter.incrementAndGet(); } } return null; } }; reader.accept(visitor, 0); return counter.get() > 0; } private static class DesugaredThrowableMethodCallCounter extends ClassVisitor { private final ClassLoader classLoader; private final Map counterMap; private int syntheticCloseResourceCount; public DesugaredThrowableMethodCallCounter(ClassLoader loader) { super(ASM5); classLoader = loader; counterMap = new HashMap<>(); TryWithResourcesRewriter.TARGET_METHODS .entries() .forEach(entry -> counterMap.put(entry.getKey() + entry.getValue(), new AtomicInteger())); TryWithResourcesRewriter.TARGET_METHODS .entries() .forEach( entry -> counterMap.put( entry.getKey() + TryWithResourcesRewriter.METHOD_DESC_MAP.get(entry.getValue()), new AtomicInteger())); } @Override public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { if (BitFlags.isSet(access, Opcodes.ACC_SYNTHETIC | Opcodes.ACC_STATIC) && name.equals("$closeResource") && Type.getArgumentTypes(desc).length == 2 && Type.getArgumentTypes(desc)[0].getDescriptor().equals("Ljava/lang/Throwable;")) { ++syntheticCloseResourceCount; } return new InvokeCounter(); } private class InvokeCounter extends MethodVisitor { public InvokeCounter() { super(ASM5); } private boolean isAssignableToThrowable(String owner) { try { Class ownerClass = classLoader.loadClass(owner.replace('/', '.')); return Throwable.class.isAssignableFrom(ownerClass); } catch (ClassNotFoundException e) { throw new AssertionError(e); } } @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { String signature = name + desc; if ((opcode == INVOKEVIRTUAL && isAssignableToThrowable(owner)) || (opcode == INVOKESTATIC && Type.getInternalName(ThrowableExtension.class).equals(owner))) { AtomicInteger counter = counterMap.get(signature); if (counter == null) { return; } counter.incrementAndGet(); } } } public int getSyntheticCloseResourceCount() { return syntheticCloseResourceCount; } public int countThrowableAddSuppressed() { return counterMap.get("addSuppressed(Ljava/lang/Throwable;)V").get(); } public int countThrowableGetSuppressed() { return counterMap.get("getSuppressed()[Ljava/lang/Throwable;").get(); } public int countThrowablePrintStackTrace() { return counterMap.get("printStackTrace()V").get(); } public int countThrowablePrintStackTracePrintStream() { return counterMap.get("printStackTrace(Ljava/io/PrintStream;)V").get(); } public int countThrowablePrintStackTracePrintWriter() { return counterMap.get("printStackTrace(Ljava/io/PrintWriter;)V").get(); } public int countExtAddSuppressed() { return counterMap.get("addSuppressed(Ljava/lang/Throwable;Ljava/lang/Throwable;)V").get(); } public int countExtGetSuppressed() { return counterMap.get("getSuppressed(Ljava/lang/Throwable;)[Ljava/lang/Throwable;").get(); } public int countExtPrintStackTrace() { return counterMap.get("printStackTrace(Ljava/lang/Throwable;)V").get(); } public int countExtPrintStackTracePrintStream() { return counterMap.get("printStackTrace(Ljava/lang/Throwable;Ljava/io/PrintStream;)V").get(); } public int countExtPrintStackTracePrintWriter() { return counterMap.get("printStackTrace(Ljava/lang/Throwable;Ljava/io/PrintWriter;)V").get(); } } private static class DesugaringClassLoader extends ClassLoader { private final String targetedClassName; private Class klass; private byte[] classContent; private final Set visitedExceptionTypes = new HashSet<>(); private final AtomicInteger numOfTryWithResourcesInvoked = new AtomicInteger(); public DesugaringClassLoader(String targetedClassName) { super(DesugaringClassLoader.class.getClassLoader()); this.targetedClassName = targetedClassName; } @Override protected Class findClass(String name) throws ClassNotFoundException { if (name.equals(targetedClassName)) { if (klass != null) { return klass; } // desugar the class, and return the desugared one. classContent = desugarTryWithResources(name); klass = defineClass(name, classContent, 0, classContent.length); return klass; } else { return super.findClass(name); } } private byte[] desugarTryWithResources(String className) { try { ClassReader reader = new ClassReader(className); CloseResourceMethodScanner scanner = new CloseResourceMethodScanner(); reader.accept(scanner, ClassReader.SKIP_DEBUG); ClassWriter writer = new ClassWriter(reader, COMPUTE_MAXS); TryWithResourcesRewriter rewriter = new TryWithResourcesRewriter( writer, TryWithResourcesRewriterTest.class.getClassLoader(), visitedExceptionTypes, numOfTryWithResourcesInvoked, scanner.hasCloseResourceMethod()); reader.accept(rewriter, 0); return writer.toByteArray(); } catch (IOException e) { fail(e.toString()); return null; // suppress compiler error. } } } }