| // 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. |
| } |
| } |
| } |
| } |