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());
+  }
+}