Desugared lib: Feature split support

Bug: 146329897
Change-Id: I08fd5cd22e616a490b1c301c390a3ddabfa735fc
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index 5daaccc..7ba04b5 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -71,6 +71,7 @@
   public final GraphLense graphLense;
   public final NamingLens namingLens;
   public final InternalOptions options;
+  private final CodeToKeep desugaredLibraryCodeToKeep;
   public List<Marker> markers;
   public List<DexString> markerStrings;
 
@@ -167,6 +168,7 @@
     this.appView = appView;
     assert options != null;
     this.options = options;
+    this.desugaredLibraryCodeToKeep = CodeToKeep.createCodeToKeep(options, namingLens);
     this.markers = markers;
     this.graphLense = graphLense;
     this.namingLens = namingLens;
@@ -310,6 +312,11 @@
       }
       // Wait for all files to be processed before moving on.
       ThreadUtils.awaitFutures(dexDataFutures);
+      // A consumer can manage the generated keep rules.
+      if (options.desugaredLibraryKeepRuleConsumer != null && !desugaredLibraryCodeToKeep.isNop()) {
+        assert !options.isDesugaredLibraryCompilation();
+        desugaredLibraryCodeToKeep.generateKeepRules(options);
+      }
       // Fail if there are pending errors, e.g., the program consumers may have reported errors.
       options.reporter.failIfPendingErrors();
       // Supply info to all additional resource consumers.
@@ -576,7 +583,14 @@
       MethodToCodeObjectMapping codeMapping,
       ByteBufferProvider provider) {
     FileWriter fileWriter =
-        new FileWriter(provider, objectMapping, codeMapping, application, options, namingLens);
+        new FileWriter(
+            provider,
+            objectMapping,
+            codeMapping,
+            application,
+            options,
+            namingLens,
+            desugaredLibraryCodeToKeep);
     // Collect the non-fixed sections.
     fileWriter.collect();
     // Generate and write the bytes.
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index fa50d92..5cbd975 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -101,7 +101,8 @@
       MethodToCodeObjectMapping codeMapping,
       DexApplication application,
       InternalOptions options,
-      NamingLens namingLens) {
+      NamingLens namingLens,
+      CodeToKeep desugaredLibraryCodeToKeep) {
     this.mapping = mapping;
     this.codeMapping = codeMapping;
     this.application = application;
@@ -109,7 +110,7 @@
     this.namingLens = namingLens;
     this.dest = new DexOutputBuffer(provider);
     this.mixedSectionOffsets = new MixedSectionOffsets(options, codeMapping);
-    this.desugaredLibraryCodeToKeep = CodeToKeep.createCodeToKeep(options, namingLens);
+    this.desugaredLibraryCodeToKeep = desugaredLibraryCodeToKeep;
   }
 
   public static void writeEncodedAnnotation(
@@ -223,12 +224,6 @@
     writeSignature(layout);
     writeChecksum(layout);
 
-    // A consumer can manage the generated keep rules.
-    if (options.desugaredLibraryKeepRuleConsumer != null && !desugaredLibraryCodeToKeep.isNop()) {
-      assert !options.isDesugaredLibraryCompilation();
-      desugaredLibraryCodeToKeep.generateKeepRules(options);
-    }
-
     // Wrap backing buffer with actual length.
     return new ByteBufferResult(dest.stealByteBuffer(), layout.getEndOfFile());
   }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/FeatureSplitTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/FeatureSplitTest.java
new file mode 100644
index 0000000..2f02755
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/FeatureSplitTest.java
@@ -0,0 +1,269 @@
+// Copyright (c) 2019, 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.desugar.desugaredlibrary;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.dexsplitter.SplitterTestBase;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import dalvik.system.PathClassLoader;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+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 FeatureSplitTest extends DesugaredLibraryTestBase {
+
+  private final TestParameters parameters;
+  private final boolean shrinkDesugaredLibrary;
+  private final Class<?>[] baseAndFeatureClasses =
+      new Class<?>[] {BaseClass.class, FeatureClass.class, FeatureClass2.class};
+
+  @Parameters(name = "{1}, shrinkDesugaredLibrary: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withDexRuntimes().withAllApiLevels().build());
+  }
+
+  public FeatureSplitTest(boolean shrinkDesugaredLibrary, TestParameters parameters) {
+    this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testTwoFeatures() throws CompilationFailedException, IOException, ExecutionException {
+    CompiledWithFeature compiledWithFeature = new CompiledWithFeature().invoke(this);
+    Path basePath = compiledWithFeature.getBasePath();
+    Path feature1Path = compiledWithFeature.getFeature1Path();
+    Path feature2Path = compiledWithFeature.getFeature2Path();
+    Path desugaredLibrary = compiledWithFeature.getDesugaredLibrary();
+
+    assertKeepThe3StreamMethods(compiledWithFeature.getKeepRules());
+
+    assertClassPresent(basePath, BaseClass.class);
+    assertClassPresent(feature1Path, FeatureClass.class);
+    assertClassPresent(feature2Path, FeatureClass2.class);
+
+    verifyRun(BaseClass.class, basePath, desugaredLibrary, null, "42");
+    verifyRun(FeatureClass.class, basePath, desugaredLibrary, feature1Path, "1");
+    verifyRun(FeatureClass2.class, basePath, desugaredLibrary, feature2Path, "7");
+  }
+
+  private void assertKeepThe3StreamMethods(String keepRules) {
+    // Ensure count, toArray and forEach are kept.
+    assertTrue(
+        keepRules.contains(
+            "-keep class j$.lang.Iterable$-EL {\n"
+                + "    void forEach(java.lang.Iterable, j$.util.function.Consumer);"));
+    assertTrue(
+        keepRules.contains(
+            "-keep class j$.util.stream.Stream {\n"
+                + "    long count();\n"
+                + "    java.lang.Object[] toArray();"));
+  }
+
+  private void assertClassPresent(Path basePath, Class<?> present)
+      throws IOException, ExecutionException {
+    CodeInspector inspector = new CodeInspector(basePath);
+    for (Class<?> clazz : baseAndFeatureClasses) {
+      if (clazz == present) {
+        assertTrue(inspector.clazz(present).isPresent());
+      } else {
+        assertFalse(inspector.clazz(present).isPresent());
+      }
+    }
+  }
+
+  private void verifyRun(
+      Class<?> toRun,
+      Path basePath,
+      Path desugaredLibrary,
+      Path splitterFeatureDexFile,
+      String expectedResult)
+      throws IOException {
+    ProcessResult result =
+        runFeatureOnArt(
+            toRun, desugaredLibrary, basePath, splitterFeatureDexFile, parameters.getRuntime());
+    assertEquals(result.exitCode, 0);
+    assertEquals(result.stdout, StringUtils.lines(expectedResult));
+  }
+
+  protected ProcessResult runFeatureOnArt(
+      Class toRun,
+      Path desugaredLibrary,
+      Path splitterBaseDexFile,
+      Path splitterFeatureDexFile,
+      TestRuntime runtime)
+      throws IOException {
+    assumeTrue(runtime.isDex());
+    ArtCommandBuilder commandBuilder = new ArtCommandBuilder(runtime.asDex().getVm());
+    commandBuilder.appendClasspath(splitterBaseDexFile.toString());
+    if (desugaredLibrary != null) {
+      commandBuilder.appendClasspath(desugaredLibrary.toString());
+    }
+    commandBuilder.appendProgramArgument(toRun.getName());
+    if (splitterFeatureDexFile != null) {
+      commandBuilder.appendProgramArgument(splitterFeatureDexFile.toString());
+    }
+    commandBuilder.setMainClass(SplitRunner.class.getName());
+    return ToolHelper.runArtRaw(commandBuilder);
+  }
+
+  public interface RunInterface {
+
+    void run();
+  }
+
+  // Base using ForEach.
+  public static class BaseClass implements RunInterface {
+
+    @Override
+    public void run() {
+      ArrayList<Integer> list = new ArrayList<>();
+      list.add(42);
+      list.forEach(System.out::println);
+    }
+  }
+
+  // Feature using count.
+  public static class FeatureClass implements RunInterface {
+
+    @SuppressWarnings("ReplaceInefficientStreamCount")
+    @Override
+    public void run() {
+      ArrayList<Object> list = new ArrayList<>();
+      list.add(new Object());
+      System.out.println(list.stream().count());
+    }
+  }
+
+  // Feature using toArray.
+  public static class FeatureClass2 implements RunInterface {
+
+    @SuppressWarnings("SimplifyStreamApiCallChains")
+    @Override
+    public void run() {
+      ArrayList<Integer> list = new ArrayList<>();
+      list.add(7);
+      System.out.println(list.stream().toArray()[0]);
+    }
+  }
+
+  static class SplitRunner {
+
+    /* We support two different modes:
+     *   - One argument to main:
+     *     Pass in the class to be loaded, must implement RunInterface, run will be called.
+     *   - Two arguments to main:
+     *     Pass in the class to be loaded, must implement RunInterface, run will be called.
+     *     Pass in the feature split that we class load.
+     */
+    public static void main(String[] args) {
+      if (args.length < 1 || args.length > 2) {
+        throw new RuntimeException("Unsupported number of arguments");
+      }
+      String classToRun = args[0];
+      ClassLoader loader = SplitRunner.class.getClassLoader();
+      // In the case where we simulate splits, we pass in the feature as the second argument
+      if (args.length == 2) {
+        try {
+          loader = new PathClassLoader(args[1], SplitRunner.class.getClassLoader());
+        } catch (MalformedURLException e) {
+          throw new RuntimeException("Failed reading input URL");
+        }
+      }
+
+      try {
+        Class<?> aClass = loader.loadClass(classToRun);
+        RunInterface b = (RunInterface) aClass.newInstance();
+        b.run();
+      } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
+        throw new RuntimeException("Failed loading class");
+      }
+    }
+  }
+
+  private class CompiledWithFeature {
+
+    private Path basePath;
+    private Path feature1Path;
+    private Path feature2Path;
+    private Path desugaredLibrary;
+    private String keepRules = "";
+
+    public Path getBasePath() {
+      return basePath;
+    }
+
+    public Path getFeature1Path() {
+      return feature1Path;
+    }
+
+    public Path getFeature2Path() {
+      return feature2Path;
+    }
+
+    public Path getDesugaredLibrary() {
+      return desugaredLibrary;
+    }
+
+    public String getKeepRules() {
+      return keepRules;
+    }
+
+    public CompiledWithFeature invoke(FeatureSplitTest tester)
+        throws IOException, CompilationFailedException {
+
+      basePath = temp.newFile("base.zip").toPath();
+      feature1Path = temp.newFile("feature1.zip").toPath();
+      feature2Path = temp.newFile("feature2.zip").toPath();
+
+      KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+      testForR8(parameters.getBackend())
+          .addProgramClasses(BaseClass.class, RunInterface.class, SplitRunner.class)
+          .setMinApi(parameters.getApiLevel())
+          .addFeatureSplit(
+              builder ->
+                  SplitterTestBase.simpleSplitProvider(
+                      builder, feature1Path, temp, FeatureClass.class))
+          .addFeatureSplit(
+              builder ->
+                  SplitterTestBase.simpleSplitProvider(
+                      builder, feature2Path, temp, FeatureClass2.class))
+          .addKeepAllClassesRule()
+          .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+          .compile()
+          .writeToZip(basePath);
+      // Library desugaring is not needed above N.
+      if (parameters.getApiLevel().getLevel() > AndroidApiLevel.N.getLevel()) {
+        return this;
+      }
+      desugaredLibrary =
+          tester.buildDesugaredLibrary(
+              parameters.getApiLevel(), keepRuleConsumer.get(), shrinkDesugaredLibrary);
+      keepRules = keepRuleConsumer.get();
+      return this;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java b/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
index fea2bce..1e31022 100644
--- a/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
+++ b/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
@@ -41,12 +41,12 @@
 
 public class SplitterTestBase extends TestBase {
 
-  protected static FeatureSplit simpleSplitProvider(
+  public static FeatureSplit simpleSplitProvider(
       FeatureSplit.Builder builder, Path outputPath, TemporaryFolder temp, Class... classes) {
     return simpleSplitProvider(builder, outputPath, temp, Arrays.asList(classes));
   }
 
-  protected static FeatureSplit simpleSplitProvider(
+  public static FeatureSplit simpleSplitProvider(
       FeatureSplit.Builder builder,
       Path outputPath,
       TemporaryFolder temp,