Better error handling for GenerateMainDexList

Unwrap ExecutionException, and report all errors on the provided
DiagnosticsHandler same way as the D8, R8 and DexSplitter.

Bug: 112679440
Change-Id: I6b803f0e6585c1461eeacfcac1845b363508ef69
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 475d34f..76e7277 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -201,8 +201,7 @@
           .write(executor);
       options.printWarnings();
     } catch (ExecutionException e) {
-      R8.unwrapExecutionException(e);
-      throw new AssertionError(e); // unwrapping method should have thrown
+      throw R8.unwrapExecutionException(e);
     } finally {
       options.signalFinishedToConsumers();
       // Dump timings.
diff --git a/src/main/java/com/android/tools/r8/DexFileMergerHelper.java b/src/main/java/com/android/tools/r8/DexFileMergerHelper.java
index a26924e..d7183c8 100644
--- a/src/main/java/com/android/tools/r8/DexFileMergerHelper.java
+++ b/src/main/java/com/android/tools/r8/DexFileMergerHelper.java
@@ -105,8 +105,7 @@
         writer.write(executor);
         options.printWarnings();
       } catch (ExecutionException e) {
-        R8.unwrapExecutionException(e);
-        throw new AssertionError(e); // unwrapping method should have thrown
+        throw R8.unwrapExecutionException(e);
       } finally {
         options.signalFinishedToConsumers();
       }
diff --git a/src/main/java/com/android/tools/r8/DexSplitterHelper.java b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
index 68dc702..163c46f 100644
--- a/src/main/java/com/android/tools/r8/DexSplitterHelper.java
+++ b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
@@ -109,8 +109,7 @@
         }
       }
     } catch (ExecutionException e) {
-      R8.unwrapExecutionException(e);
-      throw new AssertionError(e); // unwrapping method should have thrown
+      throw R8.unwrapExecutionException(e);
     } catch (FeatureMappingException e) {
       options.reporter.error(e.getMessage());
     } finally {
diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexList.java b/src/main/java/com/android/tools/r8/GenerateMainDexList.java
index fe946da..43989ff 100644
--- a/src/main/java/com/android/tools/r8/GenerateMainDexList.java
+++ b/src/main/java/com/android/tools/r8/GenerateMainDexList.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
 import com.android.tools.r8.shaking.TreePruner;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
@@ -36,37 +37,41 @@
   }
 
   private List<String> run(AndroidApp app, ExecutorService executor)
-      throws IOException, ExecutionException {
-    DexApplication application =
-        new ApplicationReader(app, options, timing).read(executor).toDirect();
-    AppInfoWithSubtyping appInfo = new AppInfoWithSubtyping(application);
-    RootSet mainDexRootSet =
-        new RootSetBuilder(appInfo, application, options.mainDexKeepRules, options).run(executor);
-    Enqueuer enqueuer = new Enqueuer(appInfo, GraphLense.getIdentityLense(), options, true);
-    AppInfoWithLiveness mainDexAppInfo = enqueuer.traceMainDex(mainDexRootSet, executor, timing);
-    // LiveTypes is the result.
-    Set<DexType> mainDexClasses =
-        new MainDexListBuilder(new HashSet<>(mainDexAppInfo.liveTypes), application).run();
+      throws IOException {
+    try {
+      DexApplication application =
+          new ApplicationReader(app, options, timing).read(executor).toDirect();
+      AppInfoWithSubtyping appInfo = new AppInfoWithSubtyping(application);
+      RootSet mainDexRootSet =
+          new RootSetBuilder(appInfo, application, options.mainDexKeepRules, options).run(executor);
+      Enqueuer enqueuer = new Enqueuer(appInfo, GraphLense.getIdentityLense(), options, true);
+      AppInfoWithLiveness mainDexAppInfo = enqueuer.traceMainDex(mainDexRootSet, executor, timing);
+      // LiveTypes is the result.
+      Set<DexType> mainDexClasses =
+          new MainDexListBuilder(new HashSet<>(mainDexAppInfo.liveTypes), application).run();
 
-    List<String> result = mainDexClasses.stream()
-        .map(c -> c.toSourceString().replace('.', '/') + ".class")
-        .sorted()
-        .collect(Collectors.toList());
+      List<String> result = mainDexClasses.stream()
+          .map(c -> c.toSourceString().replace('.', '/') + ".class")
+          .sorted()
+          .collect(Collectors.toList());
 
-    if (options.mainDexListConsumer != null) {
-      options.mainDexListConsumer.accept(String.join("\n", result), options.reporter);
+      if (options.mainDexListConsumer != null) {
+        options.mainDexListConsumer.accept(String.join("\n", result), options.reporter);
+      }
+
+      // Print -whyareyoukeeping results if any.
+      if (mainDexRootSet.reasonAsked.size() > 0) {
+        // Print reasons on the application after pruning, so that we reflect the actual result.
+        TreePruner pruner = new TreePruner(application, mainDexAppInfo.withLiveness(), options);
+        application = pruner.run();
+        ReasonPrinter reasonPrinter = enqueuer.getReasonPrinter(mainDexRootSet.reasonAsked);
+        reasonPrinter.run(application);
+      }
+
+      return result;
+    } catch (ExecutionException e) {
+      throw R8.unwrapExecutionException(e);
     }
-
-    // Print -whyareyoukeeping results if any.
-    if (mainDexRootSet.reasonAsked.size() > 0) {
-      // Print reasons on the application after pruning, so that we reflect the actual result.
-      TreePruner pruner = new TreePruner(application, mainDexAppInfo.withLiveness(), options);
-      application = pruner.run();
-      ReasonPrinter reasonPrinter = enqueuer.getReasonPrinter(mainDexRootSet.reasonAsked);
-      reasonPrinter.run(application);
-    }
-
-    return result;
   }
 
   /**
@@ -82,7 +87,7 @@
    * @return classes to keep in the primary dex file.
    */
   public static List<String> run(GenerateMainDexListCommand command)
-      throws IOException, ExecutionException {
+      throws CompilationFailedException {
     ExecutorService executorService = ThreadUtils.getExecutorService(command.getInternalOptions());
     try {
       return run(command, executorService);
@@ -105,14 +110,28 @@
    * @return classes to keep in the primary dex file.
    */
   public static List<String> run(GenerateMainDexListCommand command, ExecutorService executor)
-      throws IOException, ExecutionException {
+      throws CompilationFailedException {
     AndroidApp app = command.getInputApp();
     InternalOptions options = command.getInternalOptions();
-    return new GenerateMainDexList(options).run(app, executor);
+    ResultBox result = new ResultBox();
+
+    ExceptionUtils.withMainDexListHandler(
+        command.getReporter(),
+        () -> {
+          try {
+            result.content = new GenerateMainDexList(options).run(app, executor);
+          } finally {
+            executor.shutdown();
+          }
+        });
+    return result.content;
   }
 
-  public static void main(String[] args)
-      throws IOException, ExecutionException, CompilationFailedException {
+  private static class ResultBox {
+    List<String> content;
+  }
+
+  public static void main(String[] args) throws CompilationFailedException {
     GenerateMainDexListCommand.Builder builder = GenerateMainDexListCommand.parse(args);
     GenerateMainDexListCommand command = builder.build();
     if (command.isPrintHelp()) {
diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java b/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
index f9e0ebf..2d06da8 100644
--- a/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
+++ b/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
@@ -150,6 +150,10 @@
     return mainDexListConsumer;
   }
 
+  Reporter getReporter() {
+    return reporter;
+  }
+
   private static void parse(String[] args, GenerateMainDexListCommand.Builder builder) {
     for (int i = 0; i < args.length; i++) {
       String arg = args[i].trim();
diff --git a/src/main/java/com/android/tools/r8/PrintSeeds.java b/src/main/java/com/android/tools/r8/PrintSeeds.java
index 4410d8a..78cb75a 100644
--- a/src/main/java/com/android/tools/r8/PrintSeeds.java
+++ b/src/main/java/com/android/tools/r8/PrintSeeds.java
@@ -92,8 +92,7 @@
           System.out,
           type -> descriptors.contains(type.toDescriptorString()));
     } catch (ExecutionException e) {
-      R8.unwrapExecutionException(e);
-      throw new AssertionError(e); // unwrapping method should have thrown
+      throw R8.unwrapExecutionException(e);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 9738c8d..1bfe039 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -525,8 +525,7 @@
 
       options.printWarnings();
     } catch (ExecutionException e) {
-      unwrapExecutionException(e);
-      throw new AssertionError(e); // unwrapping method should have thrown
+      throw unwrapExecutionException(e);
     } finally {
       options.signalFinishedToConsumers();
       // Dump timings.
@@ -545,7 +544,7 @@
     }
   }
 
-  static void unwrapExecutionException(ExecutionException executionException) {
+  static RuntimeException unwrapExecutionException(ExecutionException executionException) {
     Throwable cause = executionException.getCause();
     if (cause instanceof CompilationError) {
       // add original exception as suppressed exception to provide the original stack trace
diff --git a/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java b/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java
index fe8adfa..1ff2664 100644
--- a/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java
@@ -48,6 +48,11 @@
     withCompilationHandler(reporter, action);
   }
 
+  public static void withMainDexListHandler(
+      Reporter reporter, CompileAction action) throws CompilationFailedException {
+    withCompilationHandler(reporter, action);
+  }
+
   public static void withCompilationHandler(Reporter reporter, CompileAction action)
       throws CompilationFailedException {
     try {
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
index 374d58f..ddb0e7d 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
@@ -27,7 +27,6 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
-import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import org.junit.Assert;
@@ -218,92 +217,89 @@
     Path out = temp.getRoot().toPath().resolve(testName + ZIP_EXTENSION);
 
     Path inputJar = Paths.get(buildDir, packageName + JAR_EXTENSION);
-    try {
-      // Build main-dex list using GenerateMainDexList and test the output from run.
-      GenerateMainDexListCommand.Builder mdlCommandBuilder = GenerateMainDexListCommand.builder();
-      GenerateMainDexListCommand mdlCommand = mdlCommandBuilder
-          .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.K))
-          .addProgramFiles(inputJar)
-          .addProgramFiles(Paths.get(EXAMPLE_BUILD_DIR, "multidexfakeframeworks" + JAR_EXTENSION))
-          .addMainDexRulesFiles(mainDexRules)
-          .build();
-      List<String> mainDexGeneratorMainDexList =
-          GenerateMainDexList.run(mdlCommand).stream()
-              .map(this::mainDexStringToDescriptor)
-              .sorted()
-              .collect(Collectors.toList());
+    // Build main-dex list using GenerateMainDexList and test the output from run.
+    GenerateMainDexListCommand.Builder mdlCommandBuilder = GenerateMainDexListCommand.builder();
+    GenerateMainDexListCommand mdlCommand = mdlCommandBuilder
+        .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.K))
+        .addProgramFiles(inputJar)
+        .addProgramFiles(Paths.get(EXAMPLE_BUILD_DIR, "multidexfakeframeworks" + JAR_EXTENSION))
+        .addMainDexRulesFiles(mainDexRules)
+        .build();
+    List<String> mainDexGeneratorMainDexList =
+        GenerateMainDexList.run(mdlCommand).stream()
+            .map(this::mainDexStringToDescriptor)
+            .sorted()
+            .collect(Collectors.toList());
 
-      class Box {
-        String content;
+    class Box {
+
+      String content;
+    }
+
+    // Build main-dex list using GenerateMainDexList and test the output from a consumer.
+    final Box mainDexListOutput = new Box();
+    mdlCommandBuilder = GenerateMainDexListCommand.builder();
+    mdlCommand = mdlCommandBuilder
+        .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.K))
+        .addProgramFiles(inputJar)
+        .addProgramFiles(Paths.get(EXAMPLE_BUILD_DIR, "multidexfakeframeworks" + JAR_EXTENSION))
+        .addMainDexRulesFiles(mainDexRules)
+        .setMainDexListConsumer((string, handler) -> mainDexListOutput.content = string)
+        .build();
+    GenerateMainDexList.run(mdlCommand);
+    List<String> mainDexGeneratorMainDexListFromConsumer =
+        StringUtils.splitLines(mainDexListOutput.content).stream()
+            .map(this::mainDexStringToDescriptor)
+            .sorted()
+            .collect(Collectors.toList());
+
+    // Build main-dex list using R8.
+    final Box r8MainDexListOutput = new Box();
+    R8Command.Builder r8CommandBuilder = R8Command.builder();
+    R8Command command =
+        r8CommandBuilder
+            .setMinApiLevel(minSdk.getLevel())
+            .addProgramFiles(inputJar)
+            .addProgramFiles(
+                Paths.get(EXAMPLE_BUILD_DIR, "multidexfakeframeworks" + JAR_EXTENSION))
+            .addLibraryFiles(ToolHelper.getAndroidJar(minSdk))
+            .setOutput(out, OutputMode.DexIndexed)
+            .addMainDexRulesFiles(mainDexRules)
+            .setMainDexListConsumer((string, handler) -> r8MainDexListOutput.content = string)
+            .build();
+    ToolHelper.runR8WithFullResult(command, optionsConsumer);
+    List<String> r8MainDexList =
+        StringUtils.splitLines(r8MainDexListOutput.content).stream()
+            .map(this::mainDexStringToDescriptor)
+            .sorted()
+            .collect(Collectors.toList());
+    // Check that generated lists are the same as the reference list, except for lambda
+    // classes which are only produced when running R8.
+    String[] r8RefList = new String(Files.readAllBytes(
+        expectedR8MainDexList), StandardCharsets.UTF_8).split("\n");
+    for (int i = 0; i < r8RefList.length; i++) {
+      String reference = r8RefList[i].trim();
+      if (r8MainDexList.size() <= i) {
+        Assert.fail("R8 main dex list is missing '" + reference + "'");
       }
-
-      // Build main-dex list using GenerateMainDexList and test the output from a consumer.
-      final Box mainDexListOutput = new Box();
-      mdlCommandBuilder = GenerateMainDexListCommand.builder();
-      mdlCommand = mdlCommandBuilder
-          .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.K))
-          .addProgramFiles(inputJar)
-          .addProgramFiles(Paths.get(EXAMPLE_BUILD_DIR, "multidexfakeframeworks" + JAR_EXTENSION))
-          .addMainDexRulesFiles(mainDexRules)
-          .setMainDexListConsumer((string, handler) -> mainDexListOutput.content = string)
-          .build();
-      GenerateMainDexList.run(mdlCommand);
-      List<String> mainDexGeneratorMainDexListFromConsumer =
-          StringUtils.splitLines(mainDexListOutput.content).stream()
-              .map(this::mainDexStringToDescriptor)
-              .sorted()
-              .collect(Collectors.toList());
-
-      // Build main-dex list using R8.
-      final Box r8MainDexListOutput = new Box();
-      R8Command.Builder r8CommandBuilder = R8Command.builder();
-      R8Command command =
-          r8CommandBuilder
-              .setMinApiLevel(minSdk.getLevel())
-              .addProgramFiles(inputJar)
-              .addProgramFiles(
-                  Paths.get(EXAMPLE_BUILD_DIR, "multidexfakeframeworks" + JAR_EXTENSION))
-              .addLibraryFiles(ToolHelper.getAndroidJar(minSdk))
-              .setOutput(out, OutputMode.DexIndexed)
-              .addMainDexRulesFiles(mainDexRules)
-              .setMainDexListConsumer((string, handler) -> r8MainDexListOutput.content = string)
-              .build();
-      ToolHelper.runR8WithFullResult(command, optionsConsumer);
-      List<String> r8MainDexList =
-          StringUtils.splitLines(r8MainDexListOutput.content).stream()
-              .map(this::mainDexStringToDescriptor)
-              .sorted()
-              .collect(Collectors.toList());
-      // Check that generated lists are the same as the reference list, except for lambda
-      // classes which are only produced when running R8.
-      String[] r8RefList = new String(Files.readAllBytes(
-          expectedR8MainDexList), StandardCharsets.UTF_8).split("\n");
-      for (int i = 0; i < r8RefList.length; i++) {
-        String reference = r8RefList[i].trim();
-        if (r8MainDexList.size() <= i) {
-          Assert.fail("R8 main dex list is missing '" + reference + "'");
+      checkSameMainDexEntry(reference, r8MainDexList.get(i));
+    }
+    String[] refList = new String(Files.readAllBytes(
+        expectedMainDexList), StandardCharsets.UTF_8).split("\n");
+    int nonLambdaOffset = 0;
+    for (int i = 0; i < refList.length; i++) {
+      String reference = refList[i].trim();
+      // The main dex list generator does not do any lambda desugaring.
+      if (!isLambda(reference)) {
+        if (mainDexGeneratorMainDexList.size() <= i - nonLambdaOffset) {
+          Assert.fail("Main dex list generator is missing '" + reference + "'");
         }
-        checkSameMainDexEntry(reference, r8MainDexList.get(i));
+        checkSameMainDexEntry(reference, mainDexGeneratorMainDexList.get(i - nonLambdaOffset));
+        checkSameMainDexEntry(
+            reference, mainDexGeneratorMainDexListFromConsumer.get(i - nonLambdaOffset));
+      } else {
+        nonLambdaOffset++;
       }
-      String[] refList = new String(Files.readAllBytes(
-          expectedMainDexList), StandardCharsets.UTF_8).split("\n");
-      int nonLambdaOffset = 0;
-      for (int i = 0; i < refList.length; i++) {
-        String reference = refList[i].trim();
-        // The main dex list generator does not do any lambda desugaring.
-        if (!isLambda(reference)) {
-          if (mainDexGeneratorMainDexList.size() <= i - nonLambdaOffset) {
-            Assert.fail("Main dex list generator is missing '" + reference + "'");
-          }
-          checkSameMainDexEntry(reference, mainDexGeneratorMainDexList.get(i - nonLambdaOffset));
-          checkSameMainDexEntry(
-              reference, mainDexGeneratorMainDexListFromConsumer.get(i - nonLambdaOffset));
-        } else {
-          nonLambdaOffset++;
-        }
-      }
-    } catch (ExecutionException e) {
-      throw e.getCause();
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/utils/GenerateMainDexListCommandTest.java b/src/test/java/com/android/tools/r8/utils/GenerateMainDexListCommandTest.java
index fb4fe70..c80b72e 100644
--- a/src/test/java/com/android/tools/r8/utils/GenerateMainDexListCommandTest.java
+++ b/src/test/java/com/android/tools/r8/utils/GenerateMainDexListCommandTest.java
@@ -3,14 +3,20 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import static com.android.tools.r8.ToolHelper.EXAMPLES_BUILD_DIR;
 import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DiagnosticsChecker;
+import com.android.tools.r8.GenerateMainDexList;
 import com.android.tools.r8.GenerateMainDexListCommand;
 import com.android.tools.r8.StringConsumer;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.origin.Origin;
+import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.PrintWriter;
@@ -154,6 +160,53 @@
     parse("--main-dex-list", mainDexList1.toString(), "--main-dex-list", mainDexList2.toString());
   }
 
+  @Test(expected = CompilationFailedException.class)
+  public void missingAndroidJar() throws Throwable {
+    DiagnosticsChecker.checkErrorsContains(
+        "Tracing for legacy multi dex is not possible without all classpath libraries",
+        handler ->
+            GenerateMainDexList.run(
+                GenerateMainDexListCommand.builder(handler).build()));
+  }
+
+  @Test(expected = CompilationFailedException.class)
+  public void duplicateProgramClasses() throws Throwable {
+    Path input = Paths.get(EXAMPLES_BUILD_DIR, "arithmetic.jar");
+    DiagnosticsChecker.checkErrorsContains(
+        "Program type already present",
+        handler ->
+            GenerateMainDexList.run(
+                GenerateMainDexListCommand.builder(handler)
+                    .addLibraryFiles(ToolHelper.getDefaultAndroidJar())
+                    .addProgramFiles(input)
+                    .addProgramFiles(input)
+                    .build()));
+  }
+
+  @Test
+  public void emptyMainDex() throws Throwable {
+    Path input = Paths.get(EXAMPLES_BUILD_DIR, "arithmetic.jar");
+    List<String> result = GenerateMainDexList.run(
+        GenerateMainDexListCommand.builder()
+            .addLibraryFiles(ToolHelper.getDefaultAndroidJar())
+            .addProgramFiles(input)
+            .build());
+    assertEquals(0, result.size());
+  }
+
+  @Test
+  public void nonEmptyMainDex() throws Throwable {
+    Path input = Paths.get(EXAMPLES_BUILD_DIR, "arithmetic.jar");
+    List<String> result = GenerateMainDexList.run(
+        GenerateMainDexListCommand.builder()
+            .addLibraryFiles(ToolHelper.getDefaultAndroidJar())
+            .addProgramFiles(input)
+            .addMainDexRules(
+                ImmutableList.of("-keep class arithmetic.Arithmetic"), Origin.unknown())
+            .build());
+    assertEquals(1, result.size());
+  }
+
   private GenerateMainDexListCommand parse(String... args) throws Throwable {
     return GenerateMainDexListCommand.parse(args).build();
   }