Introduce a RelocatorTestBuilder to better write tests

This will help out when expanding the language for relocation.

Bug: b/155618698
Change-Id: I2336c519b0c324dc8f0094f412839b99673f9dbb
diff --git a/src/main/java/com/android/tools/r8/relocator/RelocatorCommandLine.java b/src/main/java/com/android/tools/r8/relocator/RelocatorCommandLine.java
index dc11daa..f065d5e 100644
--- a/src/main/java/com/android/tools/r8/relocator/RelocatorCommandLine.java
+++ b/src/main/java/com/android/tools/r8/relocator/RelocatorCommandLine.java
@@ -27,7 +27,7 @@
     ExceptionUtils.withMainProgramHandler(() -> run(args));
   }
 
-  static void run(String[] args) throws CompilationFailedException {
+  public static void run(String[] args) throws CompilationFailedException {
     RelocatorCommand command =
         RelocatorCommand.Builder.parse(args, CommandLineOrigin.INSTANCE).build();
     if (command.isPrintHelp()) {
diff --git a/src/test/java/com/android/tools/r8/RelocatorTestBuilder.java b/src/test/java/com/android/tools/r8/RelocatorTestBuilder.java
new file mode 100644
index 0000000..4d221c9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/RelocatorTestBuilder.java
@@ -0,0 +1,158 @@
+// Copyright (c) 2023, 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;
+
+import static com.android.tools.r8.TestBase.extractClassDescriptor;
+
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.PackageReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.relocator.Relocator;
+import com.android.tools.r8.relocator.RelocatorCommand;
+import com.android.tools.r8.relocator.RelocatorCommandLine;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+public class RelocatorTestBuilder
+    extends TestBuilder<RelocatorTestCompileResult, RelocatorTestBuilder> {
+
+  private final boolean isExternal;
+  private final List<String> commandLineArgs = new ArrayList<>();
+  private final RelocatorCommand.Builder commandBuilder = RelocatorCommand.builder();
+  private Path output = null;
+
+  private RelocatorTestBuilder(TestState state, boolean isExternal) {
+    super(state);
+    this.isExternal = isExternal;
+  }
+
+  public static RelocatorTestBuilder create(TestState state, boolean external) {
+    return new RelocatorTestBuilder(state, external);
+  }
+
+  @Override
+  RelocatorTestBuilder self() {
+    return this;
+  }
+
+  public RelocatorTestCompileResult run() throws Exception {
+    if (output == null) {
+      setOutputPath(getState().getNewTempFolder().resolve("output.jar"));
+    }
+    if (isExternal) {
+      RelocatorCommandLine.run(commandLineArgs.toArray(new String[0]));
+    } else {
+      Relocator.run(commandBuilder.build());
+    }
+    return new RelocatorTestCompileResult(output);
+  }
+
+  public RelocatorTestBuilder setOutputPath(Path path) {
+    assert output == null;
+    output = path;
+    if (isExternal) {
+      commandLineArgs.add("--output");
+      commandLineArgs.add(path.toString());
+    } else {
+      commandBuilder.setOutputPath(path);
+    }
+    return self();
+  }
+
+  @Override
+  public RelocatorTestCompileResult run(TestRuntime runtime, String mainClass, String... args)
+      throws CompilationFailedException, ExecutionException, IOException {
+    throw new Unreachable("Not implemented - use run()");
+  }
+
+  @Override
+  public RelocatorTestBuilder addProgramFiles(Collection<Path> files) {
+    if (isExternal) {
+      files.forEach(
+          file -> {
+            commandLineArgs.add("--input");
+            commandLineArgs.add(file.toString());
+          });
+    } else {
+      commandBuilder.addProgramFiles(files);
+    }
+    return self();
+  }
+
+  @Override
+  public RelocatorTestBuilder addProgramClassFileData(Collection<byte[]> classes) {
+    try {
+      Path resolve = getState().getNewTempFolder().resolve("input.jar");
+      ClassFileConsumer inputConsumer = new ClassFileConsumer.ArchiveConsumer(resolve);
+      for (byte[] clazz : classes) {
+        inputConsumer.accept(ByteDataView.of(clazz), extractClassDescriptor(clazz), null);
+      }
+      inputConsumer.finished(null);
+      addProgramFiles(resolve);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+    return self();
+  }
+
+  @Override
+  public RelocatorTestBuilder addProgramDexFileData(Collection<byte[]> data) {
+    throw new Unimplemented("No support for adding dex file data directly");
+  }
+
+  @Override
+  public RelocatorTestBuilder addLibraryFiles(Collection<Path> files) {
+    throw new Unimplemented("No support for adding library");
+  }
+
+  @Override
+  public RelocatorTestBuilder addLibraryClasses(Collection<Class<?>> classes) {
+    throw new Unimplemented("No support for adding library");
+  }
+
+  @Override
+  public RelocatorTestBuilder addClasspathClasses(Collection<Class<?>> classes) {
+    throw new Unimplemented("No support for adding classpath");
+  }
+
+  @Override
+  public RelocatorTestBuilder addClasspathFiles(Collection<Path> files) {
+    throw new Unimplemented("No support for adding classpath");
+  }
+
+  @Override
+  public RelocatorTestBuilder addRunClasspathFiles(Collection<Path> files) {
+    throw new Unimplemented("No support for adding run classpath");
+  }
+
+  public RelocatorTestBuilder addPackageMapping(PackageReference from, PackageReference to) {
+    if (isExternal) {
+      commandLineArgs.add("--map");
+      commandLineArgs.add(from.getPackageName() + "->" + to.getPackageName());
+    } else {
+      commandBuilder.addPackageMapping(from, to);
+    }
+    return self();
+  }
+
+  public RelocatorTestBuilder addClassMapping(ClassReference from, ClassReference to) {
+    if (isExternal) {
+      commandLineArgs.add("--map");
+      commandLineArgs.add(from.getDescriptor() + "->" + to.getDescriptor());
+    } else {
+      commandBuilder.addClassMapping(from, to);
+    }
+    return self();
+  }
+
+  public RelocatorTestBuilder addPackageMapping(String from, String to) {
+    return addPackageMapping(Reference.packageFromString(from), Reference.packageFromString(to));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/RelocatorTestCompileResult.java b/src/test/java/com/android/tools/r8/RelocatorTestCompileResult.java
new file mode 100644
index 0000000..1f58f84
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/RelocatorTestCompileResult.java
@@ -0,0 +1,115 @@
+// Copyright (c) 2023, 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;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.ThrowingConsumer;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+import com.android.tools.r8.utils.codeinspector.FoundFieldSubject;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.concurrent.ExecutionException;
+import org.hamcrest.Matcher;
+
+public class RelocatorTestCompileResult extends TestRunResult<RelocatorTestCompileResult> {
+
+  private final Path output;
+
+  public RelocatorTestCompileResult(Path output) {
+    this.output = output;
+  }
+
+  @Override
+  RelocatorTestCompileResult self() {
+    return this;
+  }
+
+  @Override
+  public RelocatorTestCompileResult assertSuccess() {
+    // If we produced a RelocatorTestRunResult the compilation was a success.
+    return self();
+  }
+
+  @Override
+  public RelocatorTestCompileResult assertStdoutMatches(Matcher<String> matcher) {
+    throw new Unreachable("Not implemented");
+  }
+
+  @Override
+  public RelocatorTestCompileResult assertFailure() {
+    throw new Unreachable("Not implemented");
+  }
+
+  @Override
+  public RelocatorTestCompileResult assertStderrMatches(Matcher<String> matcher) {
+    throw new Unreachable("Not implemented");
+  }
+
+  @Override
+  public <E extends Throwable> RelocatorTestCompileResult inspect(
+      ThrowingConsumer<CodeInspector, E> consumer) throws IOException, ExecutionException, E {
+    consumer.accept(new CodeInspector(output));
+    return self();
+  }
+
+  @Override
+  public <E extends Throwable> RelocatorTestCompileResult inspectFailure(
+      ThrowingConsumer<CodeInspector, E> consumer) throws IOException, E {
+    throw new Unreachable("NOT IMPLEMENTED");
+  }
+
+  @Override
+  public RelocatorTestCompileResult disassemble() throws IOException, ExecutionException {
+    throw new Unreachable("NOT IMPLEMENTED");
+  }
+
+  public RelocatorTestCompileResult inspectAllClassesRelocated(
+      Path original, String originalPrefix, String newPrefix) throws Exception {
+    CodeInspector originalInspector = new CodeInspector(original);
+    inspect(
+        relocatedInspector -> {
+          for (FoundClassSubject clazz : originalInspector.allClasses()) {
+            if (originalPrefix.isEmpty()
+                || clazz
+                    .getFinalName()
+                    .startsWith(originalPrefix + DescriptorUtils.JAVA_PACKAGE_SEPARATOR)) {
+              String relocatedName =
+                  newPrefix + clazz.getFinalName().substring(originalPrefix.length());
+              ClassSubject relocatedClass = relocatedInspector.clazz(relocatedName);
+              assertThat(relocatedClass, isPresent());
+            }
+          }
+        });
+    return self();
+  }
+
+  public void inspectAllSignaturesNotContainingString(String originalPrefix) throws Exception {
+    inspect(
+        inspector -> {
+          for (FoundClassSubject clazz : inspector.allClasses()) {
+            assertThat(clazz.getFinalSignatureAttribute(), not(containsString(originalPrefix)));
+            for (FoundMethodSubject method : clazz.allMethods()) {
+              assertThat(
+                  method.getJvmMethodSignatureAsString(), not(containsString(originalPrefix)));
+            }
+            for (FoundFieldSubject field : clazz.allFields()) {
+              assertThat(field.getJvmFieldSignatureAsString(), not(containsString(originalPrefix)));
+            }
+          }
+        });
+  }
+
+  public Path getOutput() {
+    return output;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 3bad712..b5f533d 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -223,6 +223,10 @@
     return testForD8(temp, backend);
   }
 
+  public RelocatorTestBuilder testForRelocator(boolean external) {
+    return RelocatorTestBuilder.create(new TestState(temp), external);
+  }
+
   public JvmTestBuilder testForJvm(TestParameters parameters) {
     parameters.assertCfRuntime();
     parameters.assertIsRepresentativeApiLevelForRuntime();
diff --git a/src/test/java/com/android/tools/r8/relocator/RelocatorPathExpansionTest.java b/src/test/java/com/android/tools/r8/relocator/RelocatorPathExpansionTest.java
index 5f5de97..9216acb 100644
--- a/src/test/java/com/android/tools/r8/relocator/RelocatorPathExpansionTest.java
+++ b/src/test/java/com/android/tools/r8/relocator/RelocatorPathExpansionTest.java
@@ -8,8 +8,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertThrows;
 
-import com.android.tools.r8.ByteDataView;
-import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.RelocatorTestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.references.ClassReference;
@@ -19,11 +18,7 @@
 import com.android.tools.r8.relocator.foo.bar.BazImpl;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import java.nio.file.Path;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -46,80 +41,70 @@
 
   @Test
   public void testRewritingSingleClassWithSubPackageMatch() throws Exception {
-    Path testJarPath = temp.newFile("test.jar").toPath();
-    writeClassesToPath(
-        testJarPath,
-        Baz.dump(),
-        BazImpl.dump(),
-        transformer(Base.class).setClassDescriptor("Lfoo/Base;").transform());
-    Path relocatedJar = temp.newFile("out.jar").toPath();
     ClassReference destination =
         Reference.classFromDescriptor("Lcom/android/tools/r8/foo/bar/Baz;");
-    Map<String, String> mapping = new LinkedHashMap<>();
-    mapping.put("foo", "com.android.tools.r8.foo");
-    mapping.put("Lfoo/bar/Baz;", destination.getDescriptor());
-    RelocatorUtils.runRelocator(testJarPath, mapping, relocatedJar, external);
-    CodeInspector inspector = new CodeInspector(relocatedJar);
-    assertThat(inspector.clazz("com.android.tools.r8.foo.Base"), isPresent());
-    assertThat(inspector.clazz(destination), isPresent());
-    assertThat(inspector.clazz("com.android.tools.r8.foo.bar.BazImpl"), isPresent());
+    testForRelocator(external)
+        .addProgramClassFileData(
+            Baz.dump(),
+            BazImpl.dump(),
+            transformer(Base.class).setClassDescriptor("Lfoo/Base;").transform())
+        .addPackageMapping("foo", "com.android.tools.r8.foo")
+        .addClassMapping(
+            destination, Reference.classFromDescriptor("Lcom/android/tools/r8/foo/bar/Baz;"))
+        .run()
+        .inspect(
+            inspector -> {
+              assertThat(inspector.clazz("com.android.tools.r8.foo.Base"), isPresent());
+              assertThat(inspector.clazz(destination), isPresent());
+              assertThat(inspector.clazz("com.android.tools.r8.foo.bar.BazImpl"), isPresent());
+            });
   }
 
   @Test
   public void rewriteTopLevelClass() throws Exception {
-    Path testJarPath = temp.newFile("test.jar").toPath();
-    writeClassesToPath(
-        testJarPath,
-        Baz.dump(),
-        BazImpl.dump(),
-        transformer(Base.class).setClassDescriptor("LBase;").transform());
-    Path relocatedJar = temp.newFile("out.jar").toPath();
+    ClassReference base = Reference.classFromDescriptor("LBase;");
     ClassReference destination = Reference.classFromDescriptor("Lcom/android/tools/r8/Base;");
-    Map<String, String> mapping = new LinkedHashMap<>();
-    mapping.put("LBase;", destination.getDescriptor());
-    RelocatorUtils.runRelocator(testJarPath, mapping, relocatedJar, external);
-    CodeInspector inspector = new CodeInspector(relocatedJar);
-    assertThat(inspector.clazz(destination), isPresent());
-    // Assert that we did not rename a class in a package that is under root.
-    ClassSubject relocatedBaz = inspector.clazz("foo.bar.Baz");
-    assertThat(relocatedBaz, isPresent());
+    testForRelocator(external)
+        .addProgramClassFileData(
+            Baz.dump(),
+            BazImpl.dump(),
+            transformer(Base.class).setClassDescriptor(base.getDescriptor()).transform())
+        .addClassMapping(base, destination)
+        .run()
+        .inspect(
+            inspector -> {
+              assertThat(inspector.clazz(destination), isPresent());
+              // Assert that we did not rename a class in a package that is under root.
+              ClassSubject relocatedBaz = inspector.clazz("foo.bar.Baz");
+              assertThat(relocatedBaz, isPresent());
+            });
   }
 
   @Test
   public void rewriteSingleClassDifferentlyFromPackage() throws Exception {
-    Path testJarPath = temp.newFile("test.jar").toPath();
-    writeClassesToPath(testJarPath, Baz.dump(), BazImpl.dump());
-    Path relocatedJar = temp.newFile("out.jar").toPath();
     ClassReference destination =
         Reference.classFromDescriptor("Lcom/android/tools/r8/foo1/bar/Baz;");
-    Map<String, String> mapping = new LinkedHashMap<>();
-    mapping.put("foo.bar", "com.android.tools.r8.foo2.bar");
-    // Relocation of specific classes should take precedence over relocation of packages.
-    mapping.put("Lfoo/bar/Baz;", destination.getDescriptor());
-    RelocatorUtils.runRelocator(testJarPath, mapping, relocatedJar, external);
-    CodeInspector inspector = new CodeInspector(relocatedJar);
-    assertThat(inspector.clazz(destination), isPresent());
-    assertThat(inspector.clazz("com.android.tools.r8.foo2.bar.BazImpl"), isPresent());
+    String destinationPackage = "com.android.tools.r8.foo2.bar";
+    testForRelocator(external)
+        .addProgramClassFileData(Baz.dump(), BazImpl.dump())
+        .addPackageMapping("foo.bar", destinationPackage)
+        .addClassMapping(Reference.classFromDescriptor("Lfoo/bar/Baz;"), destination)
+        .run()
+        .inspect(
+            inspector -> {
+              // Relocation of specific classes should take precedence over relocation of packages.
+              assertThat(inspector.clazz(destination), isPresent());
+              assertThat(inspector.clazz(destinationPackage + ".BazImpl"), isPresent());
+            });
   }
 
   @Test
-  public void rewriteAllSubPackages() throws Exception {
-    Path testJarPath = temp.newFile("test.jar").toPath();
-    writeClassesToPath(testJarPath, Baz.dump(), BazImpl.dump());
-    Path relocatedJar = temp.newFile("out.jar").toPath();
-    Map<String, String> mapping = new LinkedHashMap<>();
+  public void rewriteAllSubPackages() {
+    RelocatorTestBuilder relocatorTestBuilder =
+        testForRelocator(external).addProgramClassFileData(Baz.dump(), BazImpl.dump());
     // The language as input is very simple and ** to match sub-packages would be a feature request.
-    mapping.put("foo/**", "com.android.tools.r8.foo");
     assertThrows(
         IllegalArgumentException.class,
-        () -> RelocatorUtils.runRelocator(testJarPath, mapping, relocatedJar, external));
-  }
-
-  private void writeClassesToPath(Path inputJar, byte[]... classes) {
-    ClassFileConsumer inputConsumer = new ClassFileConsumer.ArchiveConsumer(inputJar);
-    for (byte[] clazz : classes) {
-      inputConsumer.accept(ByteDataView.of(clazz), extractClassDescriptor(clazz), null);
-    }
-    inputConsumer.finished(null);
+        () -> relocatorTestBuilder.addPackageMapping("foo/**", "com.android.tools.r8.foo"));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/relocator/RelocatorTest.java b/src/test/java/com/android/tools/r8/relocator/RelocatorTest.java
index bbb6172..9a0a971 100644
--- a/src/test/java/com/android/tools/r8/relocator/RelocatorTest.java
+++ b/src/test/java/com/android/tools/r8/relocator/RelocatorTest.java
@@ -4,22 +4,24 @@
 
 package com.android.tools.r8.relocator;
 
-import static com.android.tools.r8.relocator.RelocatorUtils.inspectAllClassesRelocated;
-import static com.android.tools.r8.relocator.RelocatorUtils.inspectAllSignaturesNotContainingString;
-import static com.android.tools.r8.relocator.RelocatorUtils.runRelocator;
+import static com.android.tools.r8.ToolHelper.CHECKED_IN_R8_17_WITH_DEPS;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static junit.framework.TestCase.assertFalse;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.RelocatorTestBuilder;
+import com.android.tools.r8.RelocatorTestCompileResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.CfRuntime;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.references.PackageReference;
+import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -31,11 +33,8 @@
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.nio.file.Path;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.HashSet;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -59,140 +58,145 @@
 
   @Test
   public void testRelocatorIdentity() throws Exception {
-    Path output = temp.newFile("output.jar").toPath();
-    runRelocator(ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, new HashMap<>(), output, external);
-    inspectAllClassesRelocated(ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, output, "", "");
+    testForRelocator(external)
+        .addProgramFiles(CHECKED_IN_R8_17_WITH_DEPS)
+        .run()
+        .inspectAllClassesRelocated(CHECKED_IN_R8_17_WITH_DEPS, "", "");
   }
 
   @Test
   public void testRelocatorEmptyToSomething() throws Exception {
     String originalPrefix = "";
     String newPrefix = "foo.bar.baz";
-    Path output = temp.newFile("output.jar").toPath();
-    Map<String, String> mapping = new HashMap<>();
-    mapping.put(originalPrefix, newPrefix);
-    runRelocator(ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, mapping, output, external);
+    RelocatorTestCompileResult compileResult =
+        testForRelocator(external)
+            .addPackageMapping(
+                Reference.packageFromString(originalPrefix), Reference.packageFromString(newPrefix))
+            .addProgramFiles(CHECKED_IN_R8_17_WITH_DEPS)
+            .run();
     // TODO(b/155618698): Extend relocator with a richer language such that java.lang.Object is not
     //   relocated.
     RuntimeException compilationError =
         assertThrows(
             RuntimeException.class,
             () ->
-                inspectAllClassesRelocated(
-                    ToolHelper.CHECKED_IN_R8_17_WITH_DEPS,
-                    output,
-                    originalPrefix,
-                    newPrefix + "."));
+                compileResult.inspectAllClassesRelocated(
+                    CHECKED_IN_R8_17_WITH_DEPS, originalPrefix, newPrefix + "."));
     assertThat(compilationError.getMessage(), containsString("must extend class java.lang.Object"));
   }
 
   @Test
   public void testRelocatorSomethingToEmpty() throws Exception {
     String originalPrefix = "com.android.tools.r8";
-    String newPrefix = "";
-    Path output = temp.newFile("output.jar").toPath();
-    Map<String, String> mapping = new HashMap<>();
-    mapping.put(originalPrefix, newPrefix);
-    runRelocator(ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, mapping, output, external);
-    inspectAllSignaturesNotContainingString(output, originalPrefix);
+    testForRelocator(external)
+        .addProgramFiles(CHECKED_IN_R8_17_WITH_DEPS)
+        .addPackageMapping(
+            Reference.packageFromString(originalPrefix), Reference.packageFromString(""))
+        .run()
+        .inspectAllSignaturesNotContainingString(originalPrefix);
   }
 
   @Test
   public void testRelocateKeepsDebugInfo() throws Exception {
-    String originalPrefix = "com.android.tools.r8";
-    String newPrefix = "com.android.tools.r8";
-    Path output = temp.newFile("output.jar").toPath();
-    Map<String, String> mapping = new HashMap<>();
-    mapping.put(originalPrefix, newPrefix);
-    runRelocator(ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, mapping, output, external);
-    // Assert that all classes are the same, have the same methods and debug info:
-    CodeInspector originalInspector = new CodeInspector(ToolHelper.CHECKED_IN_R8_17_WITH_DEPS);
-    CodeInspector relocatedInspector = new CodeInspector(output);
-    for (FoundClassSubject clazz : originalInspector.allClasses()) {
-      ClassSubject relocatedClass = relocatedInspector.clazz(clazz.getFinalName());
-      assertThat(relocatedClass, isPresent());
-      assertEquals(
-          clazz.getDexProgramClass().sourceFile, relocatedClass.getDexProgramClass().sourceFile);
-      for (FoundMethodSubject originalMethod : clazz.allMethods()) {
-        MethodSubject relocatedMethod = relocatedClass.method(originalMethod.asMethodReference());
-        assertThat(relocatedMethod, isPresent());
-        assertEquals(originalMethod.hasLineNumberTable(), relocatedMethod.hasLineNumberTable());
-        if (originalMethod.hasLineNumberTable()) {
-          // TODO(b/155303677): Figure out why we cannot assert the same lines.
-          // assertEquals(
-          //     originalMethod.getLineNumberTable().getLines().size(),
-          //     relocatedMethod.getLineNumberTable().getLines().size());
-        }
-        assertEquals(
-            originalMethod.hasLocalVariableTable(), relocatedMethod.hasLocalVariableTable());
-        if (originalMethod.hasLocalVariableTable()) {
-          LocalVariableTable originalVariableTable = originalMethod.getLocalVariableTable();
-          LocalVariableTable relocatedVariableTable = relocatedMethod.getLocalVariableTable();
-          assertEquals(originalVariableTable.size(), relocatedVariableTable.size());
-          for (int i = 0; i < originalVariableTable.getEntries().size(); i++) {
-            LocalVariableTableEntry originalEntry = originalVariableTable.get(i);
-            LocalVariableTableEntry relocatedEntry = relocatedVariableTable.get(i);
-            assertEquals(originalEntry.name, relocatedEntry.name);
-            assertEquals(originalEntry.signature, relocatedEntry.signature);
-            assertEquals(originalEntry.type.toString(), relocatedEntry.type.toString());
-          }
-        }
-      }
-    }
+    PackageReference pkg = Reference.packageFromString("com.android.tools.r8");
+    testForRelocator(external)
+        .addProgramFiles(CHECKED_IN_R8_17_WITH_DEPS)
+        .addPackageMapping(pkg, pkg)
+        .run()
+        .inspect(
+            inspector -> {
+              // Assert that all classes are the same, have the same methods and debug info:
+              CodeInspector originalInspector = new CodeInspector(CHECKED_IN_R8_17_WITH_DEPS);
+              for (FoundClassSubject clazz : originalInspector.allClasses()) {
+                ClassSubject relocatedClass = inspector.clazz(clazz.getFinalName());
+                assertThat(relocatedClass, isPresent());
+                assertEquals(
+                    clazz.getDexProgramClass().sourceFile,
+                    relocatedClass.getDexProgramClass().sourceFile);
+                for (FoundMethodSubject originalMethod : clazz.allMethods()) {
+                  MethodSubject relocatedMethod =
+                      relocatedClass.method(originalMethod.asMethodReference());
+                  assertThat(relocatedMethod, isPresent());
+                  assertEquals(
+                      originalMethod.hasLineNumberTable(), relocatedMethod.hasLineNumberTable());
+                  if (originalMethod.hasLineNumberTable()) {
+                    // TODO(b/155303677): Figure out why we cannot assert the same lines.
+                    // assertEquals(
+                    //     originalMethod.getLineNumberTable().getLines().size(),
+                    //     relocatedMethod.getLineNumberTable().getLines().size());
+                  }
+                  assertEquals(
+                      originalMethod.hasLocalVariableTable(),
+                      relocatedMethod.hasLocalVariableTable());
+                  if (originalMethod.hasLocalVariableTable()) {
+                    LocalVariableTable originalVariableTable =
+                        originalMethod.getLocalVariableTable();
+                    LocalVariableTable relocatedVariableTable =
+                        relocatedMethod.getLocalVariableTable();
+                    assertEquals(originalVariableTable.size(), relocatedVariableTable.size());
+                    for (int i = 0; i < originalVariableTable.getEntries().size(); i++) {
+                      LocalVariableTableEntry originalEntry = originalVariableTable.get(i);
+                      LocalVariableTableEntry relocatedEntry = relocatedVariableTable.get(i);
+                      assertEquals(originalEntry.name, relocatedEntry.name);
+                      assertEquals(originalEntry.signature, relocatedEntry.signature);
+                      assertEquals(originalEntry.type.toString(), relocatedEntry.type.toString());
+                    }
+                  }
+                }
+              }
+            });
   }
 
   @Test
   public void testRelocateAll() throws Exception {
     String originalPrefix = "com.android.tools.r8";
     String newPrefix = "foo.bar.baz";
-    Map<String, String> mapping = new HashMap<>();
-    mapping.put("some.package.that.does.not.exist", "foo");
-    mapping.put(originalPrefix, newPrefix);
-    Path output = temp.newFile("output.jar").toPath();
-    runRelocator(ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, mapping, output, external);
-    inspectAllClassesRelocated(
-        ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, output, originalPrefix, newPrefix);
+    testForRelocator(external)
+        .addProgramFiles(CHECKED_IN_R8_17_WITH_DEPS)
+        .addPackageMapping(
+            Reference.packageFromString(originalPrefix), Reference.packageFromString(newPrefix))
+        .addPackageMapping("some.package.that.does.not.exist", "foo")
+        .run()
+        .inspectAllClassesRelocated(CHECKED_IN_R8_17_WITH_DEPS, originalPrefix, newPrefix);
   }
 
   @Test
   public void testOrderingOfPrefixes() throws Exception {
-    String originalPrefix = "com.android";
-    String newPrefix = "foo.bar.baz";
-    Path output = temp.newFile("output.jar").toPath();
-    Map<String, String> mapping = new LinkedHashMap<>();
-    mapping.put(originalPrefix, newPrefix);
-    mapping.put("com.android.tools.r8", "qux");
-    runRelocator(ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, mapping, output, external);
-    // Because we see "com.android.tools.r8" before seeing "com.android" we always choose qux.
-    inspectAllClassesRelocated(
-        ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, output, "com.android.tools.r8", "qux");
-    inspectAllSignaturesNotContainingString(output, "foo.bar.baz");
+    testForRelocator(external)
+        .addProgramFiles(CHECKED_IN_R8_17_WITH_DEPS)
+        .addPackageMapping("com.android", "foo.bar.baz")
+        .addPackageMapping("com.android.tools.r8", "qux")
+        .run()
+        // Because we see "com.android.tools.r8" before seeing "com.android" we always choose qux.
+        .inspectAllClassesRelocated(
+            ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, "com.android.tools.r8", "qux")
+        .inspectAllSignaturesNotContainingString("foo.bar.baz");
   }
 
   @Test
   public void testNoReEntry() throws Exception {
     // TODO(b/154909222): Check if this is the behavior we would like.
-    String originalPrefix = "com.android";
-    String newPrefix = "foo.bar.baz";
-    Map<String, String> mapping = new LinkedHashMap<>();
-    mapping.put(originalPrefix, newPrefix);
-    mapping.put(newPrefix, "qux");
-    Path output = temp.newFile("output.jar").toPath();
-    runRelocator(ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, mapping, output, external);
-    inspectAllClassesRelocated(
-        ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, output, originalPrefix, newPrefix);
-    // Assert that no mappings of com.android.tools.r8 -> qux exists.
-    CodeInspector inspector = new CodeInspector(output);
-    assertFalse(
-        inspector.allClasses().stream().anyMatch(clazz -> clazz.getFinalName().startsWith("qux")));
+    testForRelocator(external)
+        .addProgramFiles(CHECKED_IN_R8_17_WITH_DEPS)
+        .addPackageMapping("com.android", "foo.bar.baz")
+        .addPackageMapping("foo.bar.baz", "qux")
+        .run()
+        .inspect(
+            inspector -> {
+              // Assert that no mappings of com.android.tools.r8 -> qux exists.
+              assertTrue(
+                  inspector.allClasses().stream()
+                      .noneMatch(clazz -> clazz.getFinalName().startsWith("qux")));
+            });
   }
 
   @Test
   public void testMultiplePackages() throws Exception {
+    RelocatorTestBuilder testBuilder =
+        testForRelocator(external).addProgramFiles(CHECKED_IN_R8_17_WITH_DEPS);
     Set<String> seenPackages = new HashSet<>();
     List<Pair<String, String>> packageMappings = new ArrayList<>();
-    Map<String, String> mapping = new LinkedHashMap<>();
-    CodeInspector inspector = new CodeInspector(ToolHelper.CHECKED_IN_R8_17_WITH_DEPS);
+    CodeInspector inspector = new CodeInspector(CHECKED_IN_R8_17_WITH_DEPS);
     int packageNameCounter = 0;
     // Generate a mapping for each package name directly below com.android.tools.r8.
     for (FoundClassSubject clazz : inspector.allClasses()) {
@@ -208,49 +212,43 @@
         if (seenPackages.add(mappedPackageName)) {
           String relocatedPackageName = "number" + packageNameCounter++;
           packageMappings.add(new Pair<>(mappedPackageName, relocatedPackageName));
-          mapping.put(mappedPackageName, relocatedPackageName);
+          testBuilder.addPackageMapping(mappedPackageName, relocatedPackageName);
         }
       }
     }
-    Path output = temp.newFile("output.jar").toPath();
-    runRelocator(ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, mapping, output, external);
+    RelocatorTestCompileResult result = testBuilder.run();
     for (Pair<String, String> packageMapping : packageMappings) {
-      inspectAllClassesRelocated(
-          ToolHelper.CHECKED_IN_R8_17_WITH_DEPS,
-          output,
-          packageMapping.getFirst(),
-          packageMapping.getSecond());
+      result.inspectAllClassesRelocated(
+          CHECKED_IN_R8_17_WITH_DEPS, packageMapping.getFirst(), packageMapping.getSecond());
     }
   }
 
   @Test
   public void testPartialPrefix() throws Exception {
     String originalPrefix = "com.android.tools.r";
-    String newPrefix = "i_cannot_w";
-    Map<String, String> mapping = new LinkedHashMap<>();
-    mapping.put(originalPrefix, newPrefix);
-    Path output = temp.newFile("output.jar").toPath();
-    runRelocator(ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, mapping, output, external);
-    inspectAllClassesRelocated(
-        ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, output, originalPrefix, originalPrefix);
+    testForRelocator(external)
+        .addProgramFiles(CHECKED_IN_R8_17_WITH_DEPS)
+        .addPackageMapping(originalPrefix, "i_cannot_w")
+        .run()
+        .inspectAllClassesRelocated(CHECKED_IN_R8_17_WITH_DEPS, originalPrefix, originalPrefix);
   }
 
   @Test
   public void testBootstrap() throws Exception {
     String originalPrefix = "com.android.tools.r8";
     String newPrefix = "relocated_r8";
-    Map<String, String> mapping = new LinkedHashMap<>();
-    mapping.put(originalPrefix, newPrefix);
-    Path output = temp.newFile("output.jar").toPath();
-    runRelocator(ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, mapping, output, external);
+    RelocatorTestCompileResult result =
+        testForRelocator(external)
+            .addProgramFiles(CHECKED_IN_R8_17_WITH_DEPS)
+            .addPackageMapping(originalPrefix, newPrefix)
+            .run();
     // Check that all classes has been remapped.
-    inspectAllClassesRelocated(
-        ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, output, originalPrefix, newPrefix);
-    inspectAllSignaturesNotContainingString(output, originalPrefix);
+    result.inspectAllClassesRelocated(CHECKED_IN_R8_17_WITH_DEPS, originalPrefix, newPrefix);
+    result.inspectAllSignaturesNotContainingString(originalPrefix);
     // We should be able to call the relocated relocator.
     Path bootstrapOutput = temp.newFile("bootstrap.jar").toPath();
     List<Path> classPath = new ArrayList<>();
-    classPath.add(output);
+    classPath.add(result.getOutput());
     ProcessResult processResult =
         ToolHelper.runJava(
             CfRuntime.getCheckedInJdk17(),
@@ -258,42 +256,46 @@
             newPrefix + ".SwissArmyKnife",
             "relocator",
             "--input",
-            output.toString(),
+            result.getOutput().toString(),
             "--output",
             bootstrapOutput.toString(),
             "--map",
             newPrefix + "->" + originalPrefix);
     System.out.println(processResult.stderr);
     assertEquals(0, processResult.exitCode);
-    inspectAllClassesRelocated(output, bootstrapOutput, newPrefix, originalPrefix);
-    inspectAllSignaturesNotContainingString(bootstrapOutput, newPrefix);
+    RelocatorTestCompileResult bootstrapResult = new RelocatorTestCompileResult(bootstrapOutput);
+    bootstrapResult.inspectAllClassesRelocated(result.getOutput(), newPrefix, originalPrefix);
+    bootstrapResult.inspectAllSignaturesNotContainingString(newPrefix);
     // Assert that this is in fact an identity transformation.
-    inspectAllClassesRelocated(ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, bootstrapOutput, "", "");
+    bootstrapResult.inspectAllClassesRelocated(CHECKED_IN_R8_17_WITH_DEPS, "", "");
   }
 
   @Test
   public void testNest() throws Exception {
     String originalPrefix = "com.android.tools.r8";
     String newPrefix = "com.android.tools.r8";
-    Path output = temp.newFile("output.jar").toPath();
-    Map<String, String> mapping = new HashMap<>();
-    mapping.put(originalPrefix, newPrefix);
-    runRelocator(ToolHelper.CHECKED_IN_R8_17_WITH_DEPS, mapping, output, external);
+    CodeInspector originalInspector = new CodeInspector(CHECKED_IN_R8_17_WITH_DEPS);
     // Assert that all classes are the same, have the same methods and nest info.
-    CodeInspector originalInspector = new CodeInspector(ToolHelper.CHECKED_IN_R8_17_WITH_DEPS);
-    CodeInspector relocatedInspector = new CodeInspector(output);
-    for (FoundClassSubject originalSubject : originalInspector.allClasses()) {
-      ClassSubject relocatedSubject = relocatedInspector.clazz(originalSubject.getFinalName());
-      assertThat(relocatedSubject, isPresent());
-      DexClass originalClass = originalSubject.getDexProgramClass();
-      DexClass relocatedClass = relocatedSubject.getDexProgramClass();
-      assertEquals(originalClass.isNestHost(), relocatedClass.isNestHost());
-      assertEquals(originalClass.isNestMember(), relocatedClass.isNestMember());
-      if (originalClass.isInANest()) {
-        assertEquals(
-            originalClass.getNestHost().descriptor, relocatedClass.getNestHost().descriptor);
-      }
-    }
+    testForRelocator(external)
+        .addProgramFiles(CHECKED_IN_R8_17_WITH_DEPS)
+        .addPackageMapping(originalPrefix, newPrefix)
+        .run()
+        .inspect(
+            relocatedInspector -> {
+              for (FoundClassSubject originalSubject : originalInspector.allClasses()) {
+                ClassSubject relocatedSubject =
+                    relocatedInspector.clazz(originalSubject.getFinalName());
+                assertThat(relocatedSubject, isPresent());
+                DexClass originalClass = originalSubject.getDexProgramClass();
+                DexClass relocatedClass = relocatedSubject.getDexProgramClass();
+                assertEquals(originalClass.isNestHost(), relocatedClass.isNestHost());
+                assertEquals(originalClass.isNestMember(), relocatedClass.isNestMember());
+                if (originalClass.isInANest()) {
+                  assertEquals(
+                      originalClass.getNestHost().descriptor,
+                      relocatedClass.getNestHost().descriptor);
+                }
+              }
+            });
   }
-
 }
diff --git a/src/test/java/com/android/tools/r8/relocator/RelocatorUtils.java b/src/test/java/com/android/tools/r8/relocator/RelocatorUtils.java
deleted file mode 100644
index d13c5c0..0000000
--- a/src/test/java/com/android/tools/r8/relocator/RelocatorUtils.java
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright (c) 2023, 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.relocator;
-
-import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static org.hamcrest.CoreMatchers.containsString;
-import static org.hamcrest.CoreMatchers.not;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import com.android.tools.r8.CompilationFailedException;
-import com.android.tools.r8.references.Reference;
-import com.android.tools.r8.utils.DescriptorUtils;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
-import com.android.tools.r8.utils.codeinspector.FoundFieldSubject;
-import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-public class RelocatorUtils {
-
-  public static void runRelocator(
-      Path input, Map<String, String> mapping, Path output, boolean external)
-      throws CompilationFailedException {
-    if (external) {
-      List<String> args = new ArrayList<>();
-      args.add("--input");
-      args.add(input.toString());
-      args.add("--output");
-      args.add(output.toString());
-      mapping.forEach(
-          (key, value) -> {
-            args.add("--map");
-            args.add(key + "->" + value);
-          });
-      RelocatorCommandLine.run(args.toArray(new String[0]));
-    } else {
-      RelocatorCommand.Builder builder =
-          RelocatorCommand.builder().addProgramFiles(input).setOutputPath(output);
-      mapping.forEach(
-          (key, value) -> {
-            if (DescriptorUtils.isClassDescriptor(key)) {
-              builder.addClassMapping(
-                  Reference.classFromDescriptor(key), Reference.classFromDescriptor(value));
-            } else {
-              builder.addPackageMapping(
-                  Reference.packageFromString(key), Reference.packageFromString(value));
-            }
-          });
-      Relocator.run(builder.build());
-    }
-  }
-
-  public static void inspectAllClassesRelocated(
-      Path original, Path relocated, String originalPrefix, String newPrefix) throws IOException {
-    CodeInspector originalInspector = new CodeInspector(original);
-    CodeInspector relocatedInspector = new CodeInspector(relocated);
-    for (FoundClassSubject clazz : originalInspector.allClasses()) {
-      if (originalPrefix.isEmpty()
-          || clazz
-              .getFinalName()
-              .startsWith(originalPrefix + DescriptorUtils.JAVA_PACKAGE_SEPARATOR)) {
-        String relocatedName = newPrefix + clazz.getFinalName().substring(originalPrefix.length());
-        ClassSubject relocatedClass = relocatedInspector.clazz(relocatedName);
-        assertThat(relocatedClass, isPresent());
-      }
-    }
-  }
-
-  public static void inspectAllSignaturesNotContainingString(Path relocated, String originalPrefix)
-      throws IOException {
-    CodeInspector relocatedInspector = new CodeInspector(relocated);
-    for (FoundClassSubject clazz : relocatedInspector.allClasses()) {
-      assertThat(clazz.getFinalSignatureAttribute(), not(containsString(originalPrefix)));
-      for (FoundMethodSubject method : clazz.allMethods()) {
-        assertThat(method.getJvmMethodSignatureAsString(), not(containsString(originalPrefix)));
-      }
-      for (FoundFieldSubject field : clazz.allFields()) {
-        assertThat(field.getJvmFieldSignatureAsString(), not(containsString(originalPrefix)));
-      }
-    }
-  }
-}