Merge "Add easier control of whether data resources are processed or not by R8"
diff --git a/src/main/java/com/android/tools/r8/BaseCommand.java b/src/main/java/com/android/tools/r8/BaseCommand.java
index 8828248..a537d7f 100644
--- a/src/main/java/com/android/tools/r8/BaseCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCommand.java
@@ -172,7 +172,7 @@
                   try {
                     app.addProgramFile(path);
                     programFiles.add(path);
-                  } catch (IOException | CompilationError e) {
+                  } catch (CompilationError e) {
                     error(new ProgramInputOrigin(path), e);
                   }
                 });
diff --git a/src/main/java/com/android/tools/r8/CompatDxHelper.java b/src/main/java/com/android/tools/r8/CompatDxHelper.java
index f540ed0..a84bab5 100644
--- a/src/main/java/com/android/tools/r8/CompatDxHelper.java
+++ b/src/main/java/com/android/tools/r8/CompatDxHelper.java
@@ -6,10 +6,10 @@
 
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
-import java.io.IOException;
 
 public class CompatDxHelper {
-  public static void run(D8Command command, Boolean minimalMainDex) throws IOException {
+  public static void run(D8Command command, Boolean minimalMainDex)
+      throws CompilationFailedException {
     AndroidApp app = command.getInputApp();
     InternalOptions options = command.getInternalOptions();
     // DX does not desugar.
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 76e7277..68d9095 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -110,7 +110,7 @@
     }
     InternalOptions options = command.getInternalOptions();
     AndroidApp app = command.getInputApp();
-    ExceptionUtils.withD8CompilationHandler(options.reporter, () -> runForTesting(app, options));
+    runForTesting(app, options);
   }
 
   /**
@@ -126,13 +126,18 @@
     ExceptionUtils.withMainProgramHandler(() -> run(args));
   }
 
-  static void runForTesting(AndroidApp inputApp, InternalOptions options) throws IOException {
+  static void runForTesting(AndroidApp inputApp, InternalOptions options)
+      throws CompilationFailedException {
     ExecutorService executor = ThreadUtils.getExecutorService(options);
-    try {
-      run(inputApp, options, executor);
-    } finally {
-      executor.shutdown();
-    }
+    ExceptionUtils.withD8CompilationHandler(
+        options.reporter,
+        () -> {
+          try {
+            run(inputApp, options, executor);
+          } finally {
+            executor.shutdown();
+          }
+        });
   }
 
   // Compute the marker to be placed in the main dex file.
diff --git a/src/main/java/com/android/tools/r8/DexFileMergerHelper.java b/src/main/java/com/android/tools/r8/DexFileMergerHelper.java
index d7183c8..b6bb4e0 100644
--- a/src/main/java/com/android/tools/r8/DexFileMergerHelper.java
+++ b/src/main/java/com/android/tools/r8/DexFileMergerHelper.java
@@ -115,7 +115,7 @@
   }
 
   public static void runD8ForTesting(D8Command command, boolean dontCreateMarkerInD8)
-      throws IOException {
+      throws CompilationFailedException {
     InternalOptions options = command.getInternalOptions();
     options.testing.dontCreateMarkerInD8 = dontCreateMarkerInD8;
     D8.runForTesting(command.getInputApp(), options);
diff --git a/src/main/java/com/android/tools/r8/DexRoundTrip.java b/src/main/java/com/android/tools/r8/DexRoundTrip.java
index b6bebbb..fcf308b 100644
--- a/src/main/java/com/android/tools/r8/DexRoundTrip.java
+++ b/src/main/java/com/android/tools/r8/DexRoundTrip.java
@@ -5,10 +5,8 @@
 
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.AndroidAppConsumers;
-import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
-import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 
@@ -20,11 +18,10 @@
     InternalOptions options = command.getInternalOptions();
     AndroidApp app = command.getInputApp();
     options.passthroughDexCode = false;
-    ExceptionUtils.withD8CompilationHandler(options.reporter, () -> D8.runForTesting(app, options));
+    D8.runForTesting(app, options);
   }
 
-  public static void main(String[] args)
-      throws CompilationFailedException, IOException, ResourceException {
+  public static void main(String[] args) throws CompilationFailedException {
     D8Command.Builder builder = D8Command.builder();
     for (String arg : args) {
       Path file = Paths.get(arg);
diff --git a/src/main/java/com/android/tools/r8/DexSplitterHelper.java b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
index 163c46f..51424d5 100644
--- a/src/main/java/com/android/tools/r8/DexSplitterHelper.java
+++ b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
@@ -136,7 +136,7 @@
   }
 
   public static void runD8ForTesting(D8Command command, boolean dontCreateMarkerInD8)
-      throws IOException {
+      throws CompilationFailedException {
     InternalOptions options = command.getInternalOptions();
     options.testing.dontCreateMarkerInD8 = dontCreateMarkerInD8;
     D8.runForTesting(command.getInputApp(), options);
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 6de196e..5d3f795 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -126,16 +126,7 @@
   public static void run(R8Command command) throws CompilationFailedException {
     AndroidApp app = command.getInputApp();
     InternalOptions options = command.getInternalOptions();
-    ExecutorService executor = ThreadUtils.getExecutorService(options);
-    ExceptionUtils.withR8CompilationHandler(
-        command.getReporter(),
-        () -> {
-          try {
-            run(app, options, executor);
-          } finally {
-            executor.shutdown();
-          }
-        });
+    runForTesting(app, options);
   }
 
   /**
@@ -218,13 +209,18 @@
     return result;
   }
 
-  static void runForTesting(AndroidApp app, InternalOptions options) throws IOException {
+  static void runForTesting(AndroidApp app, InternalOptions options)
+      throws CompilationFailedException {
     ExecutorService executor = ThreadUtils.getExecutorService(options);
-    try {
-      run(app, options, executor);
-    } finally {
-      executor.shutdown();
-    }
+    ExceptionUtils.withR8CompilationHandler(
+        options.reporter,
+        () -> {
+          try {
+            run(app, options, executor);
+          } finally {
+            executor.shutdown();
+          }
+        });
   }
 
   private static void run(AndroidApp app, InternalOptions options, ExecutorService executor)
diff --git a/src/main/java/com/android/tools/r8/naming/NamingState.java b/src/main/java/com/android/tools/r8/naming/NamingState.java
index fc00d91..6b64017 100644
--- a/src/main/java/com/android/tools/r8/naming/NamingState.java
+++ b/src/main/java/com/android/tools/r8/naming/NamingState.java
@@ -22,7 +22,7 @@
 class NamingState<ProtoType extends CachedHashValueDexItem, KeyType> {
 
   private final NamingState<ProtoType, KeyType> parent;
-  private final Map<KeyType, InternalState<ProtoType>> usedNames = new HashMap<>();
+  private final Map<KeyType, InternalState> usedNames = new HashMap<>();
   private final DexItemFactory itemFactory;
   private final ImmutableList<String> dictionary;
   private final Function<ProtoType, KeyType> keyTransform;
@@ -53,25 +53,25 @@
     return new NamingState<>(this, itemFactory, dictionary, keyTransform, useUniqueMemberNames);
   }
 
-  private InternalState<ProtoType> findInternalStateFor(ProtoType proto) {
+  private InternalState findInternalStateFor(ProtoType proto) {
     KeyType key = keyTransform.apply(proto);
-    InternalState<ProtoType> result = usedNames.get(key);
+    InternalState result = usedNames.get(key);
     if (result == null && parent != null) {
       result = parent.findInternalStateFor(proto);
     }
     return result;
   }
 
-  private InternalState<ProtoType> getOrCreateInternalStateFor(ProtoType proto) {
+  private InternalState getOrCreateInternalStateFor(ProtoType proto) {
     // TODO(herhut): Maybe allocate these sparsely and search via state chain.
     KeyType key = keyTransform.apply(proto);
-    InternalState<ProtoType> result = usedNames.get(key);
+    InternalState result = usedNames.get(key);
     if (result == null) {
       if (parent != null) {
-        InternalState<ProtoType> parentState = parent.getOrCreateInternalStateFor(proto);
+        InternalState parentState = parent.getOrCreateInternalStateFor(proto);
         result = parentState.createChild();
       } else {
-        result = new InternalState<>(itemFactory, null, dictionary);
+        result = new InternalState(itemFactory, null, dictionary);
       }
       usedNames.put(key, result);
     }
@@ -79,7 +79,7 @@
   }
 
   private DexString getAssignedNameFor(DexString name, ProtoType proto) {
-    InternalState<ProtoType> state = findInternalStateFor(proto);
+    InternalState state = findInternalStateFor(proto);
     if (state == null) {
       return null;
     }
@@ -89,19 +89,19 @@
   public DexString assignNewNameFor(DexString original, ProtoType proto, boolean markAsUsed) {
     DexString result = getAssignedNameFor(original, proto);
     if (result == null) {
-      InternalState<ProtoType> state = getOrCreateInternalStateFor(proto);
+      InternalState state = getOrCreateInternalStateFor(proto);
       result = state.getNameFor(original, proto, markAsUsed);
     }
     return result;
   }
 
   public void reserveName(DexString name, ProtoType proto) {
-    InternalState<ProtoType> state = getOrCreateInternalStateFor(proto);
+    InternalState state = getOrCreateInternalStateFor(proto);
     state.reserveName(name);
   }
 
   public boolean isReserved(DexString name, ProtoType proto) {
-    InternalState<ProtoType> state = findInternalStateFor(proto);
+    InternalState state = findInternalStateFor(proto);
     if (state == null) {
       return false;
     }
@@ -109,7 +109,7 @@
   }
 
   public boolean isAvailable(DexString original, ProtoType proto, DexString candidate) {
-    InternalState<ProtoType> state = findInternalStateFor(proto);
+    InternalState state = findInternalStateFor(proto);
     if (state == null) {
       return true;
     }
@@ -118,25 +118,25 @@
   }
 
   public void addRenaming(DexString original, ProtoType proto, DexString newName) {
-    InternalState<ProtoType> state = getOrCreateInternalStateFor(proto);
+    InternalState state = getOrCreateInternalStateFor(proto);
     state.addRenaming(original, proto, newName);
   }
 
-  private class InternalState<InternalProtoType extends CachedHashValueDexItem> {
+  private class InternalState {
 
     private static final int INITIAL_NAME_COUNT = 1;
     private final char[] EMPTY_CHAR_ARRARY = new char[0];
 
     protected final DexItemFactory itemFactory;
-    private final InternalState<InternalProtoType> parentInternalState;
+    private final InternalState parentInternalState;
     private Set<DexString> reservedNames = null;
-    private Table<DexString, InternalProtoType, DexString> renamings = null;
+    private Table<DexString, ProtoType, DexString> renamings = null;
     private int nameCount;
     private final Iterator<String> dictionaryIterator;
 
     private InternalState(
         DexItemFactory itemFactory,
-        InternalState<InternalProtoType> parentInternalState,
+        InternalState parentInternalState,
         Iterator<String> dictionaryIterator) {
       this.itemFactory = itemFactory;
       this.parentInternalState = parentInternalState;
@@ -146,9 +146,7 @@
     }
 
     private InternalState(
-        DexItemFactory itemFactory,
-        InternalState<InternalProtoType> parentInternalState,
-        List<String> dictionary) {
+        DexItemFactory itemFactory, InternalState parentInternalState, List<String> dictionary) {
       this(itemFactory, parentInternalState, dictionary.iterator());
     }
 
@@ -163,8 +161,8 @@
           && (parentInternalState == null || parentInternalState.isAvailable(name));
     }
 
-    InternalState<InternalProtoType> createChild() {
-      return new InternalState<>(itemFactory, this, dictionaryIterator);
+    InternalState createChild() {
+      return new InternalState(itemFactory, this, dictionaryIterator);
     }
 
     void reserveName(DexString name) {
@@ -174,11 +172,11 @@
       reservedNames.add(name);
     }
 
-    DexString getAssignedNameFor(DexString original, InternalProtoType proto) {
+    DexString getAssignedNameFor(DexString original, ProtoType proto) {
       DexString result = null;
       if (renamings != null) {
         if (useUniqueMemberNames) {
-          Map<InternalProtoType, DexString> row = renamings.row(original);
+          Map<ProtoType, DexString> row = renamings.row(original);
           if (row != null) {
             // Either not renamed yet (0) or renamed (1). If renamed, return the same renamed name
             // so that other members with the same name can be renamed to the same renamed name.
@@ -196,7 +194,7 @@
       return result;
     }
 
-    DexString getNameFor(DexString original, InternalProtoType proto, boolean markAsUsed) {
+    DexString getNameFor(DexString original, ProtoType proto, boolean markAsUsed) {
       DexString name = getAssignedNameFor(original, proto);
       if (name != null) {
         return name;
@@ -210,7 +208,7 @@
       return name;
     }
 
-    void addRenaming(DexString original, InternalProtoType proto, DexString newName) {
+    void addRenaming(DexString original, ProtoType proto, DexString newName) {
       if (renamings == null) {
         renamings = HashBasedTable.create();
       }
diff --git a/src/main/java/com/android/tools/r8/shaking/ReasonPrinter.java b/src/main/java/com/android/tools/r8/shaking/ReasonPrinter.java
index 3b3063d..1eb43f5 100644
--- a/src/main/java/com/android/tools/r8/shaking/ReasonPrinter.java
+++ b/src/main/java/com/android/tools/r8/shaking/ReasonPrinter.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.DexItem;
 import com.android.tools.r8.graph.DexType;
 import com.google.common.collect.Sets;
+import java.io.PrintStream;
 import java.util.ArrayDeque;
 import java.util.Arrays;
 import java.util.Collections;
@@ -136,6 +137,8 @@
 
     private int indentation = -1;
 
+    private PrintStream output = System.out;
+
     void pushIsLivePrefix() {
       prefixes.push("is live because ");
     }
@@ -159,27 +162,27 @@
     void startItem(DexItem item) {
       indentation++;
       indent();
-      System.out.println(item.toSourceString());
+      output.println(item.toSourceString());
     }
 
     private void indent() {
       for (int i = 0; i < indentation; i++) {
-        System.out.print("  ");
+        output.print("  ");
       }
     }
 
     void addReason(String thing) {
       indent();
-      System.out.print("|- ");
+      output.print("|- ");
       String prefix = prefixes.peek();
-      System.out.print(prefix);
-      System.out.println(thing);
+      output.print(prefix);
+      output.println(thing);
     }
 
     void addMessage(String thing) {
       indent();
-      System.out.print("|  ");
-      System.out.println(thing);
+      output.print("|  ");
+      output.println(thing);
     }
 
     void endItem() {
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 4dff93c..383b9c3 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -12,7 +12,6 @@
 import com.android.tools.r8.ClassFileResourceProvider;
 import com.android.tools.r8.DataEntryResource;
 import com.android.tools.r8.DataResourceProvider;
-import com.android.tools.r8.DataResourceProvider.Visitor;
 import com.android.tools.r8.DexFilePerClassFileConsumer;
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.DirectoryClassFileProvider;
@@ -303,12 +302,12 @@
     }
 
     /** Add program file resources. */
-    public Builder addProgramFiles(Path... files) throws NoSuchFileException {
+    public Builder addProgramFiles(Path... files) {
       return addProgramFiles(Arrays.asList(files));
     }
 
     /** Add program file resources. */
-    public Builder addProgramFiles(Collection<Path> files) throws NoSuchFileException {
+    public Builder addProgramFiles(Collection<Path> files) {
       for (Path file : files) {
         addProgramFile(file);
       }
@@ -366,12 +365,12 @@
     }
 
     /** Add library file resources. */
-    public Builder addLibraryFiles(Path... files) throws IOException {
+    public Builder addLibraryFiles(Path... files) {
       return addLibraryFiles(Arrays.asList(files));
     }
 
     /** Add library file resources. */
-    public Builder addLibraryFiles(Collection<Path> files) throws IOException {
+    public Builder addLibraryFiles(Collection<Path> files) {
       for (Path file : files) {
         addClasspathOrLibraryProvider(file, libraryResourceProviders);
       }
@@ -578,9 +577,11 @@
           mainDexListClasses);
     }
 
-    public void addProgramFile(Path file) throws NoSuchFileException {
+    public void addProgramFile(Path file) {
       if (!Files.exists(file)) {
-        throw new NoSuchFileException(file.toString());
+        PathOrigin pathOrigin = new PathOrigin(file);
+        NoSuchFileException noSuchFileException = new NoSuchFileException(file.toString());
+        reporter.error(new ExceptionDiagnostic(noSuchFileException, pathOrigin));
       }
       if (isDexFile(file)) {
         addProgramResources(ProgramResource.fromFile(Kind.DEX, file));
@@ -610,12 +611,18 @@
     }
 
     private void addClasspathOrLibraryProvider(
-        Path file, List<ClassFileResourceProvider> providerList) throws IOException {
+        Path file, List<ClassFileResourceProvider> providerList) {
       if (!Files.exists(file)) {
-        throw new NoSuchFileException(file.toString());
+        reporter.error(
+            new ExceptionDiagnostic(
+                new NoSuchFileException(file.toString()), new PathOrigin(file)));
       }
       if (isArchive(file)) {
-        providerList.add(new ArchiveClassFileProvider(file));
+        try {
+          providerList.add(new ArchiveClassFileProvider(file));
+        } catch (IOException e) {
+          reporter.error(new ExceptionDiagnostic(e, new PathOrigin(file)));
+        }
       } else if (Files.isDirectory(file) ) {
         providerList.add(DirectoryClassFileProvider.fromDirectory(file));
       } else {
diff --git a/src/test/java/com/android/tools/r8/AsmTestBase.java b/src/test/java/com/android/tools/r8/AsmTestBase.java
index 35767fb..2c4cb6b 100644
--- a/src/test/java/com/android/tools/r8/AsmTestBase.java
+++ b/src/test/java/com/android/tools/r8/AsmTestBase.java
@@ -8,9 +8,7 @@
 
 import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
 import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
@@ -94,17 +92,17 @@
       throws Exception {
     // TODO(zerny): Port this to use diagnostics handler.
     AndroidApp app = buildAndroidApp(classes);
-    CompilationError r8Error = null;
-    CompilationError r8ShakenError = null;
+    CompilationFailedException r8Error = null;
+    CompilationFailedException r8ShakenError = null;
     try {
       runOnArtRaw(compileWithR8(app), main);
-    } catch (CompilationError e) {
+    } catch (CompilationFailedException e) {
       r8Error = e;
     }
     try {
       runOnArtRaw(compileWithR8(app, keepMainProguardConfiguration(main) + "-dontobfuscate\n"),
           main);
-    } catch (CompilationError e) {
+    } catch (CompilationFailedException e) {
       r8ShakenError = e;
     }
     Assert.assertNotNull(r8Error);
@@ -112,7 +110,7 @@
   }
 
   protected void ensureSameOutputAfterMerging(String main, byte[]... classes)
-      throws IOException, CompilationFailedException, ProguardRuleParserException {
+      throws IOException, CompilationFailedException {
     AndroidApp app = buildAndroidApp(classes);
     // Compile to dex files with D8.
     AndroidApp dexApp = compileWithD8(app);
diff --git a/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java
index e01373c..6ccf086 100644
--- a/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java
@@ -4,20 +4,15 @@
 
 package com.android.tools.r8;
 
-import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.errors.InternalCompilerError;
-import com.android.tools.r8.errors.Unimplemented;
+import static org.junit.Assert.assertTrue;
+
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.OffOrAuto;
 import java.nio.file.Path;
 import java.util.function.UnaryOperator;
-import org.hamcrest.core.CombinableMatcher;
-import org.hamcrest.core.IsInstanceOf;
-import org.hamcrest.core.StringContains;
 import org.junit.Assert;
 import org.junit.Assume;
 import org.junit.Test;
-import org.junit.internal.matchers.ThrowableMessageMatcher;
 
 public class D8RunExamplesAndroidOTest extends RunExamplesAndroidOTest<D8Command.Builder> {
 
@@ -37,7 +32,7 @@
     }
 
     @Override
-    void build(Path inputFile, Path out, OutputMode mode) throws Throwable {
+    void build(Path inputFile, Path out, OutputMode mode) throws CompilationFailedException {
       D8Command.Builder builder = D8Command.builder().setOutput(out, mode);
       for (UnaryOperator<D8Command.Builder> transformation : builderTransformations) {
         builder = transformation.apply(builder);
@@ -46,13 +41,7 @@
           ToolHelper.getAndroidJar(
               androidJarVersion == null ? builder.getMinApiLevel() : androidJarVersion.getLevel()));
       builder.addProgramFiles(inputFile);
-      try {
-        ToolHelper.runD8(builder, this::combinedOptionConsumer);
-      } catch (Unimplemented | CompilationError | InternalCompilerError re) {
-        throw re;
-      } catch (RuntimeException re) {
-        throw re.getCause() == null ? re : re.getCause();
-      }
+      ToolHelper.runD8(builder, this::combinedOptionConsumer);
     }
 
     D8TestRunner withIntermediate(boolean intermediate) {
@@ -77,7 +66,7 @@
 
       // compilation should have failed on CompilationError since A is declaring a default method.
       Assert.fail();
-    } catch (CompilationError e) {
+    } catch (CompilationFailedException e) {
       // Expected.
     }
   }
@@ -363,13 +352,14 @@
             .withClasspath(lib1.getInputJar())
             .withClasspath(lib2.getInputJar())
             .withMinApiLevel(minApi);
-    thrown.expect(
-        new CombinableMatcher<CompilationError>(new IsInstanceOf(CompilationError.class))
-        .and(new ThrowableMessageMatcher<CompilationError>(
-            new StringContains("desugaringwithmissingclasstest2.ImplementMethodsWithDefault")))
-        .and(new ThrowableMessageMatcher<CompilationError>(
-            new StringContains("desugaringwithmissingclasslib3.C"))));
-    test.build();
+    try {
+      test.build();
+      Assert.fail("Expected build to fail with CompilationFailedException");
+    } catch (CompilationFailedException e) {
+      String message = e.getCause().getMessage();
+      assertTrue(message.contains("desugaringwithmissingclasstest2.ImplementMethodsWithDefault"));
+      assertTrue(message.contains("desugaringwithmissingclasslib3.C"));
+    }
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index 37515d2..3ef4037 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -1408,8 +1408,7 @@
       boolean disableInlining,
       boolean disableClassInlining,
       boolean hasMissingClasses)
-      throws IOException, ProguardRuleParserException, ExecutionException,
-      CompilationFailedException {
+      throws CompilationFailedException {
     executeCompilerUnderTest(compilerUnderTest, fileNames, resultPath, compilationMode, null,
         disableInlining, disableClassInlining, hasMissingClasses);
   }
@@ -1423,8 +1422,7 @@
       boolean disableInlining,
       boolean disableClassInlining,
       boolean hasMissingClasses)
-      throws IOException, ProguardRuleParserException, ExecutionException,
-        CompilationFailedException {
+      throws CompilationFailedException {
     assert mode != null;
     switch (compilerUnderTest) {
       case D8_AFTER_R8CF:
@@ -1956,8 +1954,6 @@
             specification.hasMissingClasses);
       } catch (CompilationFailedException e) {
         throw new CompilationError(e.getMessage(), e);
-      } catch (ExecutionException e) {
-        throw e.getCause();
       }
       System.err.println("Should have failed R8/D8 compilation with a CompilationError.");
       return;
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java b/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
index 008c6b9..428c611 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
@@ -12,7 +12,6 @@
 import com.android.tools.r8.R8RunArtTestsTest.DexTool;
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
 import com.android.tools.r8.utils.TestDescriptionWatcher;
 import java.io.IOException;
@@ -170,19 +169,16 @@
                   .setOutput(getOutputFile(), outputMode)
                   .setMode(mode)
                   .build();
-        ExceptionUtils.withR8CompilationHandler(
-            command.getReporter(),
-            () ->
-                ToolHelper.runR8(
-                    command,
-                    options -> {
-                      options.lineNumberOptimization = LineNumberOptimization.OFF;
-                      options.enableCfFrontend = frontend == Frontend.CF;
-                      if (output == Output.CF) {
-                        // Class inliner is not supported with CF backend yet.
-                        options.enableClassInlining = false;
-                      }
-                    }));
+          ToolHelper.runR8(
+              command,
+              options -> {
+                options.lineNumberOptimization = LineNumberOptimization.OFF;
+                options.enableCfFrontend = frontend == Frontend.CF;
+                if (output == Output.CF) {
+                  // Class inliner is not supported with CF backend yet.
+                  options.enableClassInlining = false;
+                }
+              });
         break;
       }
       default:
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java
index 83df845..5b4482e 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java
@@ -129,9 +129,8 @@
         .run();
   }
 
-  @Test
+  @Test(expected = CompilationFailedException.class)
   public void staticInterfaceMethodsErrorDueToMinSdk() throws Throwable {
-    thrown.expect(ApiLevelException.class);
     test("staticinterfacemethods-error-due-to-min-sdk", "interfacemethods",
         "StaticInterfaceMethods")
         .withInterfaceMethodDesugaring(OffOrAuto.Off)
@@ -146,9 +145,8 @@
         .run();
   }
 
-  @Test
+  @Test(expected = CompilationFailedException.class)
   public void defaultMethodsErrorDueToMinSdk() throws Throwable {
-    thrown.expect(ApiLevelException.class);
     test("defaultmethods-error-due-to-min-sdk", "interfacemethods",
         "DefaultMethods")
         .withInterfaceMethodDesugaring(OffOrAuto.Off)
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
index 1f0c464..68aec3f 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
@@ -149,7 +149,7 @@
 
     void run() throws Throwable {
       if (minSdkErrorExpected(testName)) {
-        thrown.expect(ApiLevelException.class);
+        thrown.expect(CompilationFailedException.class);
       }
 
       String qualifiedMainClass = packageName + "." + mainClass;
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
index 7f15eca..177fe89 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
@@ -136,7 +136,7 @@
 
     void run() throws Throwable {
       if (minSdkErrorExpected(testName)) {
-        thrown.expect(ApiLevelException.class);
+        thrown.expect(CompilationFailedException.class);
       }
 
       String qualifiedMainClass = packageName + "." + mainClass;
diff --git a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
index 48a14f3..99ac1f7 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
@@ -139,7 +139,7 @@
 
     void run() throws Throwable {
       if (minSdkErrorExpected(testName)) {
-        thrown.expect(ApiLevelException.class);
+        thrown.expect(CompilationFailedException.class);
       }
 
       String qualifiedMainClass = packageName + "." + mainClass;
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 48e7d1a..22bb5e5 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.code.Instruction;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.SmaliWriter;
@@ -106,10 +107,8 @@
     return file;
   }
 
-  /**
-   * Build an AndroidApp with the specified test classes as byte array.
-   */
-  protected AndroidApp buildAndroidApp(byte[]... classes) throws IOException {
+  /** Build an AndroidApp with the specified test classes as byte array. */
+  protected AndroidApp buildAndroidApp(byte[]... classes) {
     AndroidApp.Builder builder = AndroidApp.builder();
     for (byte[] clazz : classes) {
       builder.addClassProgramData(clazz, Origin.unknown());
@@ -342,7 +341,8 @@
   }
 
   /** Compile an application with R8. */
-  protected AndroidApp compileWithR8(Class... classes) throws IOException {
+  protected AndroidApp compileWithR8(Class... classes)
+      throws IOException, CompilationFailedException {
     return ToolHelper.runR8(readClasses(classes));
   }
 
@@ -582,13 +582,8 @@
    */
   protected String runOnJava(Class mainClass) throws Exception {
     ProcessResult result = ToolHelper.runJava(mainClass);
-    if (result.exitCode != 0) {
-      System.out.println("Std out:");
-      System.out.println(result.stdout);
-      System.out.println("Std err:");
-      System.out.println(result.stderr);
-      assertEquals(0, result.exitCode);
-    }
+    ToolHelper.failOnProcessFailure(result);
+    ToolHelper.failOnVerificationErrors(result);
     return result.stdout;
   }
 
@@ -604,9 +599,17 @@
   }
 
   /** Run application on Java with the specified main class and provided arguments. */
+  protected String runOnJava(AndroidApp app, String mainClass, String... args) throws IOException {
+    return runOnJava(app, mainClass, Arrays.asList(args));
+  }
+
+  /** Run application on Java with the specified main class and provided arguments. */
   protected String runOnJava(AndroidApp app, String mainClass, List<String> args)
       throws IOException {
-    return runOnJavaRaw(app, mainClass, args).stdout;
+    ProcessResult result = runOnJavaRaw(app, mainClass, args);
+    ToolHelper.failOnProcessFailure(result);
+    ToolHelper.failOnVerificationErrors(result);
+    return result.stdout;
   }
 
   protected ProcessResult runOnJavaRawNoVerify(String main, byte[]... classes) throws IOException {
@@ -648,6 +651,18 @@
     return ToolHelper.runJavaNoVerify(out, mainClass, args.toArray(new String[0]));
   }
 
+  /** Run application on Art or Java with the specified main class. */
+  protected String runOnVM(AndroidApp app, String mainClass, Backend backend) throws IOException {
+    switch (backend) {
+      case CF:
+        return runOnJava(app, mainClass);
+      case DEX:
+        return runOnArt(app, mainClass);
+      default:
+        throw new Unreachable("Unexpected backend: " + backend);
+    }
+  }
+
   private String extractClassName(byte[] ccc) {
     class ClassNameExtractor extends ClassVisitor {
       private String className;
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 530518a..e34282d 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -840,39 +840,32 @@
         .setProguardMapConsumer(StringConsumer.emptyConsumer());
   }
 
-  public static AndroidApp runR8(AndroidApp app) throws IOException {
+  public static AndroidApp runR8(AndroidApp app) throws CompilationFailedException {
     return runR8WithProgramConsumer(app, DexIndexedConsumer.emptyConsumer());
   }
 
   public static AndroidApp runR8WithProgramConsumer(AndroidApp app, ProgramConsumer programConsumer)
-      throws IOException {
-    try {
-      return runR8(prepareR8CommandBuilder(app, programConsumer).build());
-    } catch (CompilationFailedException e) {
-      throw new RuntimeException(e);
-    }
+      throws CompilationFailedException {
+    return runR8(prepareR8CommandBuilder(app, programConsumer).build());
   }
 
   public static AndroidApp runR8(AndroidApp app, Consumer<InternalOptions> optionsConsumer)
-      throws IOException {
-    try {
-      return runR8(prepareR8CommandBuilder(app).build(), optionsConsumer);
-    } catch (CompilationFailedException e) {
-      throw new RuntimeException(e);
-    }
+      throws CompilationFailedException {
+    return runR8(prepareR8CommandBuilder(app).build(), optionsConsumer);
   }
 
-  public static AndroidApp runR8(R8Command command) throws IOException {
+  public static AndroidApp runR8(R8Command command) throws CompilationFailedException {
     return runR8(command, null);
   }
 
   public static AndroidApp runR8(R8Command command, Consumer<InternalOptions> optionsConsumer)
-      throws IOException {
+      throws CompilationFailedException {
     return runR8WithFullResult(command, optionsConsumer);
   }
 
   public static AndroidApp runR8WithFullResult(
-      R8Command command, Consumer<InternalOptions> optionsConsumer) throws IOException {
+      R8Command command, Consumer<InternalOptions> optionsConsumer)
+      throws CompilationFailedException {
     // TODO(zerny): Should we really be adding the android library in ToolHelper?
     AndroidApp app = command.getInputApp();
     if (app.getLibraryResourceProviders().isEmpty()) {
@@ -891,39 +884,32 @@
     return compatSink.build();
   }
 
-  public static void addFilteredAndroidJar(BaseCommand.Builder builder, AndroidApiLevel apiLevel)
-      throws IOException {
+  public static void addFilteredAndroidJar(BaseCommand.Builder builder, AndroidApiLevel apiLevel) {
     addFilteredAndroidJar(getAppBuilder(builder), apiLevel);
   }
 
-  public static void addFilteredAndroidJar(AndroidApp.Builder builder, AndroidApiLevel apiLevel)
-      throws IOException {
+  public static void addFilteredAndroidJar(AndroidApp.Builder builder, AndroidApiLevel apiLevel) {
     builder.addFilteredLibraryArchives(Collections.singletonList(
         new FilteredClassPath(getAndroidJar(apiLevel),
             ImmutableList.of("!junit/**", "!android/test/**"))));
   }
 
-  public static AndroidApp runD8(AndroidApp app) throws IOException {
+  public static AndroidApp runD8(AndroidApp app) throws CompilationFailedException {
     return runD8(app, null);
   }
 
   public static AndroidApp runD8(AndroidApp app, Consumer<InternalOptions> optionsConsumer)
-      throws IOException {
-    try {
-      return runD8(D8Command.builder(app), optionsConsumer);
-    } catch (CompilationFailedException e) {
-      throw new RuntimeException(e);
-    }
+      throws CompilationFailedException {
+    return runD8(D8Command.builder(app), optionsConsumer);
   }
 
-  public static AndroidApp runD8(D8Command.Builder builder)
-      throws IOException, CompilationFailedException {
+  public static AndroidApp runD8(D8Command.Builder builder) throws CompilationFailedException {
     return runD8(builder, null);
   }
 
   public static AndroidApp runD8(
       D8Command.Builder builder, Consumer<InternalOptions> optionsConsumer)
-      throws IOException, CompilationFailedException {
+      throws CompilationFailedException {
     AndroidAppConsumers compatSink = new AndroidAppConsumers(builder);
     D8Command command = builder.build();
     InternalOptions options = command.getInternalOptions();
@@ -1310,13 +1296,13 @@
     return runArtNoVerificationErrorsRaw(files, mainClass, extras, version).stdout;
   }
 
-  private static void failOnProcessFailure(ProcessResult result) {
+  protected static void failOnProcessFailure(ProcessResult result) {
     if (result.exitCode != 0) {
-      fail("Unexpected art failure: '" + result.stderr + "'\n" + result.stdout);
+      fail("Unexpected failure: '" + result.stderr + "'\n" + result.stdout);
     }
   }
 
-  private static void failOnVerificationErrors(ProcessResult result) {
+  protected static void failOnVerificationErrors(ProcessResult result) {
     if (result.stderr.contains("Verification error")) {
       fail("Verification error: \n" + result.stderr);
     }
diff --git a/src/test/java/com/android/tools/r8/cf/DebugInfoTestRunner.java b/src/test/java/com/android/tools/r8/cf/DebugInfoTestRunner.java
index ad6a72b..3514a00 100644
--- a/src/test/java/com/android/tools/r8/cf/DebugInfoTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/DebugInfoTestRunner.java
@@ -7,6 +7,7 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.ProgramConsumer;
 import com.android.tools.r8.R8Command;
@@ -14,8 +15,6 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.errors.InvalidDebugInfoException;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.ThrowingConsumer;
 import java.nio.file.Path;
@@ -39,8 +38,8 @@
     boolean invalidDebugInfo = false;
     try {
       build(builder -> builder.addProgramFiles(out1), new ClassFileConsumer.ArchiveConsumer(out2));
-    } catch (CompilationError e) {
-      invalidDebugInfo = e.getCause() instanceof InvalidDebugInfoException;
+    } catch (CompilationFailedException e) {
+      invalidDebugInfo = e.getCause().getMessage().contains("Invalid debug info");
     }
     // TODO(b/77522100): Change to assertFalse when fixed.
     assertTrue(invalidDebugInfo);
diff --git a/src/test/java/com/android/tools/r8/checkdiscarded/CheckDiscardedTest.java b/src/test/java/com/android/tools/r8/checkdiscarded/CheckDiscardedTest.java
index 1938c46..a1042b8 100644
--- a/src/test/java/com/android/tools/r8/checkdiscarded/CheckDiscardedTest.java
+++ b/src/test/java/com/android/tools/r8/checkdiscarded/CheckDiscardedTest.java
@@ -3,13 +3,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.checkdiscarded;
 
+import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.checkdiscarded.testclasses.Main;
 import com.android.tools.r8.checkdiscarded.testclasses.UnusedClass;
 import com.android.tools.r8.checkdiscarded.testclasses.UsedClass;
 import com.android.tools.r8.checkdiscarded.testclasses.WillBeGone;
 import com.android.tools.r8.checkdiscarded.testclasses.WillStay;
-import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableList;
 import java.util.List;
@@ -28,7 +28,7 @@
         + checkDiscardRule(checkMembers, annotation);
     try {
       compileWithR8(classes, proguardConfig, this::noInlining);
-    } catch (CompilationError e) {
+    } catch (CompilationFailedException e) {
       Assert.assertTrue(shouldFail);
       return;
     }
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/InterfaceMethodDesugaringTests.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/InterfaceMethodDesugaringTests.java
index 2b73eaa..3674108 100644
--- a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/InterfaceMethodDesugaringTests.java
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/InterfaceMethodDesugaringTests.java
@@ -7,6 +7,7 @@
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.AsmTestBase;
+import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.VmTestRunner;
@@ -20,7 +21,6 @@
 import com.android.tools.r8.desugaring.interfacemethods.default2.TestMainDefault2;
 import com.android.tools.r8.desugaring.interfacemethods.static0.TestMainStatic0;
 import com.android.tools.r8.desugaring.interfacemethods.static1.TestMainStatic1;
-import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
@@ -139,7 +139,7 @@
         ToolHelper.getClassAsBytes(TestMainDefault0.class));
   }
 
-  @Test(expected = CompilationError.class)
+  @Test(expected = CompilationFailedException.class)
   @IgnoreForRangeOfVmVersions(from = Version.V7_0_0, to = Version.DEFAULT) // No desugaring
   public void testInvokeDefault1() throws Exception {
     ensureSameOutput(
diff --git a/src/test/java/com/android/tools/r8/graph/TargetLookupTest.java b/src/test/java/com/android/tools/r8/graph/TargetLookupTest.java
index 39b5ff8..7e0bfec 100644
--- a/src/test/java/com/android/tools/r8/graph/TargetLookupTest.java
+++ b/src/test/java/com/android/tools/r8/graph/TargetLookupTest.java
@@ -8,6 +8,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
+import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.ProcessResult;
@@ -175,7 +176,7 @@
   }
 
   @Test
-  public void lookupFieldWithDefaultInInterface() {
+  public void lookupFieldWithDefaultInInterface() throws CompilationFailedException {
     SmaliBuilder builder = new SmaliBuilder();
 
     builder.addInterface("Interface");
diff --git a/src/test/java/com/android/tools/r8/jasmin/MemberResolutionTest.java b/src/test/java/com/android/tools/r8/jasmin/MemberResolutionTest.java
index 49516d7..3d0aaff 100644
--- a/src/test/java/com/android/tools/r8/jasmin/MemberResolutionTest.java
+++ b/src/test/java/com/android/tools/r8/jasmin/MemberResolutionTest.java
@@ -5,12 +5,12 @@
 
 import static java.util.Collections.emptyList;
 
+import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.R8RunArtTestsTest.CompilerUnderTest;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.VmTestRunner;
 import com.android.tools.r8.VmTestRunner.IgnoreForRangeOfVmVersions;
-import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
 import com.android.tools.r8.jasmin.JasminBuilder.ClassFileVersion;
 import com.android.tools.r8.utils.ThrowingBiFunction;
@@ -572,7 +572,7 @@
             ProcessResult result = process.get();
             Assert.assertFalse(compiler != null && predicate.test(compiler));
             Assert.assertTrue(result.stderr.contains(name));
-          } catch (CompilationError e) {
+          } catch (CompilationFailedException e) {
             Assert.assertTrue(compiler == null || predicate.test(compiler));
           } catch (Exception e) {
             Assert.fail();
diff --git a/src/test/java/com/android/tools/r8/jasmin/NameTestBase.java b/src/test/java/com/android/tools/r8/jasmin/NameTestBase.java
index 1104e54..6589e06 100644
--- a/src/test/java/com/android/tools/r8/jasmin/NameTestBase.java
+++ b/src/test/java/com/android/tools/r8/jasmin/NameTestBase.java
@@ -7,8 +7,8 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.utils.StringUtils;
 import java.nio.file.InvalidPathException;
 import java.util.Arrays;
@@ -112,8 +112,8 @@
       try {
         runOnArtD8(jasminBuilder, mainClassName);
         fail("D8 should have rejected this case.");
-      } catch (CompilationError t) {
-        assertTrue(t.getMessage().contains(expectedNameInFailingD8Message));
+      } catch (CompilationFailedException t) {
+        assertTrue(t.getCause().getMessage().contains(expectedNameInFailingD8Message));
       }
 
       // Make sure ART also fail, if D8 rejects it.
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
index e608a20..4a76bde 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -185,7 +185,7 @@
     try {
       verifyMainDexContains(TWO_LARGE_CLASSES, getTwoLargeClassesAppPath(), false);
       fail("Expect to fail, for there are too many classes for the main-dex list.");
-    } catch (AbortException e) {
+    } catch (CompilationFailedException e) {
       assertEquals(1, errors.size());
       String message = errors.get(0).getDiagnosticMessage();
       // Make sure {@link MonoDexDistributor} was _not_ used.
@@ -229,7 +229,7 @@
     try {
       verifyMainDexContains(MANY_CLASSES, getManyClassesMultiDexAppPath(), false);
       fail("Expect to fail, for there are too many classes for the main-dex list.");
-    } catch (AbortException e) {
+    } catch (CompilationFailedException e) {
       assertEquals(1, errors.size());
       String message = errors.get(0).getDiagnosticMessage();
       // Make sure {@link MonoDexDistributor} was _not_ used.
diff --git a/src/test/java/com/android/tools/r8/naming/ApplyMappingTest.java b/src/test/java/com/android/tools/r8/naming/ApplyMappingTest.java
index 1e2e46e..7ce8a05 100644
--- a/src/test/java/com/android/tools/r8/naming/ApplyMappingTest.java
+++ b/src/test/java/com/android/tools/r8/naming/ApplyMappingTest.java
@@ -10,6 +10,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.StringConsumer;
@@ -17,7 +18,6 @@
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -287,8 +287,8 @@
     try {
       runR8(getCommandForApps(out, flag, MINIFICATION_JAR).build());
       fail("Expect to detect renaming conflict");
-    } catch (ProguardMapError e) {
-      assertTrue(e.getMessage().contains("functionFromIntToInt"));
+    } catch (CompilationFailedException e) {
+      assertTrue(e.getCause().getMessage().contains("functionFromIntToInt"));
     }
   }
 
@@ -310,8 +310,7 @@
         .addProguardConfigurationFiles(flag);
   }
 
-  private static AndroidApp runR8(R8Command command)
-      throws ProguardRuleParserException, ExecutionException, IOException {
+  private static AndroidApp runR8(R8Command command) throws CompilationFailedException {
     return ToolHelper.runR8(
         command,
         options -> {
diff --git a/src/test/java/com/android/tools/r8/naming/CovariantReturnTypeTest.java b/src/test/java/com/android/tools/r8/naming/CovariantReturnTypeTest.java
new file mode 100644
index 0000000..93f6215
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/CovariantReturnTypeTest.java
@@ -0,0 +1,113 @@
+// 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.naming;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isRenamed;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.StringConsumer.FileConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.jasmin.JasminBuilder;
+import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.io.File;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class CovariantReturnTypeTest extends TestBase {
+
+  @Ignore("b/115613850")
+  @Test
+  public void test() throws Exception {
+    for (int i = 0; i < 3; i++) {
+      JasminBuilder appBuilder = new JasminBuilder();
+      ClassBuilder classBuilder = appBuilder.addClass("package.TestClass");
+
+      classBuilder.addVirtualMethod(
+          "method1",
+          "Ljava/lang/Object;",
+          ".limit stack 1",
+          ".limit locals 1",
+          "aconst_null",
+          "areturn");
+      classBuilder.addVirtualMethod(
+          "method1", "Lpackage/A;", ".limit stack 1", ".limit locals 1", "aconst_null", "areturn");
+      classBuilder.addVirtualMethod(
+          "method1", "Lpackage/B;", ".limit stack 1", ".limit locals 1", "aconst_null", "areturn");
+
+      classBuilder.addVirtualMethod(
+          "method2", "Lpackage/A;", ".limit stack 1", ".limit locals 1", "aconst_null", "areturn");
+      classBuilder.addVirtualMethod(
+          "method2",
+          "Ljava/lang/Object;",
+          ".limit stack 1",
+          ".limit locals 1",
+          "aconst_null",
+          "areturn");
+      classBuilder.addVirtualMethod(
+          "method2", "Lpackage/B;", ".limit stack 1", ".limit locals 1", "aconst_null", "areturn");
+
+      classBuilder.addVirtualMethod(
+          "method3", "Lpackage/A;", ".limit stack 1", ".limit locals 1", "aconst_null", "areturn");
+      classBuilder.addVirtualMethod(
+          "method3", "Lpackage/B;", ".limit stack 1", ".limit locals 1", "aconst_null", "areturn");
+      classBuilder.addVirtualMethod(
+          "method3",
+          "Ljava/lang/Object;",
+          ".limit stack 1",
+          ".limit locals 1",
+          "aconst_null",
+          "areturn");
+
+      appBuilder.addInterface("package.A");
+      appBuilder.addInterface("package.B");
+
+      Path proguardMapPath = File.createTempFile("mapping", ".txt", temp.getRoot()).toPath();
+      AndroidApp output =
+          compileWithR8(
+              appBuilder.build(),
+              keepMainProguardConfiguration("package.TestClass"),
+              options -> {
+                options.enableTreeShaking = false;
+                options.proguardMapConsumer = new FileConsumer(proguardMapPath);
+              });
+
+      CodeInspector inspector = new CodeInspector(output, proguardMapPath);
+      ClassSubject clazz = inspector.clazz("package.TestClass");
+      assertThat(clazz, isPresent());
+
+      Map<String, Set<MethodSubject>> methodSubjectsByName = new HashMap<>();
+      for (String name : ImmutableList.of("method1", "method2", "method3")) {
+        methodSubjectsByName.put(
+            name,
+            ImmutableSet.of(
+                clazz.method("java.lang.Object", name, ImmutableList.of()),
+                clazz.method("package.A", name, ImmutableList.of()),
+                clazz.method("package.B", name, ImmutableList.of())));
+      }
+
+      Set<String> minifiedMethodNames = new HashSet<>();
+      for (Set<MethodSubject> methodSubjects : methodSubjectsByName.values()) {
+        for (MethodSubject methodSubject : methodSubjects) {
+          assertThat(methodSubject, isRenamed());
+          minifiedMethodNames.add(methodSubject.getFinalName());
+        }
+      }
+      assertEquals(3, minifiedMethodNames.size());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/b114554345/B114554345.java b/src/test/java/com/android/tools/r8/naming/b114554345/B114554345.java
new file mode 100644
index 0000000..edf77b0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/b114554345/B114554345.java
@@ -0,0 +1,136 @@
+// 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.naming.b114554345;
+
+import static junit.framework.TestCase.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.AndroidApp;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class B114554345 extends TestBase {
+
+  private final Backend backend;
+
+  @Parameters(name = "mode:{0}")
+  public static Collection<Object[]> data() {
+    List<Object[]> parameters = new ArrayList<>();
+    for (Backend backend : Backend.values()) {
+      parameters.add(new Object[] {backend});
+    }
+    return parameters;
+  }
+
+  public B114554345(Backend backend) {
+    this.backend = backend;
+  }
+
+  @Ignore("b/114554345")
+  @Test
+  public void test() throws Exception {
+    AndroidApp input =
+        AndroidApp.builder()
+            .addProgramFiles(
+                ToolHelper.getClassFilesForTestDirectory(
+                    ToolHelper.getPackageDirectoryForTestPackage(this.getClass().getPackage()),
+                    path -> !path.toString().contains("B114554345")))
+            .build();
+    AndroidApp output =
+        compileWithR8(
+            input,
+            keepMainProguardConfiguration(TestDriver.class),
+            options -> options.enableInlining = false,
+            backend);
+    String mainClass = TestDriver.class.getName();
+    assertEquals(runOnJava(TestDriver.class), runOnVM(output, mainClass, backend));
+  }
+}
+
+// Interface and two implementations.
+
+interface Interface {
+  Interface method();
+}
+
+class InterfaceImpl implements Interface {
+
+  @Override
+  public InterfaceImpl method() {
+    System.out.println("In InterfaceImpl.method()");
+    return this;
+  }
+}
+
+class OtherInterfaceImpl extends InterfaceImpl {
+
+  @Override
+  public OtherInterfaceImpl method() {
+    System.out.println("In OtherInterfaceImpl.method()");
+    return this;
+  }
+}
+
+// Sub-interface and three implementations.
+
+interface SubInterface extends Interface {
+  SubInterface method();
+}
+
+class SubInterfaceImpl implements SubInterface {
+
+  @Override
+  public SubInterfaceImpl method() {
+    System.out.println("In SubInterfaceImpl.method()");
+    return this;
+  }
+}
+
+class OtherSubInterfaceImpl implements SubInterface {
+
+  @Override
+  public OtherSubInterfaceImpl method() {
+    System.out.println("In OtherSubInterfaceImpl.method()");
+    return this;
+  }
+}
+
+class YetAnotherSubInterfaceImpl extends InterfaceImpl implements SubInterface {
+
+  @Override
+  public YetAnotherSubInterfaceImpl method() {
+    System.out.println("In YetAnotherSubInterfaceImpl.method()");
+    return this;
+  }
+}
+
+class TestDriver {
+
+  public static void main(String[] args) {
+    foo(new InterfaceImpl());
+    foo(new OtherInterfaceImpl());
+    foo(new SubInterfaceImpl());
+    foo(new YetAnotherSubInterfaceImpl());
+    bar(new SubInterfaceImpl());
+    bar(new OtherSubInterfaceImpl());
+    bar(new YetAnotherSubInterfaceImpl());
+  }
+
+  private static void foo(Interface obj) {
+    obj.method();
+  }
+
+  private static void bar(SubInterface obj) {
+    obj.method();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/switches/SwitchRewritingTest.java b/src/test/java/com/android/tools/r8/rewrite/switches/SwitchRewritingTest.java
index 2073ab9..354f3c2 100644
--- a/src/test/java/com/android/tools/r8/rewrite/switches/SwitchRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/switches/SwitchRewritingTest.java
@@ -7,6 +7,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.code.Const;
 import com.android.tools.r8.code.Const4;
 import com.android.tools.r8.code.ConstHigh16;
@@ -39,7 +40,8 @@
         || instruction instanceof ConstHigh16
         || instruction instanceof Const;
   }
-  private void runSingleCaseDexTest(boolean packed, int key) {
+
+  private void runSingleCaseDexTest(boolean packed, int key) throws CompilationFailedException {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
     String switchInstruction;
     String switchData;
@@ -100,7 +102,7 @@
   }
 
   @Test
-  public void singleCaseDex() {
+  public void singleCaseDex() throws CompilationFailedException {
     for (boolean packed : new boolean[]{true, false}) {
       runSingleCaseDexTest(packed, Integer.MIN_VALUE);
       runSingleCaseDexTest(packed, -1);
@@ -110,7 +112,8 @@
     }
   }
 
-  private void runTwoCaseSparseToPackedOrIfsDexTest(int key1, int key2) {
+  private void runTwoCaseSparseToPackedOrIfsDexTest(int key1, int key2)
+      throws CompilationFailedException {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     MethodSignature signature = builder.addStaticMethod(
@@ -162,7 +165,7 @@
   }
 
   @Test
-  public void twoCaseSparseToPackedOrIfsDex() {
+  public void twoCaseSparseToPackedOrIfsDex() throws CompilationFailedException {
     for (int delta = 1; delta <= 3; delta++) {
       runTwoCaseSparseToPackedOrIfsDexTest(0, delta);
       runTwoCaseSparseToPackedOrIfsDexTest(-delta, 0);
diff --git a/src/test/java/com/android/tools/r8/shaking/LibraryExtendsProgramTest.java b/src/test/java/com/android/tools/r8/shaking/LibraryExtendsProgramTest.java
index 09faa8b..834a486 100644
--- a/src/test/java/com/android/tools/r8/shaking/LibraryExtendsProgramTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/LibraryExtendsProgramTest.java
@@ -3,40 +3,29 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.shaking;
 
+import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.shaking.libraryextendsprogram.Interface;
 import com.android.tools.r8.shaking.libraryextendsprogram.Main;
 import com.android.tools.r8.shaking.libraryextendsprogram.SubClass;
 import com.android.tools.r8.shaking.libraryextendsprogram.SuperClass;
-import com.android.tools.r8.utils.AbortException;
 import com.android.tools.r8.utils.AndroidApp;
 import com.google.common.collect.ImmutableList;
-import org.junit.Assert;
 import org.junit.Test;
 
 public class LibraryExtendsProgramTest extends TestBase {
 
-  @Test
+  @Test(expected = CompilationFailedException.class)
   public void libraryClassExtendsProgramClass() throws Exception {
     AndroidApp theApp = readClasses(ImmutableList.of(Main.class, SuperClass.class),
         ImmutableList.of(SubClass.class, Interface.class));
-    try {
-      compileWithR8(theApp, o -> o.ignoreMissingClasses = true);
-    } catch (AbortException e) {
-      return;
-    }
-    Assert.fail();
+    compileWithR8(theApp, o -> o.ignoreMissingClasses = true);
   }
 
-  @Test
+  @Test(expected = CompilationFailedException.class)
   public void libraryClassImplementsProgramInterface() throws Exception {
     AndroidApp theApp = readClasses(ImmutableList.of(Main.class, Interface.class),
         ImmutableList.of(SubClass.class, SuperClass.class));
-    try {
-      compileWithR8(theApp, o -> o.ignoreMissingClasses = true);
-    } catch (AbortException e) {
-      return;
-    }
-    Assert.fail();
+    compileWithR8(theApp, o -> o.ignoreMissingClasses = true);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java
index ff762b7..30de112 100644
--- a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java
@@ -9,10 +9,10 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.CompatProguardCommandBuilder;
+import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.invokesuper.Consumer;
@@ -588,7 +588,7 @@
     builder.setProgramConsumer(emptyConsumer(backend)).addLibraryFiles(runtimeJar(backend));
     try {
       app = ToolHelper.runR8(builder.build(), o -> o.enableClassInlining = false);
-    } catch (CompilationError e) {
+    } catch (CompilationFailedException e) {
       assertTrue(!forceProguardCompatibility && (!innerClasses || !enclosingMethod));
       return;
     }
diff --git a/src/test/java/com/android/tools/r8/smali/ComputeBlockTryRangeTest.java b/src/test/java/com/android/tools/r8/smali/ComputeBlockTryRangeTest.java
index de8b832..9cfd7fa 100644
--- a/src/test/java/com/android/tools/r8/smali/ComputeBlockTryRangeTest.java
+++ b/src/test/java/com/android/tools/r8/smali/ComputeBlockTryRangeTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.smali;
 
+import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.smali.SmaliBuilder.MethodSignature;
 import com.android.tools.r8.utils.AndroidApp;
@@ -13,7 +14,7 @@
 public class ComputeBlockTryRangeTest extends SmaliTestBase {
 
   @Test
-  public void jumpIntoTryRange() {
+  public void jumpIntoTryRange() throws CompilationFailedException {
 
     SmaliBuilder builder = new SmaliBuilder("Test");
 
diff --git a/src/test/java/com/android/tools/r8/smali/SmaliBuildTest.java b/src/test/java/com/android/tools/r8/smali/SmaliBuildTest.java
index 8d780b4..912915b 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliBuildTest.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliBuildTest.java
@@ -6,6 +6,7 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.origin.EmbeddedOrigin;
 import com.android.tools.r8.utils.AndroidApp;
@@ -28,7 +29,7 @@
   }
 
   @Test
-  public void buildWithoutLibrary() {
+  public void buildWithoutLibrary() throws CompilationFailedException {
     // Build simple "Hello, world!" application.
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
     builder.addMainMethod(
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 b12c0b9..0aa8cae 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
@@ -69,17 +69,15 @@
     }
   }
 
-  protected AndroidApp processApplication(AndroidApp application) {
+  protected AndroidApp processApplication(AndroidApp application)
+      throws CompilationFailedException {
     return processApplication(application, null);
   }
 
-  protected AndroidApp processApplication(AndroidApp application,
-      Consumer<InternalOptions> optionsConsumer) {
-    try {
-      return ToolHelper.runR8(application, optionsConsumer);
-    } catch (IOException e) {
-      throw new RuntimeException(e);
-    }
+  protected AndroidApp processApplication(
+      AndroidApp application, Consumer<InternalOptions> optionsConsumer)
+      throws CompilationFailedException {
+    return ToolHelper.runR8(application, optionsConsumer);
   }
 
   protected Path runR8(SmaliBuilder builder, List<String> proguardConfigurations) {
@@ -224,7 +222,12 @@
         returnType, parameters, locals, instructions);
 
     // Process the application with R8.
-    AndroidApp processdApplication = processApplication(application);
+    AndroidApp processdApplication = null;
+    try {
+      processdApplication = processApplication(application);
+    } catch (CompilationFailedException e) {
+      throw new RuntimeException(e);
+    }
     assertEquals(1, getNumberOfProgramClasses(processdApplication));
 
     // Return the processed method for inspection.
diff --git a/tools/compare_apk_sizes.py b/tools/compare_apk_sizes.py
new file mode 100755
index 0000000..e477320
--- /dev/null
+++ b/tools/compare_apk_sizes.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python
+# 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.
+
+# Script for checking impact of a change by comparing the sizes of generated
+# classes in an apk.
+
+import glob
+import optparse
+import os
+import shutil
+import sys
+import toolhelper
+import utils
+import zipfile
+import StringIO
+
+USAGE = """%prog [options] app1 app2
+  NOTE: This only makes sense if minification is disabled"""
+
+def parse_options():
+  result = optparse.OptionParser(usage=USAGE)
+  result.add_option('--temp',
+                    help='Temporary directory to store extracted classes in')
+  result.add_option('--report',
+                    help='Print comparison to this location instead of stdout')
+  return result.parse_args()
+
+def extract_apk(apk, output):
+  if os.path.exists(output):
+    shutil.rmtree(output)
+  zipfile.ZipFile(apk).extractall(output)
+  with utils.ChangedWorkingDirectory(output):
+    dex = glob.glob('*.dex')
+    return [os.path.join(output, dexfile) for dexfile in dex]
+
+def ensure_exists(files):
+  for f in files:
+    if not os.path.exists(f):
+      raise Exception('%s does not exist')
+
+def extract_classes(input, output):
+  if os.path.exists(output):
+    shutil.rmtree(output)
+  os.makedirs(output)
+  args = ['--file-per-class',
+          '--output', output]
+  args.extend(input)
+  if toolhelper.run('d8', args) is not 0:
+    raise Exception('Failed running d8')
+
+class FileInfo:
+  def __init__(self, path, root):
+    self.path = path
+    self.full_path = os.path.join(root, path)
+    self.size = os.path.getsize(self.full_path)
+
+def generate_file_info(path):
+  file_info_map = {}
+  with utils.ChangedWorkingDirectory(path):
+    for root, dirs, files in os.walk('.'):
+      for f in files:
+        assert f.endswith('dex')
+        file_path = os.path.join(root, f)
+        entry = FileInfo(file_path, path)
+        file_info_map[file_path] = entry
+  return file_info_map
+
+def print_info(app, app_files, only_in_app, bigger_in_app, output):
+  output.write('Only in %s\n' % app)
+  only_app_sorted = sorted(only_in_app,
+                           key=lambda a: app_files[a].size,
+                           reverse=True)
+  output.write('\n'.join(['  %s %s bytes' %
+                          (x, app_files[x].size) for x in only_app_sorted]))
+  output.write('\n\n')
+  output.write('Bigger in %s\n' % app)
+  # Sort by the percentage diff compared to size
+  percent = lambda a: (0.0 + bigger_in_app.get(a))/app_files.get(a).size * 100
+  for bigger in sorted(bigger_in_app, key=percent, reverse=True):
+    output.write('  {0:.3f}% {1} bytes {2}\n'.format(percent(bigger),
+                                                     bigger_in_app[bigger],
+                                                     bigger))
+  output.write('\n\n')
+
+
+def compare(app1_classes_dir, app2_classes_dir, app1, app2, report):
+  app1_files = generate_file_info(app1_classes_dir)
+  app2_files = generate_file_info(app2_classes_dir)
+  only_in_app1 = [k for k in app1_files if k not in app2_files]
+  only_in_app2 = [k for k in app2_files if k not in app1_files]
+  in_both = [k for k in app2_files if k in app1_files]
+  assert len(app1_files) == len(only_in_app1) + len(in_both)
+  assert len(app2_files) == len(only_in_app2) + len(in_both)
+  bigger_in_app1 = {}
+  bigger_in_app2 = {}
+  same_size = []
+  for f in in_both:
+    app1_entry = app1_files[f]
+    app2_entry = app2_files[f]
+    if app1_entry.size > app2_entry.size:
+      bigger_in_app1[f] = app1_entry.size - app2_entry.size
+    elif app2_entry.size > app1_entry.size:
+      bigger_in_app2[f] = app2_entry.size - app1_entry.size
+    else:
+      same_size.append(f)
+  output = open(report, 'w') if report else sys.stdout
+  print_info(app1, app1_files, only_in_app1, bigger_in_app1, output)
+  print_info(app2, app2_files, only_in_app2, bigger_in_app2, output)
+  output.write('Same size\n')
+  output.write('\n'.join(['  %s' % x for x in same_size]))
+  if report:
+    output.close()
+
+def Main():
+  (options, args) = parse_options()
+  if len(args) is not 2:
+    print args
+    print('Takes exactly two arguments, the two apps to compare')
+    return 1
+  app1 = args[0]
+  app2 = args[1]
+  ensure_exists([app1, app2])
+  with utils.TempDir() as temporary:
+    # If a temp dir is passed in, use that instead of the generated temporary
+    output = options.temp if options.temp else temporary
+    ensure_exists([output])
+    app1_input = [app1]
+    app2_input = [app2]
+    if app1.endswith('apk'):
+      app1_input = extract_apk(app1, os.path.join(output, 'app1'))
+    if app2.endswith('apk'):
+      app2_input = extract_apk(app2, os.path.join(output, 'app2'))
+    app1_classes_dir = os.path.join(output, 'app1_classes')
+    app2_classes_dir = os.path.join(output, 'app2_classes')
+
+    extract_classes(app1_input, app1_classes_dir)
+    extract_classes(app2_input, app2_classes_dir)
+    compare(app1_classes_dir, app2_classes_dir, app1, app2, options.report)
+
+if __name__ == '__main__':
+  sys.exit(Main())