blob: dc0da22d47307053e17c64b785c0ddb4e691401a [file] [log] [blame]
// 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.
}
}
}
}