Overwrite SourceFile if -renamesourcefileattribute is set.

SourceFile appears at class_def_item and debug_info_item as indices.
When -renamesourcefileattribute is set, SourceFileRewriter will
1) visit all program classes and overwrite sourceFile; and
2) visit all method definitions in a class and set sourceFile in
  debug_info_item directly.

Bug: 36799675
Change-Id: I6b1e3afe0c41e51cdc8f32564209d45b54b62e94
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 3d0cee7..45af2b3 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.ir.optimize.SwitchMapCollector;
 import com.android.tools.r8.naming.Minifier;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.naming.SourceFileRewriter;
 import com.android.tools.r8.optimize.BridgeMethodAnalysis;
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
 import com.android.tools.r8.optimize.VisibilityBridgeRemover;
@@ -272,6 +273,11 @@
 
       application = optimize(application, appInfo, graphLense, executorService);
 
+      // Overwrite SourceFile if specified. This step should be done after IR conversion.
+      timing.begin("Rename SourceFile");
+      new SourceFileRewriter(appInfo, options).run();
+      timing.end();
+
       if (!options.mainDexKeepRules.isEmpty()) {
         appInfo = new AppInfoWithSubtyping(application);
         Enqueuer enqueuer = new Enqueuer(appInfo);
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index e350c33..5de6538 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -22,7 +22,7 @@
   public final DexAccessFlags accessFlags;
   public DexType superType;
   public DexTypeList interfaces;
-  public final DexString sourceFile;
+  public DexString sourceFile;
   protected DexEncodedField[] staticFields;
   protected DexEncodedField[] instanceFields;
   protected DexEncodedMethod[] directMethods;
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java b/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
index db99359..9381c90 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
@@ -294,12 +294,20 @@
 
   public static class SetFile extends DexDebugEvent {
 
-    final DexString fileName;
+    DexString fileName;
 
     SetFile(DexString fileName) {
       this.fileName = fileName;
     }
 
+    public DexString getFileName() {
+      return fileName;
+    }
+
+    public void setFileName(DexString fileName) {
+      this.fileName = fileName;
+    }
+
     public void writeOn(DebugBytecodeWriter writer, ObjectToOffsetMapping mapping) {
       writer.putByte(Constants.DBG_SET_FILE);
       writer.putString(fileName);
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugInfo.java b/src/main/java/com/android/tools/r8/graph/DexDebugInfo.java
index d2c9b3b..5236d12 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugInfo.java
@@ -12,7 +12,7 @@
 
   public final int startLine;
   public final DexString[] parameters;
-  public final DexDebugEvent[] events;
+  public DexDebugEvent[] events;
 
   public DexDebugInfo(int startLine, DexString[] parameters, DexDebugEvent[] events) {
     assert startLine >= 0;
diff --git a/src/main/java/com/android/tools/r8/naming/SourceFileRewriter.java b/src/main/java/com/android/tools/r8/naming/SourceFileRewriter.java
new file mode 100644
index 0000000..38f8cb3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/naming/SourceFileRewriter.java
@@ -0,0 +1,66 @@
+// 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.naming;
+
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexDebugEvent;
+import com.android.tools.r8.graph.DexDebugEvent.SetFile;
+import com.android.tools.r8.graph.DexDebugInfo;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.utils.InternalOptions;
+import java.util.Arrays;
+
+/**
+ * Visit program {@link DexClass}es and replace their sourceFile with the given string.
+ *
+ * If -keepattribute SourceFile is not set, we rather remove that attribute.
+ */
+public class SourceFileRewriter {
+
+  private final AppInfo appInfo;
+  private final InternalOptions options;
+
+  public SourceFileRewriter(AppInfo appInfo, InternalOptions options) {
+    this.appInfo = appInfo;
+    this.options = options;
+  }
+
+  public void run() {
+    String renameSourceFile = options.proguardConfiguration.getRenameSourceFileAttribute();
+    // Return early if a user wants to keep the current source file attribute as-is.
+    if (renameSourceFile == null && options.keepAttributes.sourceFile) {
+      return;
+    }
+    // Now, the user wants either to remove source file attribute or to rename it.
+    DexString dexRenameSourceFile =
+        renameSourceFile == null
+            ? appInfo.dexItemFactory.createString("")
+            : appInfo.dexItemFactory.createString(renameSourceFile);
+    for (DexClass clazz : appInfo.classes()) {
+      clazz.sourceFile = dexRenameSourceFile;
+      clazz.forEachMethod(encodedMethod -> {
+        // Abstract methods do not have code_item.
+        if (encodedMethod.accessFlags.isAbstract()) {
+          return;
+        }
+        Code code = encodedMethod.getCode();
+        // Other kinds of {@link Code} do not have debug_info_item.
+        if (code == null || !code.isDexCode()) {
+          return;
+        }
+        if (code.asDexCode().getDebugInfo() == null) {
+          return;
+        }
+        // Thanks to a single global source file, we can safely remove DBG_SET_FILE entirely.
+        DexDebugInfo dexDebugInfo = code.asDexCode().getDebugInfo();
+        dexDebugInfo.events =
+            Arrays.stream(dexDebugInfo.events)
+                .filter(dexDebugEvent -> !(dexDebugEvent instanceof SetFile))
+                .toArray(DexDebugEvent[]::new);
+      });
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/FileUtils.java b/src/main/java/com/android/tools/r8/utils/FileUtils.java
index 47e809c..3eaa2be 100644
--- a/src/main/java/com/android/tools/r8/utils/FileUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/FileUtils.java
@@ -26,6 +26,7 @@
   public static final String JAR_EXTENSION = ".jar";
   public static final String ZIP_EXTENSION = ".zip";
   public static final String DEFAULT_DEX_FILENAME = "classes.dex";
+  public static final String JAVA_EXTENSION = ".java";
 
   public static boolean isDexFile(Path path) {
     String name = path.getFileName().toString().toLowerCase();
diff --git a/src/test/java/com/android/tools/r8/debug/BlockReorderingTest.java b/src/test/java/com/android/tools/r8/debug/BlockReorderingTest.java
index 3c40b74..6f6b7d0 100644
--- a/src/test/java/com/android/tools/r8/debug/BlockReorderingTest.java
+++ b/src/test/java/com/android/tools/r8/debug/BlockReorderingTest.java
@@ -19,7 +19,7 @@
   public static void setUp() throws Exception {
     // Force inversion of all conditionals to reliably construct a regression test for incorrect
     // line information when reording blocks.
-    setUp(options -> options.testing.invertConditionals = true);
+    setUp(options -> options.testing.invertConditionals = true, null);
   }
 
   @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 1cd5c71..1f9101c 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -6,10 +6,13 @@
 import com.android.tools.r8.CompilationException;
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.D8Command;
+import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.shaking.ProguardConfiguration;
+import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OffOrAuto;
@@ -29,6 +32,7 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.TreeMap;
+import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -104,6 +108,7 @@
   public static TemporaryFolder temp = new TemporaryFolder();
   private static Path jdwpDexD8 = null;
   private static Path debuggeeDexD8 = null;
+  private static Path debuggeeDexR8 = null;
   private static Path debuggeeJava8DexD8 = null;
   private static Path debuggeeKotlinDexD8 = null;
 
@@ -112,25 +117,30 @@
 
   @BeforeClass
   public static void setUp() throws Exception {
-    setUp(null);
+    setUp(null, null);
   }
 
-  protected static void setUp(Consumer<InternalOptions> optionsConsumer) throws Exception {
+  protected static void setUp(
+      Consumer<InternalOptions> optionsConsumer,
+      Consumer<ProguardConfiguration.Builder> pgConsumer)
+      throws Exception {
     // Convert jar to dex with d8 with debug info
     jdwpDexD8 = compileToDex(null, JDWP_JAR);
+    // TODO(zerny): supply a set of compilers to run with.
     debuggeeDexD8 = compileToDex(optionsConsumer, DEBUGGEE_JAR);
+    debuggeeDexR8 = compileToDexViaR8(optionsConsumer, pgConsumer, DEBUGGEE_JAR);
     debuggeeJava8DexD8 = compileToDex(options -> {
-          // Enable desugaring for preN runtimes
-          options.interfaceMethodDesugaring = OffOrAuto.Auto;
-          if (optionsConsumer != null) {
-            optionsConsumer.accept(options);
-          }
-        }, DEBUGGEE_JAVA8_JAR);
+      // Enable desugaring for preN runtimes
+      options.interfaceMethodDesugaring = OffOrAuto.Auto;
+      if (optionsConsumer != null) {
+        optionsConsumer.accept(options);
+      }
+    }, DEBUGGEE_JAVA8_JAR);
     debuggeeKotlinDexD8 = compileToDex(optionsConsumer, DEBUGGEE_KOTLIN_JAR);
   }
 
-  protected static Path compileToDex(Consumer<InternalOptions> optionsConsumer,
-      Path jarToCompile) throws IOException, CompilationException {
+  static Path compileToDex(Consumer<InternalOptions> optionsConsumer, Path jarToCompile)
+      throws IOException, CompilationException {
     int minSdk = ToolHelper.getMinApiLevelForDexVm(ToolHelper.getDexVm());
     assert jarToCompile.toFile().exists();
     Path dexOutputDir = temp.newFolder().toPath();
@@ -144,7 +154,31 @@
             .build(),
         optionsConsumer);
     return dexOutputDir.resolve("classes.dex");
+  }
 
+  static Path compileToDexViaR8(
+      Consumer<InternalOptions> optionsConsumer,
+      Consumer<ProguardConfiguration.Builder> pgConsumer,
+      Path jarToCompile)
+      throws IOException, CompilationException, ExecutionException, ProguardRuleParserException {
+    int minSdk = ToolHelper.getMinApiLevelForDexVm(ToolHelper.getDexVm());
+    assert jarToCompile.toFile().exists();
+    Path dexOutputDir = temp.newFolder().toPath();
+    R8Command.Builder builder =
+        R8Command.builder()
+            .addProgramFiles(jarToCompile)
+            .setOutputPath(dexOutputDir)
+            .setMinApiLevel(minSdk)
+            .setMode(CompilationMode.DEBUG)
+            .addLibraryFiles(Paths.get(ToolHelper.getAndroidJar(minSdk)));
+    if (pgConsumer != null) {
+      builder
+          .addProguardConfiguration(
+              ImmutableList.of("-keepattributes SourceFile,LineNumberTable"))
+          .addProguardConfigurationConsumer(pgConsumer);
+    }
+    ToolHelper.runR8(builder.build(), optionsConsumer);
+    return dexOutputDir.resolve("classes.dex");
   }
 
   protected final boolean supportsDefaultMethod() {
@@ -200,6 +234,16 @@
     runDebugTest(Collections.emptyList(), LanguageFeatures.KOTLIN, debuggeeClass, commands);
   }
 
+  protected final void runDebugTestR8(String debuggeeClass, JUnit3Wrapper.Command... commands)
+      throws Throwable {
+    runDebugTestR8(debuggeeClass, Arrays.asList(commands));
+  }
+
+  protected final void runDebugTestR8(String debuggeeClass, List<JUnit3Wrapper.Command> commands)
+      throws Throwable {
+    runDebugTest(Collections.emptyList(), LanguageFeatures.R8, debuggeeClass, commands);
+  }
+
   protected enum LanguageFeatures {
     JAVA_7(DEBUGGEE_JAR) {
       @Override
@@ -218,6 +262,12 @@
       public Path getDexPath() {
         return debuggeeKotlinDexD8;
       }
+    },
+    R8(DEBUGGEE_JAR) {
+      @Override
+      public Path getDexPath() {
+        return debuggeeDexR8;
+      }
     };
 
     private final Path jarPath;
diff --git a/src/test/java/com/android/tools/r8/naming/RenameSourceFileDebugTest.java b/src/test/java/com/android/tools/r8/naming/RenameSourceFileDebugTest.java
new file mode 100644
index 0000000..d0ea94c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/RenameSourceFileDebugTest.java
@@ -0,0 +1,74 @@
+// 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.naming;
+
+import com.android.tools.r8.debug.DebugTestBase;
+import com.android.tools.r8.shaking.ProguardKeepRule;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Tests -renamesourcefileattribute.
+ */
+public class RenameSourceFileDebugTest extends DebugTestBase {
+
+  private static final String TEST_FILE = "TestFile.java";
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    setUp(null, pg -> {
+      pg.addRule(ProguardKeepRule.defaultKeepAllRule());
+      pg.setRenameSourceFileAttribute(TEST_FILE);
+    });
+  }
+
+  /**
+   * replica of {@link ClassInitializationTest#testBreakpointInEmptyClassInitializer}
+   */
+  @Test
+  public void testBreakpointInEmptyClassInitializer() throws Throwable {
+    final String CLASS = "ClassInitializerEmpty";
+    runDebugTestR8(CLASS,
+        breakpoint(CLASS, "<clinit>"),
+        run(),
+        checkLine(TEST_FILE, 8),
+        run());
+  }
+
+  /**
+   * replica of {@link LocalsTest#testNoLocal}, except for checking overwritten class file.
+   */
+  @Test
+  public void testNoLocal() throws Throwable {
+    final String className = "Locals";
+    final String methodName = "noLocals";
+    runDebugTestR8(className,
+        breakpoint(className, methodName),
+        run(),
+        checkMethod(className, methodName),
+        checkLine(TEST_FILE, 8),
+        checkNoLocal(),
+        stepOver(),
+        checkMethod(className, methodName),
+        checkLine(TEST_FILE, 9),
+        checkNoLocal(),
+        run());
+  }
+
+  /**
+   * replica of {@link MultipleReturnsTest#testMultipleReturns}
+   */
+  @Test
+  public void testMultipleReturns() throws Throwable {
+    runDebugTestR8("MultipleReturns",
+        breakpoint("MultipleReturns", "multipleReturns"),
+        run(),
+        stepOver(),
+        checkLine(TEST_FILE, 16), // this should be the 1st return statement
+        run(),
+        stepOver(),
+        checkLine(TEST_FILE, 18), // this should be the 2nd return statement
+        run());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/RenameSourceFileSmaliTest.java b/src/test/java/com/android/tools/r8/naming/RenameSourceFileSmaliTest.java
new file mode 100644
index 0000000..4506310
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/RenameSourceFileSmaliTest.java
@@ -0,0 +1,123 @@
+package com.android.tools.r8.naming;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.code.ConstString;
+import com.android.tools.r8.code.InvokeVirtual;
+import com.android.tools.r8.code.ReturnVoid;
+import com.android.tools.r8.code.SgetObject;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.graph.DexDebugEvent.SetFile;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.shaking.ProguardConfiguration;
+import com.android.tools.r8.smali.SmaliTestBase;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests -renamesourcefileattribute.
+ */
+@RunWith(Parameterized.class)
+public class RenameSourceFileSmaliTest extends SmaliTestBase {
+
+  private static final String TEST_FILE = "TestFile.java";
+
+  private static final List<String> DEFAULT_PG_CONFIGS =
+      ImmutableList.of(
+          "-keep class *** { *; }",
+          "-dontoptimize",
+          "-keepattributes SourceFile,LineNumberTable");
+
+  private void configure(ProguardConfiguration.Builder pg) {
+    if (renaming) {
+      pg.setRenameSourceFileAttribute(TEST_FILE);
+    }
+  }
+
+  @Parameter
+  public boolean renaming;
+
+  @Parameters(name="renaming:{0}")
+  public static Object[] parameters() {
+    return new Object[] {true, false};
+  }
+
+  /**
+   * replica of {@link RunArtSmokeTest#test}
+   */
+  @Test
+  public void artSmokeTest() throws Exception {
+    // Build simple "Hello, world!" application.
+    SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
+    String originalSourceFile = DEFAULT_CLASS_NAME + FileUtils.JAVA_EXTENSION;
+    builder.setSourceFile(originalSourceFile);
+    MethodSignature mainSignature = builder.addMainMethod(
+        2,
+        ".line 1",
+        "    sget-object         v0, Ljava/lang/System;->out:Ljava/io/PrintStream;",
+        "    const-string        v1, \"Hello, world!\"",
+        ".source \"PrintStream.java\"",
+        ".line 337",
+        "    invoke-virtual      { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V",
+        ".source \"" + originalSourceFile + "\"",
+        ".line 2",
+        "    return-void"
+    );
+    Path processedApp = runR8(builder, DEFAULT_PG_CONFIGS, this::configure, null);
+
+    DexClass mainClass = getClass(processedApp, DEFAULT_CLASS_NAME);
+    verifySourceFileInCodeItem(mainClass, originalSourceFile, TEST_FILE);
+
+    DexEncodedMethod mainMethod = getMethod(processedApp, mainSignature);
+    assertNotNull(mainMethod);
+
+    DexCode code = mainMethod.getCode().asDexCode();
+    assertTrue(code.instructions[0] instanceof SgetObject);
+    assertTrue(code.instructions[1] instanceof ConstString);
+    assertTrue(code.instructions[2] instanceof InvokeVirtual);
+    assertTrue(code.instructions[3] instanceof ReturnVoid);
+
+    // Run the generated code in Art.
+    String result = runArt(processedApp, DEFAULT_MAIN_CLASS_NAME);
+    assertEquals(StringUtils.lines("Hello, world!"), result);
+
+    verifySourceFileInDebugInfo(code);
+  }
+
+  private void verifySourceFileInCodeItem(DexClass clazz, String original, String rename) {
+    String processedSourceFile = clazz.sourceFile.toString();
+    if (renaming) {
+      assertEquals(rename, processedSourceFile);
+    } else {
+      assertEquals(original, processedSourceFile);
+    }
+  }
+
+  private void verifySourceFileInDebugInfo(DexCode code) {
+    assertNotNull(code.getDebugInfo());
+    assertNotEquals(0, code.getDebugInfo().events.length);
+    long setFileCount =
+        Arrays.stream(code.getDebugInfo().events)
+            .filter(dexDebugEvent -> dexDebugEvent instanceof SetFile)
+            .count();
+    if (renaming) {
+      assertEquals(0, setFileCount);
+    } else {
+      assertNotEquals(0, setFileCount);
+    }
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/smali/RunArtSmokeTest.java b/src/test/java/com/android/tools/r8/smali/RunArtSmokeTest.java
index 6a39c6d..214a3b4 100644
--- a/src/test/java/com/android/tools/r8/smali/RunArtSmokeTest.java
+++ b/src/test/java/com/android/tools/r8/smali/RunArtSmokeTest.java
@@ -5,9 +5,9 @@
 package com.android.tools.r8.smali;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
-import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.code.ConstString;
 import com.android.tools.r8.code.InvokeVirtual;
 import com.android.tools.r8.code.ReturnVoid;
@@ -41,7 +41,7 @@
 
     // Return the processed method for inspection.
     DexEncodedMethod main = getMethod(processedApplication, mainSignature);
-    assert main != null;
+    assertNotNull(main);
 
     DexCode code = main.getCode().asDexCode();
     assertTrue(code.instructions[0] instanceof SgetObject);
diff --git a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
index c580f74..f2f1220 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
@@ -7,7 +7,9 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.CompilationException;
+import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.R8;
+import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.dex.ApplicationReader;
@@ -24,6 +26,8 @@
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.shaking.FilteredClassPath;
+import com.android.tools.r8.shaking.ProguardConfiguration;
+import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.DexInspector;
@@ -32,11 +36,13 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.Smali;
+import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import java.io.IOException;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -45,6 +51,7 @@
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executors;
+import java.util.function.Consumer;
 import org.antlr.runtime.RecognitionException;
 
 public class SmaliTestBase extends TestBase {
@@ -71,6 +78,12 @@
     public static MethodSignature staticInitializer(String clazz) {
       return new MethodSignature(clazz, "<clinit>", "void", ImmutableList.of());
     }
+
+    @Override
+    public String toString() {
+      return returnType + " " + clazz + "." + name
+          + "(" + StringUtils.join(parameterTypes, ",") + ")";
+    }
   }
 
   public static class SmaliBuilder {
@@ -468,6 +481,30 @@
     }
   }
 
+  protected Path runR8(
+      SmaliBuilder builder,
+      List<String> proguardConfigurations,
+      Consumer<ProguardConfiguration.Builder> pgConsumer,
+      Consumer<InternalOptions> optionsConsumer) {
+    try {
+      Path dexOutputDir = temp.newFolder().toPath();
+      R8Command command =
+          R8Command.builder()
+              .addDexProgramData(builder.compile())
+              .setOutputPath(dexOutputDir)
+              .setMode(CompilationMode.DEBUG)
+              .addLibraryFiles(Paths.get(ToolHelper.getDefaultAndroidJar()))
+              .addProguardConfiguration(proguardConfigurations)
+              .addProguardConfigurationConsumer(pgConsumer)
+              .build();
+      ToolHelper.runR8WithFullResult(command, optionsConsumer);
+      return dexOutputDir.resolve("classes.dex");
+    } catch (CompilationException | IOException | RecognitionException | ExecutionException
+        | ProguardRuleParserException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
   protected DexClass getClass(DexApplication application, String className) {
     DexInspector inspector = new DexInspector(application);
     ClassSubject clazz = inspector.clazz(className);
@@ -479,9 +516,23 @@
     return getClass(application, signature.clazz);
   }
 
-  protected DexEncodedMethod getMethod(DexApplication application, String className,
-      String returnType, String methodName, List<String> parameters) {
-    DexInspector inspector = new DexInspector(application);
+  protected DexClass getClass(Path appPath, String className) {
+    try {
+      DexInspector inspector = new DexInspector(appPath);
+      ClassSubject clazz = inspector.clazz(className);
+      assertTrue(clazz.isPresent());
+      return clazz.getDexClass();
+    } catch (IOException | ExecutionException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  protected DexEncodedMethod getMethod(
+      DexInspector inspector,
+      String className,
+      String returnType,
+      String methodName,
+      List<String> parameters) {
     ClassSubject clazz = inspector.clazz(className);
     assertTrue(clazz.isPresent());
     MethodSubject method = clazz.method(returnType, methodName, parameters);
@@ -489,6 +540,30 @@
     return method.getMethod();
   }
 
+  protected DexEncodedMethod getMethod(
+      DexApplication application,
+      String className,
+      String returnType,
+      String methodName,
+      List<String> parameters) {
+    DexInspector inspector = new DexInspector(application);
+    return getMethod(inspector, className, returnType, methodName, parameters);
+  }
+
+  protected DexEncodedMethod getMethod(Path appPath, MethodSignature signature) {
+    try {
+      DexInspector inspector = new DexInspector(appPath);
+      return getMethod(
+          inspector,
+          signature.clazz,
+          signature.returnType,
+          signature.name,
+          signature.parameterTypes);
+    } catch (IOException | ExecutionException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
   protected DexEncodedMethod getMethod(DexApplication application, MethodSignature signature) {
     return getMethod(application,
         signature.clazz, signature.returnType, signature.name, signature.parameterTypes);
@@ -563,6 +638,14 @@
     }
   }
 
+  public static String runArt(Path path, String mainClass) {
+    try {
+      return ToolHelper.runArtNoVerificationErrors(path.toString(), mainClass);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
   public void runDex2Oat(DexApplication application, InternalOptions options)
       throws DexOverflowException {
     try {