| // 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<String, AtomicInteger> 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<String> 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. | |
| } | |
| } | |
| } | |
| } |