Add feature split policy and test

Bug: 165727167
Bug: 165000217
Change-Id: I0ced79922dad3dac8979268f1e24703431d34b2d
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
index c993870..4694c9c 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.horizontalclassmerging.policies.NotMatchedByNoHorizontalClassMerging;
 import com.android.tools.r8.horizontalclassmerging.policies.PreventMergeIntoMainDex;
 import com.android.tools.r8.horizontalclassmerging.policies.RespectPackageBoundaries;
+import com.android.tools.r8.horizontalclassmerging.policies.SameFeatureSplit;
 import com.android.tools.r8.horizontalclassmerging.policies.SameParentClass;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.ClassMergingEnqueuerExtension;
@@ -56,6 +57,7 @@
             new NotEntryPoint(appView.dexItemFactory()),
             new PreventMergeIntoMainDex(appView, mainDexTracingResult),
             new SameParentClass(),
+            new SameFeatureSplit(appView),
             new RespectPackageBoundaries(appView),
             new DontMergeSynchronizedClasses(appView)
             // TODO: add policies
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassSameReferencePolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassSameReferencePolicy.java
new file mode 100644
index 0000000..3407306
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassSameReferencePolicy.java
@@ -0,0 +1,26 @@
+// Copyright (c) 2020, 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;
+
+import com.android.tools.r8.graph.DexProgramClass;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.LinkedList;
+import java.util.Map;
+
+public abstract class MultiClassSameReferencePolicy<T> extends MultiClassPolicy {
+
+  @Override
+  public final Collection<Collection<DexProgramClass>> apply(Collection<DexProgramClass> group) {
+    Map<T, Collection<DexProgramClass>> groups = new IdentityHashMap<>();
+    for (DexProgramClass clazz : group) {
+      groups.computeIfAbsent(getMergeKey(clazz), ignore -> new LinkedList<>()).add(clazz);
+    }
+    removeTrivialGroups(groups.values());
+    return groups.values();
+  }
+
+  public abstract T getMergeKey(DexProgramClass clazz);
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameFeatureSplit.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameFeatureSplit.java
new file mode 100644
index 0000000..9e4dd2b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameFeatureSplit.java
@@ -0,0 +1,24 @@
+// Copyright (c) 2020, 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.FeatureSplit;
+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.shaking.AppInfoWithLiveness;
+
+public class SameFeatureSplit extends MultiClassSameReferencePolicy<FeatureSplit> {
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public SameFeatureSplit(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public FeatureSplit getMergeKey(DexProgramClass clazz) {
+    return appView.appInfo().getClassToFeatureSplitMap().getFeatureSplit(clazz);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithFeatureSplitTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithFeatureSplitTest.java
new file mode 100644
index 0000000..5907c150
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithFeatureSplitTest.java
@@ -0,0 +1,130 @@
+// Copyright (c) 2020, 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.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.dexsplitter.SplitterTestBase.RunInterface;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runners.Parameterized;
+
+public class ClassesWithFeatureSplitTest extends HorizontalClassMergingTestBase {
+  public ClassesWithFeatureSplitTest(
+      TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Parameterized.Parameters(name = "{0}, horizontalClassMerging:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDexRuntimes().withAllApiLevels().build(), BooleanUtils.values());
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    R8TestCompileResult compileResult =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(Base.class)
+            .addFeatureSplitRuntime()
+            .addFeatureSplit(Feature1Class1.class, Feature1Class2.class, Feature1Main.class)
+            .addFeatureSplit(Feature2Class.class, Feature2Main.class)
+            .addKeepFeatureMainRule(Feature1Main.class)
+            .addKeepFeatureMainRule(Feature2Main.class)
+            .addOptionsModification(
+                options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            .enableNeverClassInliningAnnotations()
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspect(this::inspectBase, this::inspectFeature1, this::inspectFeature2);
+
+    compileResult
+        .runFeature(parameters.getRuntime(), Feature1Main.class, compileResult.getFeature(0))
+        .assertSuccessWithOutputLines("base", "feature 1 class 1", "feature 1 class 2");
+
+    compileResult
+        .runFeature(parameters.getRuntime(), Feature2Main.class, compileResult.getFeature(1))
+        .assertSuccessWithOutputLines("base", "feature 2");
+  }
+
+  private void inspectBase(CodeInspector inspector) {
+    assertThat(inspector.clazz(Base.class), isPresent());
+    assertThat(inspector.clazz(Feature1Class1.class), not(isPresent()));
+    assertThat(inspector.clazz(Feature1Class2.class), not(isPresent()));
+    assertThat(inspector.clazz(Feature2Class.class), not(isPresent()));
+  }
+
+  private void inspectFeature1(CodeInspector inspector) {
+    assertThat(inspector.clazz(Feature1Main.class), isPresent());
+    assertThat(inspector.clazz(Feature1Class1.class), isPresent());
+    assertThat(
+        inspector.clazz(Feature1Class2.class), notIf(isPresent(), enableHorizontalClassMerging));
+    assertThat(inspector.clazz(Feature2Main.class), not(isPresent()));
+    assertThat(inspector.clazz(Feature2Class.class), not(isPresent()));
+  }
+
+  private void inspectFeature2(CodeInspector inspector) {
+    assertThat(inspector.clazz(Feature1Main.class), not(isPresent()));
+    assertThat(inspector.clazz(Feature1Class1.class), not(isPresent()));
+    assertThat(inspector.clazz(Feature1Class2.class), not(isPresent()));
+    assertThat(inspector.clazz(Feature2Main.class), isPresent());
+    assertThat(inspector.clazz(Feature2Class.class), isPresent());
+  }
+
+  @NeverClassInline
+  public static class Base {
+    public Base() {
+      System.out.println("base");
+    }
+  }
+
+  @NeverClassInline
+  public static class Feature1Class1 {
+    public Feature1Class1() {
+      System.out.println("feature 1 class 1");
+    }
+  }
+
+  @NeverClassInline
+  public static class Feature1Class2 {
+    public Feature1Class2() {
+      System.out.println("feature 1 class 2");
+    }
+  }
+
+  @NeverClassInline
+  public static class Feature2Class {
+    public Feature2Class() {
+      System.out.println("feature 2");
+    }
+  }
+
+  public static class Feature1Main implements RunInterface {
+
+    @Override
+    public void run() {
+      new Base();
+      new Feature1Class1();
+      new Feature1Class2();
+    }
+  }
+
+  public static class Feature2Main implements RunInterface {
+
+    @Override
+    public void run() {
+      new Base();
+      new Feature2Class();
+    }
+  }
+}