Add new test building abstractions.

Change-Id: I5b8595f924f2ee3715a6e7a9d7e1436ede977e56
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index a4f55c3..ed12177 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -60,6 +60,58 @@
   private final List<StringResource> mainDexListResources;
   private final List<String> mainDexClasses;
 
+  @Override
+  public String toString() {
+    StringBuilder builder = new StringBuilder();
+    try {
+      if (!programResourceProviders.isEmpty()) {
+        builder.append("  Program resources:").append(System.lineSeparator());
+        printProgramResourceProviders(builder, programResourceProviders);
+      }
+      if (!classpathResourceProviders.isEmpty()) {
+        builder.append("  Classpath resources:").append(System.lineSeparator());
+        printClassFileProviders(builder, classpathResourceProviders);
+      }
+      if (!libraryResourceProviders.isEmpty()) {
+        builder.append("  Library resources:").append(System.lineSeparator());
+        printClassFileProviders(builder, libraryResourceProviders);
+      }
+    } catch (ResourceException e) {
+      e.printStackTrace();
+    }
+    return builder.toString();
+  }
+
+  private static void printProgramResourceProviders(
+      StringBuilder builder, Collection<ProgramResourceProvider> providers)
+      throws ResourceException {
+    for (ProgramResourceProvider provider : providers) {
+      for (ProgramResource resource : provider.getProgramResources()) {
+        printProgramResource(builder, resource);
+      }
+    }
+  }
+
+  private static void printClassFileProviders(
+      StringBuilder builder, Collection<ClassFileResourceProvider> providers) {
+    for (ClassFileResourceProvider provider : providers) {
+      for (String descriptor : provider.getClassDescriptors()) {
+        ProgramResource resource = provider.getProgramResource(descriptor);
+        printProgramResource(builder, resource);
+      }
+    }
+  }
+
+  private static void printProgramResource(StringBuilder builder, ProgramResource resource) {
+    builder.append("    ").append(resource.getOrigin());
+    Set<String> descriptors = resource.getClassDescriptors();
+    if (descriptors != null && !descriptors.isEmpty()) {
+      builder.append(" contains ");
+      StringUtils.append(builder, descriptors);
+    }
+    builder.append(System.lineSeparator());
+  }
+
   // See factory methods and AndroidApp.Builder below.
   private AndroidApp(
       ImmutableList<ProgramResourceProvider> programResourceProviders,
diff --git a/src/test/java/com/android/tools/r8/D8TestBuilder.java b/src/test/java/com/android/tools/r8/D8TestBuilder.java
new file mode 100644
index 0000000..368e8e4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/D8TestBuilder.java
@@ -0,0 +1,51 @@
+// Copyright (c) 2018, 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 com.android.tools.r8.D8Command.Builder;
+import com.android.tools.r8.TestBase.Backend;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
+
+public class D8TestBuilder extends TestCompilerBuilder<D8Command, Builder, D8TestBuilder> {
+
+  private final D8Command.Builder builder;
+
+  private D8TestBuilder(TestState state, D8Command.Builder builder) {
+    super(state, builder, Backend.DEX);
+    this.builder = builder;
+  }
+
+  public static D8TestBuilder create(TestState state) {
+    return new D8TestBuilder(state, D8Command.builder());
+  }
+
+  @Override
+  D8TestBuilder self() {
+    return this;
+  }
+
+  @Override
+  void internalCompile(Builder builder) throws CompilationFailedException {
+    D8.run(builder.build());
+  }
+
+  public D8TestBuilder addClasspathClasses(Class<?>... classes) {
+    return addClasspathClasses(Arrays.asList(classes));
+  }
+
+  public D8TestBuilder addClasspathClasses(Collection<Class<?>> classes) {
+    return addClasspathFiles(getFilesForClasses(classes));
+  }
+
+  public D8TestBuilder addClasspathFiles(Path... files) {
+    return addClasspathFiles(Arrays.asList(files));
+  }
+
+  public D8TestBuilder addClasspathFiles(Collection<Path> files) {
+    builder.addClasspathFiles(files);
+    return self();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/JvmTestBuilder.java b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
new file mode 100644
index 0000000..484adee
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
@@ -0,0 +1,158 @@
+// Copyright (c) 2018, 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 com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.ListUtils;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public class JvmTestBuilder extends TestBuilder<JvmTestBuilder> {
+
+  private static class ClassFileResource implements ProgramResource {
+
+    private final Path file;
+    private final String descriptor;
+    private final Origin origin;
+
+    ClassFileResource(Class<?> clazz) {
+      this(
+          ToolHelper.getClassFileForTestClass(clazz),
+          DescriptorUtils.javaTypeToDescriptor(clazz.getTypeName()));
+    }
+
+    ClassFileResource(Path file, String descriptor) {
+      this.file = file;
+      this.descriptor = descriptor;
+      origin = new PathOrigin(file);
+    }
+
+    @Override
+    public Kind getKind() {
+      return Kind.CF;
+    }
+
+    @Override
+    public InputStream getByteStream() throws ResourceException {
+      try {
+        return Files.newInputStream(file);
+      } catch (IOException e) {
+        throw new ResourceException(getOrigin(), e);
+      }
+    }
+
+    @Override
+    public Set<String> getClassDescriptors() {
+      return Collections.singleton(descriptor);
+    }
+
+    @Override
+    public Origin getOrigin() {
+      return origin;
+    }
+  }
+
+  private static class ClassFileResourceProvider implements ProgramResourceProvider {
+
+    private final List<ProgramResource> resources;
+
+    public ClassFileResourceProvider(List<ProgramResource> resources) {
+      this.resources = resources;
+    }
+
+    @Override
+    public Collection<ProgramResource> getProgramResources() throws ResourceException {
+      return resources;
+    }
+
+    @Override
+    public DataResourceProvider getDataResourceProvider() {
+      return null;
+    }
+  }
+
+  // Ordered list of classpath entries.
+  private List<Path> classpath = new ArrayList<>();
+
+  private AndroidApp.Builder builder = AndroidApp.builder();
+
+  private JvmTestBuilder(TestState state) {
+    super(state);
+  }
+
+  public static JvmTestBuilder create(TestState state) {
+    return new JvmTestBuilder(state);
+  }
+
+  @Override
+  JvmTestBuilder self() {
+    return this;
+  }
+
+  @Override
+  public TestRunResult run(String mainClass) throws IOException {
+    ProcessResult result = ToolHelper.runJava(classpath, mainClass);
+    return new TestRunResult(builder.build(), result);
+  }
+
+  @Override
+  public JvmTestBuilder addLibraryFiles(Collection<Path> files) {
+    throw new Unimplemented("No support for changing the Java runtime library.");
+  }
+
+  @Override
+  public JvmTestBuilder addProgramClasses(Collection<Class<?>> classes) {
+    // Adding a collection of classes will build a jar of exactly those classes so that no other
+    // classes are made available via a too broad classpath directory.
+    List<ProgramResource> resources = ListUtils.map(classes, ClassFileResource::new);
+    AndroidApp build = AndroidApp.builder()
+        .addProgramResourceProvider(new ClassFileResourceProvider(resources)).build();
+    Path out;
+    try {
+      out = getState().getNewTempFolder().resolve("out.zip");
+      build.writeToZip(out, OutputMode.ClassFile);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+    classpath.add(out);
+    builder.addProgramFiles(out);
+    return self();
+  }
+
+  @Override
+  public JvmTestBuilder addProgramFiles(Collection<Path> files) {
+    throw new Unimplemented(
+        "No support for adding paths directly (we need to compute the descriptor)");
+  }
+
+  public JvmTestBuilder addClasspath(Path... paths) {
+    return addClasspath(Arrays.asList(paths));
+  }
+
+  public JvmTestBuilder addClasspath(List<Path> paths) {
+    for (Path path : paths) {
+      assert Files.isDirectory(path) || FileUtils.isArchive(path);
+      classpath.add(path);
+    }
+    return self();
+  }
+
+  public JvmTestBuilder addTestClasspath() {
+    return addClasspath(ToolHelper.getClassPathForTests());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
new file mode 100644
index 0000000..50e659c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -0,0 +1,84 @@
+// Copyright (c) 2018, 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 com.android.tools.r8.R8Command.Builder;
+import com.android.tools.r8.TestBase.Backend;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+
+public class R8TestBuilder extends TestCompilerBuilder<R8Command, Builder, R8TestBuilder> {
+
+  private final R8Command.Builder builder;
+
+  private R8TestBuilder(TestState state, Builder builder, Backend backend) {
+    super(state, builder, backend);
+    this.builder = builder;
+  }
+
+  public static R8TestBuilder create(TestState state, Backend backend) {
+    return new R8TestBuilder(state, R8Command.builder(), backend);
+  }
+
+  private boolean enableInliningAnnotations = false;
+
+  @Override
+  R8TestBuilder self() {
+    return this;
+  }
+
+  @Override
+  public void internalCompile(Builder builder) throws CompilationFailedException {
+    if (enableInliningAnnotations) {
+      ToolHelper.allowTestProguardOptions(builder);
+    }
+    R8.run(builder.build());
+  }
+
+  public R8TestBuilder addKeepRules(String... rules) {
+    return addKeepRules(Arrays.asList(rules));
+  }
+
+  public R8TestBuilder addKeepRules(Collection<String> rules) {
+    builder.addProguardConfiguration(new ArrayList<>(rules), Origin.unknown());
+    return self();
+  }
+
+  public R8TestBuilder addKeepAllClassesRule() {
+    addKeepRules("-keep class ** { *; }");
+    return self();
+  }
+
+  public R8TestBuilder addKeepClassRules(Class<?>... classes) {
+    for (Class<?> clazz : classes) {
+      addKeepRules("-keep class " + clazz.getTypeName());
+    }
+    return self();
+  }
+
+  public R8TestBuilder addKeepPackageRules(Package pkg) {
+    return addKeepRules("-keep class " + pkg.getName() + ".*");
+  }
+
+  public R8TestBuilder addKeepMainRule(Class<?> mainClass) {
+    return addKeepRules(
+        StringUtils.joinLines(
+            "-keep class " + mainClass.getTypeName() + " {",
+            "  public static void main(java.lang.String[]);",
+            "}"));
+  }
+
+  public R8TestBuilder enableInliningAnnotations() {
+    if (!enableInliningAnnotations) {
+      enableInliningAnnotations = true;
+      addKeepRules(
+          "-forceinline class * { @com.android.tools.r8.ForceInline *; }",
+          "-neverinline class * { @com.android.tools.r8.NeverInline *; }");
+    }
+    return self();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 94cc54b..efec188 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -59,6 +59,19 @@
 import org.objectweb.asm.ClassVisitor;
 
 public class TestBase {
+
+  public R8TestBuilder testForR8(Backend backend) {
+    return R8TestBuilder.create(new TestState(temp), backend);
+  }
+
+  public D8TestBuilder testForD8() {
+    return D8TestBuilder.create(new TestState(temp));
+  }
+
+  public JvmTestBuilder testForJvm() {
+    return JvmTestBuilder.create(new TestState(temp));
+  }
+
   public enum Backend {
     CF,
     DEX
diff --git a/src/test/java/com/android/tools/r8/TestBuilder.java b/src/test/java/com/android/tools/r8/TestBuilder.java
new file mode 100644
index 0000000..6d5a158
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/TestBuilder.java
@@ -0,0 +1,89 @@
+// Copyright (c) 2018, 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.FileUtils.CLASS_EXTENSION;
+
+import com.android.tools.r8.utils.ListUtils;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+public abstract class TestBuilder<T extends TestBuilder<T>> {
+
+  private final TestState state;
+
+  public TestBuilder(TestState state) {
+    this.state = state;
+  }
+
+  public TestState getState() {
+    return state;
+  }
+
+  abstract T self();
+
+  public abstract TestRunResult run(String mainClass)
+      throws IOException, CompilationFailedException;
+
+  public TestRunResult run(Class mainClass) throws IOException, CompilationFailedException {
+    return run(mainClass.getTypeName());
+  }
+
+  public abstract T addProgramFiles(Collection<Path> files);
+
+  public T addProgramClasses(Class<?>... classes) {
+    return addProgramClasses(Arrays.asList(classes));
+  }
+
+  public T addProgramClasses(Collection<Class<?>> classes) {
+    return addProgramFiles(getFilesForClasses(classes));
+  }
+
+  public T addProgramFiles(Path... files) {
+    return addProgramFiles(Arrays.asList(files));
+  }
+
+  public T addProgramClassesAndInnerClasses(Class<?>... classes) throws IOException {
+    return addProgramClassesAndInnerClasses(Arrays.asList(classes));
+  }
+
+  public T addProgramClassesAndInnerClasses(Collection<Class<?>> classes) throws IOException {
+    return addProgramFiles(getFilesForClassesAndInnerClasses(classes));
+  }
+
+  public abstract T addLibraryFiles(Collection<Path> files);
+
+  public T addLibraryClasses(Class<?>... classes) {
+    return addLibraryClasses(Arrays.asList(classes));
+  }
+
+  public T addLibraryClasses(Collection<Class<?>> classes) {
+    return addLibraryFiles(getFilesForClasses(classes));
+  }
+
+  public T addLibraryFiles(Path... files) {
+    return addLibraryFiles(Arrays.asList(files));
+  }
+
+  static Collection<Path> getFilesForClasses(Collection<Class<?>> classes) {
+    return ListUtils.map(classes, ToolHelper::getClassFileForTestClass);
+  }
+
+  static Collection<Path> getFilesForClassesAndInnerClasses(Collection<Class<?>> classes)
+      throws IOException {
+    Set<Path> paths = new HashSet<>();
+    for (Class clazz : classes) {
+      Path path = ToolHelper.getClassFileForTestClass(clazz);
+      String prefix = path.toString().replace(CLASS_EXTENSION, "$");
+      paths.addAll(
+          ToolHelper.getClassFilesForTestDirectory(
+              path.getParent(), p -> p.equals(path) || p.toString().startsWith(prefix)));
+    }
+    return paths;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestCompileResult.java b/src/test/java/com/android/tools/r8/TestCompileResult.java
new file mode 100644
index 0000000..9996dde
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/TestCompileResult.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2018, 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 com.android.tools.r8.TestBase.Backend;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.utils.AndroidApp;
+import java.io.IOException;
+import java.nio.file.Path;
+
+public class TestCompileResult {
+  private final TestState state;
+  private final Backend backend;
+  private final AndroidApp app;
+
+  public TestCompileResult(TestState state, Backend backend, AndroidApp app) {
+    this.state = state;
+    this.backend = backend;
+    this.app = app;
+  }
+
+  public TestRunResult run(String mainClass) throws IOException {
+    switch (backend) {
+      case DEX:
+        return runArt(mainClass);
+      case CF:
+        return runJava(mainClass);
+      default:
+        throw new Unreachable();
+    }
+  }
+
+  private TestRunResult runJava(String mainClass) throws IOException {
+    Path out = state.getNewTempFolder().resolve("out.zip");
+    app.writeToZip(out, OutputMode.ClassFile);
+    ProcessResult result = ToolHelper.runJava(out, mainClass);
+    return new TestRunResult(app, result);
+  }
+
+  private TestRunResult runArt(String mainClass) throws IOException {
+    Path out = state.getNewTempFolder().resolve("out.zip");
+    app.writeToZip(out, OutputMode.DexIndexed);
+    ProcessResult result = ToolHelper.runArtRaw(out.toString(), mainClass);
+    return new TestRunResult(app, result);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
new file mode 100644
index 0000000..c6af2c7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -0,0 +1,95 @@
+// Copyright (c) 2018, 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 com.android.tools.r8.TestBase.Backend;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.AndroidAppConsumers;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collection;
+
+public abstract class TestCompilerBuilder<
+        C extends BaseCompilerCommand,
+        B extends BaseCompilerCommand.Builder<C, B>,
+        T extends TestCompilerBuilder<C, B, T>>
+    extends TestBuilder<T> {
+
+  private final B builder;
+  private final Backend backend;
+
+  // Default initialized setup. Can be overwritten if needed.
+  private Path defaultLibrary;
+  private ProgramConsumer programConsumer;
+  private AndroidApiLevel defaultMinApiLevel = ToolHelper.getMinApiLevelForDexVm();
+
+  TestCompilerBuilder(TestState state, B builder, Backend backend) {
+    super(state);
+    this.builder = builder;
+    this.backend = backend;
+    defaultLibrary = TestBase.runtimeJar(backend);
+    programConsumer = TestBase.emptyConsumer(backend);
+  }
+
+  abstract T self();
+
+  abstract void internalCompile(B builder) throws CompilationFailedException;
+
+  public TestCompileResult compile() throws CompilationFailedException {
+    AndroidAppConsumers sink = new AndroidAppConsumers();
+    builder.setProgramConsumer(sink.wrapProgramConsumer(programConsumer));
+    if (defaultLibrary != null) {
+      builder.addLibraryFiles(defaultLibrary);
+    }
+    if (backend == Backend.DEX && defaultMinApiLevel != null) {
+      builder.setMinApiLevel(defaultMinApiLevel.getLevel());
+    }
+    internalCompile(builder);
+    return new TestCompileResult(getState(), backend, sink.build());
+  }
+
+  @Override
+  public TestRunResult run(String mainClass) throws IOException, CompilationFailedException {
+    return compile().run(mainClass);
+  }
+
+  public T setMode(CompilationMode mode) {
+    builder.setMode(mode);
+    return self();
+  }
+
+  public T debug() {
+    return setMode(CompilationMode.DEBUG);
+  }
+
+  public T release() {
+    return setMode(CompilationMode.RELEASE);
+  }
+
+  public T setMinApi(AndroidApiLevel minApiLevel) {
+    // Should we ignore min-api calls when backend == CF?
+    this.defaultMinApiLevel = null;
+    builder.setMinApiLevel(minApiLevel.getLevel());
+    return self();
+  }
+
+  public T setProgramConsumer(ProgramConsumer programConsumer) {
+    assert programConsumer != null;
+    this.programConsumer = programConsumer;
+    return self();
+  }
+
+  @Override
+  public T addProgramFiles(Collection<Path> files) {
+    builder.addProgramFiles(files);
+    return self();
+  }
+
+  @Override
+  public T addLibraryFiles(Collection<Path> files) {
+    defaultLibrary = null;
+    builder.addLibraryFiles(files);
+    return self();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestRunResult.java b/src/test/java/com/android/tools/r8/TestRunResult.java
new file mode 100644
index 0000000..462ca1f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/TestRunResult.java
@@ -0,0 +1,68 @@
+// Copyright (c) 2018, 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+
+public class TestRunResult {
+  private final AndroidApp app;
+  private final ProcessResult result;
+
+  public TestRunResult(AndroidApp app, ProcessResult result) {
+    this.app = app;
+    this.result = result;
+  }
+
+  public TestRunResult assertSuccess() {
+    assertEquals(errorMessage("Expected run to succeed."), 0, result.exitCode);
+    return this;
+  }
+
+  public TestRunResult assertFailure() {
+    assertNotEquals(errorMessage("Expected run to fail."), 0, result.exitCode);
+    return this;
+  }
+
+  public void assertSuccessWithOutput(String expected) {
+    assertSuccess();
+    assertEquals(errorMessage("Run std output incorrect."), expected, result.stdout);
+  }
+
+  public CodeInspector inspector() throws IOException, ExecutionException {
+    // Inspection post run implies success. If inspection of an invalid program is needed it should
+    // be done on the compilation result or on the input.
+    assertSuccess();
+    assertNotNull(app);
+    return new CodeInspector(app);
+  }
+
+  private String errorMessage(String message) {
+    StringBuilder builder = new StringBuilder(message).append('\n');
+    printInfo(builder);
+    return builder.toString();
+  }
+
+  private void printInfo(StringBuilder builder) {
+    builder.append("APPLICATION: ");
+    printApplication(builder);
+    builder.append('\n');
+    printProcessResult(builder);
+  }
+
+  private void printApplication(StringBuilder builder) {
+    builder.append(app == null ? "<default>" : app.toString());
+  }
+
+  private void printProcessResult(StringBuilder builder) {
+    builder.append("COMMAND: ").append(result.command).append('\n').append(result);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestState.java b/src/test/java/com/android/tools/r8/TestState.java
new file mode 100644
index 0000000..d516c7c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/TestState.java
@@ -0,0 +1,21 @@
+// Copyright (c) 2018, 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 java.io.IOException;
+import java.nio.file.Path;
+import org.junit.rules.TemporaryFolder;
+
+public class TestState {
+
+  private final TemporaryFolder temp;
+
+  public TestState(TemporaryFolder temp) {
+    this.temp = temp;
+  }
+
+  public Path getNewTempFolder() throws IOException {
+    return temp.newFolder().toPath();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 515d191..8ae0dad 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -1531,11 +1531,17 @@
     public final int exitCode;
     public final String stdout;
     public final String stderr;
+    public final String command;
 
-    ProcessResult(int exitCode, String stdout, String stderr) {
+    ProcessResult(int exitCode, String stdout, String stderr, String command) {
       this.exitCode = exitCode;
       this.stdout = stdout;
       this.stderr = stderr;
+      this.command = command;
+    }
+
+    ProcessResult(int exitCode, String stdout, String stderr) {
+      this(exitCode, stdout, stderr, null);
     }
 
     @Override
@@ -1574,7 +1580,8 @@
   }
 
   public static ProcessResult runProcess(ProcessBuilder builder) throws IOException {
-    System.out.println(String.join(" ", builder.command()));
+    String command = String.join(" ", builder.command());
+    System.out.println(command);
     Process p = builder.start();
     // Drain stdout and stderr so that the process does not block. Read stdout and stderr
     // in parallel to make sure that neither buffer can get filled up which will cause the
@@ -1592,7 +1599,8 @@
     } catch (InterruptedException e) {
       throw new RuntimeException("Execution interrupted", e);
     }
-    return new ProcessResult(p.exitValue(), stdoutReader.getResult(), stderrReader.getResult());
+    return new ProcessResult(
+        p.exitValue(), stdoutReader.getResult(), stderrReader.getResult(), command);
   }
 
   public static R8Command.Builder addProguardConfigurationConsumer(
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/IndirectSuperInterfaceTest.java b/src/test/java/com/android/tools/r8/memberrebinding/IndirectSuperInterfaceTest.java
index 9552a61..977b925 100644
--- a/src/test/java/com/android/tools/r8/memberrebinding/IndirectSuperInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/memberrebinding/IndirectSuperInterfaceTest.java
@@ -3,17 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.memberrebinding;
 
-import static org.junit.Assert.assertEquals;
-
 import com.android.tools.r8.NeverInline;
-import com.android.tools.r8.R8;
-import com.android.tools.r8.R8Command;
-import com.android.tools.r8.R8Command.Builder;
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.utils.AndroidAppConsumers;
+import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
 import java.util.List;
 import org.junit.Test;
@@ -149,38 +141,23 @@
 
   @Test
   public void test() throws Exception {
-    String expected =
-        String.join(
-            System.lineSeparator(),
-            "A::method -> InterfaceA::method",
-            "B::method -> InterfaceB::method",
-            "C::method -> InterfaceC::method",
-            "D::method -> InterfaceD::method");
-    assertEquals(expected, runOnJava(TestClass.class));
+    String expected = StringUtils.joinLines(
+        "A::method -> InterfaceA::method",
+        "B::method -> InterfaceB::method",
+        "C::method -> InterfaceC::method",
+        "D::method -> InterfaceD::method");
 
-    AndroidAppConsumers sink = new AndroidAppConsumers();
-    Builder builder = R8Command.builder();
-    for (Class<?> clazz : CLASSES) {
-      builder.addClassProgramData(ToolHelper.getClassAsBytes(clazz), Origin.unknown());
-    }
-    builder
-        .setProgramConsumer(sink.wrapProgramConsumer(emptyConsumer(backend)))
-        .addLibraryFiles(runtimeJar(backend))
-        .addProguardConfiguration(
-            ImmutableList.of(
-                // Keep all classes to prevent changes to the class hierarchy (e.g., due to
-                // vertical class merging).
-                "-keep class " + InterfaceA.class.getPackage().getName() + ".*",
-                keepMainProguardConfigurationWithInliningAnnotation(TestClass.class)),
-            Origin.unknown());
-    ToolHelper.allowTestProguardOptions(builder);
-    if (backend == Backend.DEX) {
-      builder.setMinApiLevel(ToolHelper.getMinApiLevelForDexVm().getLevel());
-    }
-    R8.run(builder.build());
+    testForJvm()
+        .addTestClasspath()
+        .run(TestClass.class)
+        .assertSuccessWithOutput(expected);
 
-    ProcessResult result = runOnVMRaw(sink.build(), TestClass.class, backend);
-    assertEquals(result.toString(), 0, result.exitCode);
-    assertEquals(expected, result.stdout);
+    testForR8(backend)
+        .addProgramClasses(CLASSES)
+        .addKeepPackageRules(TestClass.class.getPackage())
+        .addKeepMainRule(TestClass.class)
+        .enableInliningAnnotations()
+        .run(TestClass.class)
+        .assertSuccessWithOutput(expected);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/KeepAttributesTest.java b/src/test/java/com/android/tools/r8/shaking/KeepAttributesTest.java
index 96ce54a..6992f97 100644
--- a/src/test/java/com/android/tools/r8/shaking/KeepAttributesTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/KeepAttributesTest.java
@@ -9,15 +9,8 @@
 
 import com.android.tools.r8.CompilationFailedException;
 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.origin.Origin;
 import com.android.tools.r8.shaking.forceproguardcompatibility.keepattributes.TestKeepAttributes;
-import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.AndroidAppConsumers;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
@@ -70,7 +63,6 @@
   public void keepLineNumberTable()
       throws CompilationFailedException, IOException, ExecutionException {
     List<String> keepRules = ImmutableList.of(
-        "-keep class ** { *; }",
         "-keepattributes " + ProguardKeepAttributes.LINE_NUMBER_TABLE
     );
     MethodSubject mainMethod = compileRunAndGetMain(keepRules, CompilationMode.RELEASE);
@@ -82,7 +74,6 @@
   public void keepLineNumberTableAndLocalVariableTable()
       throws CompilationFailedException, IOException, ExecutionException {
     List<String> keepRules = ImmutableList.of(
-        "-keep class ** { *; }",
         "-keepattributes "
             + ProguardKeepAttributes.LINE_NUMBER_TABLE
             + ", "
@@ -97,7 +88,6 @@
   @Test
   public void keepLocalVariableTable() throws IOException, ExecutionException {
     List<String> keepRules = ImmutableList.of(
-        "-keep class ** { *; }",
         "-keepattributes " + ProguardKeepAttributes.LOCAL_VARIABLE_TABLE
     );
     // Compiling with a keep rule for locals but no line results in an error in R8.
@@ -113,21 +103,14 @@
 
   private MethodSubject compileRunAndGetMain(List<String> keepRules, CompilationMode mode)
       throws CompilationFailedException, IOException, ExecutionException {
-    AndroidAppConsumers sink = new AndroidAppConsumers();
-    R8.run(
-        R8Command.builder()
-            .setMode(mode)
-            .addProgramFiles(
-                ToolHelper.getClassFilesForTestDirectory(
-                    ToolHelper.getClassFileForTestClass(CLASS).getParent()))
-            .addLibraryFiles(runtimeJar(backend))
-            .addProguardConfiguration(keepRules, Origin.unknown())
-            .setProgramConsumer(sink.wrapProgramConsumer(emptyConsumer(backend)))
-            .build());
-    AndroidApp app = sink.build();
-    CodeInspector codeInspector = new CodeInspector(app);
-    runOnVM(app, CLASS.getTypeName(), backend);
-    return codeInspector.clazz(CLASS).mainMethod();
+    return testForR8(backend)
+        .setMode(mode)
+        .addProgramClassesAndInnerClasses(CLASS)
+        .addKeepAllClassesRule()
+        .addKeepRules(keepRules)
+        .run(CLASS)
+        .inspector()
+        .clazz(CLASS)
+        .mainMethod();
   }
-
 }