com.google.devtools.build.android.desugar.TryWithResourcesRewriterTest.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.android.desugar.TryWithResourcesRewriterTest.java

Source

// 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.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 is rewritten to ThrowableExtension.closeResource, so addSuppressed() is called
        // in the runtime library now.
        assertThat(orig.countThrowableAddSuppressed()).isAtLeast(desugared.countThrowableAddSuppressed());
        assertThat(orig.countThrowablePrintStackTrace()).isEqualTo(desugared.countExtPrintStackTrace());
        assertThat(orig.countThrowablePrintStackTracePrintStream())
                .isEqualTo(desugared.countExtPrintStackTracePrintStream());
        assertThat(orig.countThrowablePrintStackTracePrintWriter())
                .isEqualTo(desugared.countExtPrintStackTracePrintWriter());

        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;

        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) {
            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 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);
                ClassWriter writer = new ClassWriter(reader, COMPUTE_MAXS);
                TryWithResourcesRewriter rewriter = new TryWithResourcesRewriter(writer,
                        TryWithResourcesRewriterTest.class.getClassLoader(), visitedExceptionTypes,
                        numOfTryWithResourcesInvoked);
                reader.accept(rewriter, 0);
                return writer.toByteArray();
            } catch (IOException e) {
                fail(e.toString());
                return null; // suppress compiler error.
            }
        }
    }
}