Test to build Chrome with startup instrumentation and minimal startup

Change-Id: I19328bddaf33b59267abb041ce1197684d68f911
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
index 70f752e..21fccf4 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
@@ -51,6 +51,7 @@
 import com.android.tools.r8.horizontalclassmerging.policies.PreventClassMethodAndDefaultMethodCollisions;
 import com.android.tools.r8.horizontalclassmerging.policies.RespectPackageBoundaries;
 import com.android.tools.r8.horizontalclassmerging.policies.SameFeatureSplit;
+import com.android.tools.r8.horizontalclassmerging.policies.SameFilePolicy;
 import com.android.tools.r8.horizontalclassmerging.policies.SameInstanceFields;
 import com.android.tools.r8.horizontalclassmerging.policies.SameMainDexGroup;
 import com.android.tools.r8.horizontalclassmerging.policies.SameNestHost;
@@ -273,6 +274,7 @@
     ImmediateProgramSubtypingInfo immediateSubtypingInfo =
         ImmediateProgramSubtypingInfo.create(appView);
     builder.add(
+        new SameFilePolicy(appView),
         new CheckAbstractClasses(appView),
         new NoClassAnnotationCollisions(),
         new SameFeatureSplit(appView),
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameFilePolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameFilePolicy.java
new file mode 100644
index 0000000..5643d8c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameFilePolicy.java
@@ -0,0 +1,34 @@
+// Copyright (c) 2022, 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.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.MultiClassSameReferencePolicy;
+import com.android.tools.r8.utils.InternalOptions.HorizontalClassMergerOptions;
+
+public class SameFilePolicy extends MultiClassSameReferencePolicy<String> {
+
+  private final HorizontalClassMergerOptions options;
+
+  public SameFilePolicy(AppView<?> appView) {
+    this.options = appView.options().horizontalClassMergerOptions();
+  }
+
+  @Override
+  public String getMergeKey(DexProgramClass clazz) {
+    return clazz.getType().toDescriptorString().replaceAll("^([^$]+)\\$.*", "$1");
+  }
+
+  @Override
+  public String getName() {
+    return "SameFilePolicy";
+  }
+
+  @Override
+  public boolean shouldSkipPolicy() {
+    return !options.isSameFilePolicyEnabled();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 04eb88c..50fb125 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -1504,6 +1504,7 @@
     private boolean enableInterfaceMerging =
         System.getProperty("com.android.tools.r8.enableHorizontalInterfaceMerging") != null;
     private boolean enableInterfaceMergingInInitial = false;
+    private boolean enableSameFilePolicy = false;
     private boolean enableSyntheticMerging = true;
     private boolean ignoreRuntimeTypeChecksForTesting = false;
     private boolean restrictToSynthetics = false;
@@ -1559,6 +1560,10 @@
       return ignoreRuntimeTypeChecksForTesting;
     }
 
+    public boolean isSameFilePolicyEnabled() {
+      return enableSameFilePolicy;
+    }
+
     public boolean isSyntheticMergingEnabled() {
       return enableSyntheticMerging;
     }
@@ -1594,6 +1599,10 @@
       enableInterfaceMergingInInitial = true;
     }
 
+    public void setEnableSameFilePolicy(boolean enableSameFilePolicy) {
+      this.enableSameFilePolicy = enableSameFilePolicy;
+    }
+
     public void setIgnoreRuntimeTypeChecksForTesting() {
       ignoreRuntimeTypeChecksForTesting = true;
     }
diff --git a/src/test/java/com/android/tools/r8/internal/startup/ChromeStartupTest.java b/src/test/java/com/android/tools/r8/internal/startup/ChromeStartupTest.java
new file mode 100644
index 0000000..390e702
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/internal/startup/ChromeStartupTest.java
@@ -0,0 +1,266 @@
+// Copyright (c) 2022, 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.internal.startup;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.ArchiveProgramResourceProvider;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.experimental.startup.StartupConfiguration;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ZipUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ChromeStartupTest extends TestBase {
+
+  private static AndroidApiLevel apiLevel = AndroidApiLevel.N;
+
+  // Location of dump.zip and startup.txt.
+  private static Path chromeDirectory = Paths.get("build/chrome/startup");
+
+  // Location of test artifacts.
+  private static Path artifactDirectory = Paths.get("build/chrome/startup");
+
+  // Temporary directory where dump.zip is extracted into.
+  private static Path dumpDirectory;
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  @BeforeClass
+  public static void setup() throws IOException {
+    assumeTrue(ToolHelper.isLocalDevelopment());
+    dumpDirectory = getStaticTemp().newFolder().toPath();
+    ZipUtils.unzip(chromeDirectory.resolve("dump.zip"), dumpDirectory);
+  }
+
+  // Outputs the instrumented dex in chrome/instrumented.
+  @Test
+  public void buildInstrumentedDex() throws Exception {
+    buildInstrumentedBase();
+    buildInstrumentedChromeSplit();
+  }
+
+  private void buildInstrumentedBase() throws Exception {
+    Files.createDirectories(artifactDirectory.resolve("instrumented/base/dex"));
+    testForD8()
+        .addProgramFiles(dumpDirectory.resolve("program.jar"))
+        .addClasspathFiles(dumpDirectory.resolve("classpath.jar"))
+        .addLibraryFiles(dumpDirectory.resolve("library.jar"))
+        .addOptionsModification(
+            options ->
+                options
+                    .getStartupOptions()
+                    .setEnableStartupInstrumentation()
+                    .setStartupInstrumentationTag("r8"))
+        .setMinApi(apiLevel)
+        .release()
+        .compile()
+        .writeToDirectory(artifactDirectory.resolve("instrumented/base/dex"));
+  }
+
+  private void buildInstrumentedChromeSplit() throws Exception {
+    Files.createDirectories(artifactDirectory.resolve("instrumented/chrome/dex"));
+    testForD8()
+        // The Chrome split is the feature that contains ChromeApplicationImpl.
+        .addProgramFiles(getChromeSplit())
+        .addClasspathFiles(dumpDirectory.resolve("program.jar"))
+        .addClasspathFiles(dumpDirectory.resolve("classpath.jar"))
+        .addLibraryFiles(dumpDirectory.resolve("library.jar"))
+        .addOptionsModification(
+            options ->
+                options
+                    .getStartupOptions()
+                    .setEnableStartupInstrumentation()
+                    .setStartupInstrumentationTag("r8"))
+        .setMinApi(apiLevel)
+        .release()
+        .compile()
+        .inspect(
+            inspector ->
+                assertThat(
+                    inspector.clazz("org.chromium.chrome.browser.ChromeApplicationImpl"),
+                    isPresent()))
+        .writeToDirectory(artifactDirectory.resolve("instrumented/chrome/dex"));
+  }
+
+  private Path getChromeSplit() {
+    return getFeatureSplit(9);
+  }
+
+  private Path getFeatureSplit(int index) {
+    return dumpDirectory.resolve("feature-" + index + ".jar");
+  }
+
+  // Outputs Chrome built using R8 in chrome/default.
+  @Test
+  public void buildR8Default() throws Exception {
+    buildR8(ThrowableConsumer.empty(), artifactDirectory.resolve("default"));
+  }
+
+  // Outputs Chrome built using R8 with limited class merging in chrome/default-with-patches.
+  @Test
+  public void buildR8DefaultWithPatches() throws Exception {
+    buildR8(
+        testBuilder ->
+            testBuilder.addOptionsModification(
+                options -> options.horizontalClassMergerOptions().setEnableSameFilePolicy(true)),
+        artifactDirectory.resolve("default-with-patches"));
+  }
+
+  // Outputs Chrome built using R8 with minimal startup dex and no boundary optimizations in
+  // chrome/optimized-minimal-nooptimize.
+  @Test
+  public void buildR8MinimalStartupDexWithoutBoundaryOptimizations() throws Exception {
+    boolean enableMinimalStartupDex = true;
+    boolean enableStartupBoundaryOptimizations = false;
+    buildR8Startup(
+        enableMinimalStartupDex,
+        enableStartupBoundaryOptimizations,
+        artifactDirectory.resolve("optimized-minimal-nooptimize"));
+  }
+
+  // Outputs Chrome built using R8 with minimal startup dex and boundary optimizations enabled in
+  // chrome/optimized-minimal-optimize.
+  @Test
+  public void buildR8MinimalStartupDexWithBoundaryOptimizations() throws Exception {
+    boolean enableMinimalStartupDex = true;
+    boolean enableStartupBoundaryOptimizations = true;
+    buildR8Startup(
+        enableMinimalStartupDex,
+        enableStartupBoundaryOptimizations,
+        artifactDirectory.resolve("optimized-minimal-optimize"));
+  }
+
+  // Outputs Chrome built using R8 with startup layout enabled and no boundary optimizations in
+  // chrome/optimized-nominimal-nooptimize.
+  @Test
+  public void buildR8StartupLayoutWithoutBoundaryOptimizations() throws Exception {
+    boolean enableMinimalStartupDex = false;
+    boolean enableStartupBoundaryOptimizations = false;
+    buildR8Startup(
+        enableMinimalStartupDex,
+        enableStartupBoundaryOptimizations,
+        artifactDirectory.resolve("optimized-nominimal-nooptimize"));
+  }
+
+  // Outputs Chrome built using R8 with startup layout enabled and no boundary optimizations in
+  // chrome/optimized-nominimal-optimize.
+  @Test
+  public void buildR8StartupLayoutWithBoundaryOptimizations() throws Exception {
+    boolean enableMinimalStartupDex = false;
+    boolean enableStartupBoundaryOptimizations = true;
+    buildR8Startup(
+        enableMinimalStartupDex,
+        enableStartupBoundaryOptimizations,
+        artifactDirectory.resolve("optimized-nominimal-optimize"));
+  }
+
+  private void buildR8(ThrowableConsumer<R8FullTestBuilder> configuration, Path outDirectory)
+      throws Exception {
+    Files.createDirectories(outDirectory.resolve("base/dex"));
+    Files.createDirectories(outDirectory.resolve("chrome/dex"));
+    testForR8(Backend.DEX)
+        .addProgramFiles(dumpDirectory.resolve("program.jar"))
+        .addClasspathFiles(dumpDirectory.resolve("classpath.jar"))
+        .addLibraryFiles(dumpDirectory.resolve("library.jar"))
+        .addKeepRuleFiles(dumpDirectory.resolve("proguard.config"))
+        .apply(
+            testBuilder -> {
+              int i = 1;
+              boolean seenChromeSplit = false;
+              for (; i <= 12; i++) {
+                Path feature = getFeatureSplit(i);
+                boolean isChromeSplit = feature.equals(getChromeSplit());
+                seenChromeSplit |= isChromeSplit;
+                assertTrue(feature.toFile().exists());
+                testBuilder.addFeatureSplit(
+                    featureSplitBuilder ->
+                        featureSplitBuilder
+                            .addProgramResourceProvider(
+                                ArchiveProgramResourceProvider.fromArchive(feature))
+                            .setProgramConsumer(
+                                isChromeSplit
+                                    ? new DexIndexedConsumer.DirectoryConsumer(
+                                        outDirectory.resolve("chrome/dex"))
+                                    : DexIndexedConsumer.emptyConsumer())
+                            .build());
+              }
+              assertFalse(dumpDirectory.resolve("feature-" + i + ".jar").toFile().exists());
+              assertTrue(seenChromeSplit);
+              assertThat(
+                  new CodeInspector(getChromeSplit())
+                      .clazz("org.chromium.chrome.browser.ChromeApplicationImpl"),
+                  isPresent());
+            })
+        .apply(configuration)
+        .apply(this::disableR8StrictMode)
+        .apply(this::disableR8TestingDefaults)
+        .setMinApi(apiLevel)
+        .compile()
+        .writeToDirectory(outDirectory.resolve("base/dex"));
+  }
+
+  private void buildR8Startup(
+      boolean enableMinimalStartupDex,
+      boolean enableStartupBoundaryOptimizations,
+      Path outDirectory)
+      throws Exception {
+    Path startupList = chromeDirectory.resolve("startup.txt");
+    buildR8(
+        testBuilder ->
+            testBuilder.addOptionsModification(
+                options ->
+                    options
+                        .getStartupOptions()
+                        .setEnableMinimalStartupDex(enableMinimalStartupDex)
+                        .setEnableStartupBoundaryOptimizations(enableStartupBoundaryOptimizations)
+                        .setStartupConfiguration(
+                            StartupConfiguration.createStartupConfigurationFromFile(
+                                options.dexItemFactory(), options.reporter, startupList))),
+        outDirectory);
+  }
+
+  private void disableR8StrictMode(R8FullTestBuilder testBuilder) {
+    testBuilder
+        .allowDiagnosticMessages()
+        .allowUnnecessaryDontWarnWildcards()
+        .allowUnusedDontWarnPatterns()
+        .allowUnusedProguardConfigurationRules()
+        .addOptionsModification(
+            options -> options.getOpenClosedInterfacesOptions().suppressAllOpenInterfaces());
+  }
+
+  private void disableR8TestingDefaults(R8FullTestBuilder testBuilder) {
+    testBuilder.addOptionsModification(
+        options -> options.horizontalClassMergerOptions().setEnableInterfaceMerging(false));
+  }
+}