Home | History | Annotate | Download | only in desugar
      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