Add MinificationTest to debug tests:
- runs R8 with and without minification
- tests class/method renaming and source line numbers
Bug:
Change-Id: If9884c856a05e619f0ba647071d3f32d6c610056
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 22d08f5..7eff6fa 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -174,7 +174,7 @@
return self();
}
- Builder setProguardMapOutput(Path path) {
+ public Builder setProguardMapOutput(Path path) {
this.proguardMapOutput = path;
return self();
}
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNaming.java b/src/main/java/com/android/tools/r8/naming/ClassNaming.java
index a00455f..74c5006 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNaming.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNaming.java
@@ -7,7 +7,9 @@
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
+import java.util.ArrayList;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
@@ -19,7 +21,7 @@
public class ClassNaming {
public final String originalName;
- final String renamedName;
+ public final String renamedName;
/**
* Mapping from the renamed signature to the naming information for a member.
@@ -52,6 +54,16 @@
return null;
}
+ public List<MemberNaming> lookupByOriginalName(String originalName) {
+ List<MemberNaming> result = new ArrayList<>();
+ for (MemberNaming naming : members.values()) {
+ if (naming.signature.name.equals(originalName)) {
+ result.add(naming);
+ }
+ }
+ return result;
+ }
+
public void forAllMemberNaming(Consumer<MemberNaming> consumer) {
members.values().forEach(consumer);
}
diff --git a/src/main/java/com/android/tools/r8/naming/MemberNaming.java b/src/main/java/com/android/tools/r8/naming/MemberNaming.java
index 762bdb6..5f2ea93 100644
--- a/src/main/java/com/android/tools/r8/naming/MemberNaming.java
+++ b/src/main/java/com/android/tools/r8/naming/MemberNaming.java
@@ -8,6 +8,7 @@
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.naming.MemberNaming.Signature.SignatureKind;
+import com.android.tools.r8.utils.DescriptorUtils;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
@@ -15,6 +16,7 @@
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
+import org.objectweb.asm.Type;
/**
* Stores renaming information for a member.
@@ -281,6 +283,20 @@
method.proto.returnType.toSourceString(), paramNames);
}
+ public static MethodSignature fromSignature(String name, String signature) {
+ Type[] parameterDescriptors = Type.getArgumentTypes(signature);
+ Type returnDescriptor = Type.getReturnType(signature);
+ String[] parameterTypes = new String[parameterDescriptors.length];
+ for (int i = 0; i < parameterDescriptors.length; i++) {
+ parameterTypes[i] =
+ DescriptorUtils.descriptorToJavaType(parameterDescriptors[i].getDescriptor());
+ }
+ return new MethodSignature(
+ name,
+ DescriptorUtils.descriptorToJavaType(returnDescriptor.getDescriptor()),
+ parameterTypes);
+ }
+
public static MethodSignature initializer(String[] parameters) {
return new MethodSignature(Constants.INSTANCE_INITIALIZER_NAME, "void", parameters);
}
diff --git a/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java b/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java
index 9ceee1f..c0aef0a 100644
--- a/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java
+++ b/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java
@@ -45,7 +45,12 @@
@Override
public void writeProguardMapFile(byte[] contents) throws IOException {
- writeToFile(options.proguardConfiguration.getPrintMappingFile(), System.out, contents);
+ if (options.proguardConfiguration.getPrintMappingFile() != null) {
+ writeToFile(options.proguardConfiguration.getPrintMappingFile(), System.out, contents);
+ }
+ if (options.proguardMapOutput != null) {
+ writeToFile(options.proguardMapOutput, System.out, contents);
+ }
}
@Override
diff --git a/src/test/debugTestResources/Minified.java b/src/test/debugTestResources/Minified.java
new file mode 100644
index 0000000..af90776
--- /dev/null
+++ b/src/test/debugTestResources/Minified.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+public class Minified {
+ private static class Inner {
+ public static int innerTest() {
+ System.out.println("innerTest");
+ return 0;
+ }
+ }
+
+ private static void test() {
+ System.out.println("test");
+ Inner.innerTest();
+ }
+
+ private static void test(int i) {
+ System.out.println("test" + i);
+ }
+
+ public static void main(String[] args) {
+ test();
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index 3b12b43..35760ef 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -13,6 +13,12 @@
import com.android.tools.r8.ToolHelper.DexVm.Version;
import com.android.tools.r8.ToolHelper.ProcessResult;
import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.naming.ClassNaming;
+import com.android.tools.r8.naming.MemberNaming;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.naming.MemberNaming.Signature;
+import com.android.tools.r8.naming.ProguardMapReader;
import com.android.tools.r8.shaking.ProguardConfiguration;
import com.android.tools.r8.shaking.ProguardRuleParserException;
import com.android.tools.r8.utils.AndroidApiLevel;
@@ -23,6 +29,7 @@
import it.unimi.dsi.fastutil.longs.LongList;
import java.io.File;
import java.io.IOException;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayDeque;
@@ -110,6 +117,7 @@
.get(ToolHelper.BUILD_DIR, "test", "debug_test_resources_java8.jar");
private static final Path DEBUGGEE_KOTLIN_JAR = Paths
.get(ToolHelper.BUILD_DIR, "test", "debug_test_resources_kotlin.jar");
+ private static final String PROGUARD_MAP_FILENAME = "proguard.map";
@ClassRule
public static TemporaryFolder temp = new TemporaryFolder();
@@ -127,6 +135,9 @@
setUp(null, null);
}
+ protected static List<String> proguardConfigurations = Collections.<String>emptyList();
+ protected static boolean writeProguardMap = false;
+
protected static void setUp(
Consumer<InternalOptions> optionsConsumer,
Consumer<ProguardConfiguration.Builder> pgConsumer)
@@ -206,6 +217,12 @@
.setMinApiLevel(minSdk)
.setMode(CompilationMode.DEBUG)
.addLibraryFiles(Paths.get(ToolHelper.getAndroidJar(minSdk)));
+ if (writeProguardMap) {
+ builder.setProguardMapOutput(dexOutputDir.resolve(PROGUARD_MAP_FILENAME));
+ }
+ if (!proguardConfigurations.isEmpty()) {
+ builder.addProguardConfiguration(proguardConfigurations);
+ }
if (pgConsumer != null) {
builder.addProguardConfigurationConsumer(pgConsumer);
}
@@ -326,17 +343,23 @@
String[] paths = new String[extraPaths.size() + 2];
int indexPath = 0;
+ ClassNameMapper classNameMapper = null;
if (RUNTIME_KIND == RuntimeKind.JAVA) {
paths[indexPath++] = JDWP_JAR.toString();
paths[indexPath++] = languageFeatures.getJarPath().toString();
} else {
paths[indexPath++] = jdwpDexD8.toString();
paths[indexPath++] = languageFeatures.getDexPath().toString();
+ Path proguardMapPath = Paths.get(paths[indexPath - 1]).resolveSibling(PROGUARD_MAP_FILENAME);
+ if (Files.exists(proguardMapPath)) {
+ classNameMapper = ProguardMapReader.mapperFromFile(proguardMapPath);
+ }
}
for (Path extraPath : extraPaths) {
paths[indexPath++] = extraPath.toString();
}
- new JUnit3Wrapper(debuggeeClass, paths, commands).runBare();
+
+ new JUnit3Wrapper(debuggeeClass, paths, commands, classNameMapper).runBare();
}
protected final JUnit3Wrapper.Command run() {
@@ -513,14 +536,173 @@
private DebuggeeState debuggeeState = null;
private final Deque<Command> commandsQueue;
+ private final Translator translator;
// Active event requests.
private final Map<Integer, EventHandler> events = new TreeMap<>();
- JUnit3Wrapper(String debuggeeClassName, String[] debuggeePath, List<Command> commands) {
+ /**
+ * The Translator interface provides mapping between the class and method names and line numbers
+ * found in the binary file and their original forms.
+ *
+ * <p>Terminology:
+ *
+ * <p>The term 'original' refers to the names and line numbers found in the original source
+ * code. The term 'obfuscated' refers to the names and line numbers in the binary. Note that
+ * they may not actually be obfuscated:
+ *
+ * <p>- The obfuscated class and method names can be identical to the original ones if
+ * minification is disabled or they are 'keep' classes/methods. - The obfuscated line numbers
+ * can be identical to the original ones if neither inlining nor line number remapping took
+ * place.
+ */
+ private interface Translator {
+ public String getOriginalClassName(String obfuscatedClassName);
+
+ public String getOriginalMethodName(
+ String obfuscatedClassName, String obfuscatedMethodName, String methodSignature);
+
+ public int getOriginalLineNumber(int obfuscatedLineNumber);
+
+ public String getObfuscatedClassName(String originalClassName);
+
+ public String getObfuscatedMethodName(
+ String originalClassName, String originalMethodName, String methodSignature);
+ }
+
+ private class IdentityTranslator implements Translator {
+
+ @Override
+ public String getOriginalClassName(String obfuscatedClassName) {
+ return obfuscatedClassName;
+ }
+
+ @Override
+ public String getOriginalMethodName(
+ String obfuscatedClassName, String obfuscatedMethodName, String methodSignature) {
+ return obfuscatedMethodName;
+ }
+
+ @Override
+ public int getOriginalLineNumber(int obfuscatedLineNumber) {
+ return obfuscatedLineNumber;
+ }
+
+ @Override
+ public String getObfuscatedClassName(String originalClassName) {
+ return originalClassName;
+ }
+
+ @Override
+ public String getObfuscatedMethodName(
+ String originalClassName, String originalMethodName, String methodSignature) {
+ return originalMethodName;
+ }
+ }
+
+ private class ClassNameMapperTranslator extends IdentityTranslator {
+ private final ClassNameMapper classNameMapper;
+
+ public ClassNameMapperTranslator(ClassNameMapper classNameMapper) {
+ this.classNameMapper = classNameMapper;
+ }
+
+ @Override
+ public String getOriginalClassName(String obfuscatedClassName) {
+ // TODO(tamaskenez) Watch for inline methods (we can be in a different class).
+ return classNameMapper.deobfuscateClassName(obfuscatedClassName);
+ }
+
+ @Override
+ public String getOriginalMethodName(
+ String obfuscatedClassName, String obfuscatedMethodName, String methodSignature) {
+ MemberNaming memberNaming =
+ getMemberNaming(obfuscatedClassName, obfuscatedMethodName, methodSignature);
+ if (memberNaming == null) {
+ return obfuscatedMethodName;
+ }
+
+ Signature originalSignature = memberNaming.getOriginalSignature();
+ return originalSignature.name;
+ }
+
+ @Override
+ public int getOriginalLineNumber(int obfuscatedLineNumber) {
+ return obfuscatedLineNumber;
+ // TODO(tamaskenez) Map possibly reassigned line number to original, watch for inline.
+ // methods
+ }
+
+ @Override
+ public String getObfuscatedClassName(String originalClassName) {
+ // TODO(tamaskenez) Watch for inline methods (we can be in a different class).
+ String obfuscatedClassName =
+ classNameMapper.getObfuscatedToOriginalMapping().inverse().get(originalClassName);
+ return obfuscatedClassName == null ? originalClassName : obfuscatedClassName;
+ }
+
+ @Override
+ public String getObfuscatedMethodName(
+ String originalClassName, String originalMethodName, String methodSignatureOrNull) {
+ ClassNaming naming;
+ String obfuscatedClassName =
+ classNameMapper.getObfuscatedToOriginalMapping().inverse().get(originalClassName);
+ if (obfuscatedClassName != null) {
+ naming = classNameMapper.getClassNaming(obfuscatedClassName);
+ } else {
+ return originalMethodName;
+ }
+
+ if (methodSignatureOrNull == null) {
+ List<MemberNaming> memberNamings = naming.lookupByOriginalName(originalMethodName);
+ if (memberNamings.isEmpty()) {
+ return originalMethodName;
+ } else if (memberNamings.size() == 1) {
+ return memberNamings.get(0).getRenamedName();
+ } else
+ throw new RuntimeException(
+ String.format(
+ "Looking up method %s.%s without signature is ambiguous (%d candidates).",
+ originalClassName, originalMethodName, memberNamings.size()));
+ } else {
+ MethodSignature originalSignature =
+ MethodSignature.fromSignature(originalMethodName, methodSignatureOrNull);
+ MemberNaming memberNaming = naming.lookupByOriginalSignature(originalSignature);
+ if (memberNaming == null) {
+ return originalMethodName;
+ }
+
+ return memberNaming.getRenamedName();
+ }
+ }
+
+ /** Assumes classNameMapper is valid. Return null if no member naming found. */
+ private MemberNaming getMemberNaming(
+ String obfuscatedClassName, String obfuscatedMethodName, String genericMethodSignature) {
+ ClassNaming classNaming = classNameMapper.getClassNaming(obfuscatedClassName);
+ if (classNaming == null) {
+ return null;
+ }
+
+ MethodSignature renamedSignature =
+ MethodSignature.fromSignature(obfuscatedMethodName, genericMethodSignature);
+ return classNaming.lookup(renamedSignature);
+ }
+ }
+
+ JUnit3Wrapper(
+ String debuggeeClassName,
+ String[] debuggeePath,
+ List<Command> commands,
+ ClassNameMapper classNameMapper) {
this.debuggeeClassName = debuggeeClassName;
this.debuggeePath = debuggeePath;
this.commandsQueue = new ArrayDeque<>(commands);
+ if (classNameMapper == null) {
+ this.translator = new IdentityTranslator();
+ } else {
+ this.translator = new ClassNameMapperTranslator(classNameMapper);
+ }
}
@Override
@@ -733,10 +915,12 @@
private final long frameId;
private final Location location;
+ private final Translator translator;
- public DebuggeeFrame(long frameId, Location location) {
+ public DebuggeeFrame(long frameId, Location location, Translator translator) {
this.frameId = frameId;
this.location = location;
+ this.translator = translator;
}
public long getFrameId() {
@@ -747,7 +931,7 @@
return location;
}
- public int getLineNumber() {
+ private int getObfuscatedLineNumber() {
Location location = getLocation();
ReplyPacket reply = getMirror().getLineTable(location.classID, location.methodID);
if (reply.getErrorCode() != 0) {
@@ -775,10 +959,13 @@
break;
}
}
-
return line;
}
+ public int getLineNumber() {
+ return translator.getOriginalLineNumber(getObfuscatedLineNumber());
+ }
+
public String getSourceFile() {
// TODO(shertz) support JSR-45
Location location = getLocation();
@@ -874,7 +1061,11 @@
expectedValue, localValue);
}
- public String getClassName() {
+ /**
+ * Return class name, as found in the binary. If it has not been obfuscated (minified) it's
+ * identical to the original class name. Otherwise, it's the obfuscated one.
+ */
+ private String getObfuscatedClassName() {
String classSignature = getClassSignature();
assert classSignature.charAt(0) == 'L';
// Remove leading 'L' and trailing ';'
@@ -883,16 +1074,27 @@
return classSignature.replace('/', '.');
}
+ public String getClassName() {
+ return translator.getOriginalClassName(getObfuscatedClassName());
+ }
+
public String getClassSignature() {
Location location = getLocation();
return getMirror().getClassSignature(location.classID);
}
- public String getMethodName() {
+ // Return method name as found in the binary. Can be obfuscated (minified).
+ private String getObfuscatedMethodName() {
Location location = getLocation();
return getMirror().getMethodName(location.classID, location.methodID);
}
+ // Return original method name.
+ public String getMethodName() {
+ return translator.getOriginalMethodName(
+ getObfuscatedClassName(), getObfuscatedMethodName(), getMethodSignature());
+ }
+
public String getMethodSignature() {
Location location = getLocation();
CommandPacket command = new CommandPacket(ReferenceTypeCommandSet.CommandSetID,
@@ -1138,7 +1340,7 @@
for (int i = 0; i < frameCount; ++i) {
long frameId = replyPacket.getNextValueAsFrameID();
Location location = replyPacket.getNextValueAsLocation();
- frames.add(debuggeeState.new DebuggeeFrame(frameId, location));
+ frames.add(debuggeeState.new DebuggeeFrame(frameId, location, translator));
}
assertAllDataRead(replyPacket);
}
@@ -1207,7 +1409,8 @@
@Override
public void perform(JUnit3Wrapper testBase) {
VmMirror mirror = testBase.getMirror();
- String classSignature = getClassSignature(className);
+ String obfuscatedClassName = testBase.translator.getObfuscatedClassName(className);
+ String classSignature = getClassSignature(obfuscatedClassName);
byte typeTag = TypeTag.CLASS;
long classId = mirror.getClassID(classSignature);
if (classId == -1) {
@@ -1220,7 +1423,7 @@
// breakpoint.
assert requestedClassPrepare == false : "Already requested class prepare";
requestedClassPrepare = true;
- ReplyPacket replyPacket = mirror.setClassPrepared(className);
+ ReplyPacket replyPacket = mirror.setClassPrepared(obfuscatedClassName);
final int classPrepareRequestId = replyPacket.getNextValueAsInt();
testBase.events.put(Integer.valueOf(classPrepareRequestId), wrapper -> {
// Remove the CLASS_PREPARE
@@ -1237,7 +1440,10 @@
});
} else {
// The class is available: lookup the method then set the breakpoint.
- long breakpointMethodId = findMethod(mirror, classId, methodName, methodSignature);
+ String obfuscatedMethodName =
+ testBase.translator.getObfuscatedMethodName(className, methodName, methodSignature);
+ long breakpointMethodId =
+ findMethod(mirror, classId, obfuscatedMethodName, methodSignature);
long index = testBase.getMethodFirstCodeIndex(classId, breakpointMethodId);
Assert.assertTrue("No code in method", index >= 0);
ReplyPacket replyPacket = testBase.getMirror().setBreakpoint(
diff --git a/src/test/java/com/android/tools/r8/debug/MinificationTest.java b/src/test/java/com/android/tools/r8/debug/MinificationTest.java
new file mode 100644
index 0000000..a29562a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/MinificationTest.java
@@ -0,0 +1,96 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.debug;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+/** Tests local variable information. */
+@RunWith(Parameterized.class)
+public class MinificationTest extends DebugTestBase {
+
+ public static final String SOURCE_FILE = "Minified.java";
+
+ @Parameterized.Parameters(name = "minification: {0}, proguardMap: {1}")
+ public static Collection minificationControl() {
+ return Arrays.asList(new Object[][] {{false, false}, {true, false}, {true, true}});
+ }
+
+ private static boolean firstRun = true;
+ private static boolean minificationEnabled;
+
+ public MinificationTest(boolean enableMinification, boolean writeProguardMap) throws Exception {
+ // TODO(tamaskenez) The way we're shadowing and calling the static setUp() methods should be
+ // updated when we refactor DebugTestBase.
+ if (firstRun
+ || this.minificationEnabled != enableMinification
+ || this.writeProguardMap != writeProguardMap) {
+
+ firstRun = false;
+ this.minificationEnabled = enableMinification;
+ this.writeProguardMap = writeProguardMap;
+
+ if (minificationEnabled) {
+ proguardConfigurations =
+ ImmutableList.of(
+ "-keep public class Minified { public static void main(java.lang.String[]); }");
+ setUp(
+ null,
+ pg -> {
+ pg.addKeepAttributePatterns(ImmutableList.of("SourceFile", "LineNumberTable"));
+ });
+ } else {
+ setUp(null, null);
+ }
+ }
+ }
+
+ @BeforeClass
+ public static void setUp() throws Exception {}
+
+ @Test
+ public void testBreakInMainClass() throws Throwable {
+ boolean minifiedNames = (minificationEnabled && !writeProguardMap);
+ final String className = "Minified";
+ final String methodName = minifiedNames ? "a" : "test";
+ final String signature = "()V";
+ final String innerClassName = minifiedNames ? "a" : "Minified$Inner";
+ final String innerMethodName = minifiedNames ? "a" : "innerTest";
+ final String innerSignature = "()I";
+ runDebugTestR8(
+ className,
+ breakpoint(className, methodName, signature),
+ run(),
+ checkMethod(className, methodName, signature),
+ checkLine(SOURCE_FILE, 14),
+ stepOver(),
+ checkMethod(className, methodName, signature),
+ checkLine(SOURCE_FILE, 15),
+ stepInto(),
+ checkMethod(innerClassName, innerMethodName, innerSignature),
+ checkLine(SOURCE_FILE, 8),
+ run());
+ }
+
+ @Test
+ public void testBreakInPossiblyRenamedClass() throws Throwable {
+ boolean minifiedNames = (minificationEnabled && !writeProguardMap);
+ final String className = "Minified";
+ final String innerClassName = minifiedNames ? "a" : "Minified$Inner";
+ final String innerMethodName = minifiedNames ? "a" : "innerTest";
+ final String innerSignature = "()I";
+ runDebugTestR8(
+ className,
+ breakpoint(innerClassName, innerMethodName, innerSignature),
+ run(),
+ checkMethod(innerClassName, innerMethodName, innerSignature),
+ checkLine(SOURCE_FILE, 8),
+ run());
+ }
+}