Reproduce assertion failure from enabling of constructor shrinking
Fixes: b/244689892
Bug: b/173751869
Change-Id: I446c26118fa2a44de75ee4a73f7dcce2080b4821
diff --git a/src/test/java/com/android/tools/r8/shaking/ForwardingConstructorShakingOnDexWithClassMergingTest.java b/src/test/java/com/android/tools/r8/shaking/ForwardingConstructorShakingOnDexWithClassMergingTest.java
new file mode 100644
index 0000000..7c3abe2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ForwardingConstructorShakingOnDexWithClassMergingTest.java
@@ -0,0 +1,150 @@
+// 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.shaking;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.AssertUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.Iterables;
+import java.util.Objects;
+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 ForwardingConstructorShakingOnDexWithClassMergingTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ @Test
+ public void test() throws Exception {
+ AssertUtils.assertFailsCompilationIf(
+ canHaveNonReboundConstructorInvoke(),
+ () ->
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .addKeepMainRule(Main.class)
+ .addOptionsModification(
+ options -> {
+ options.testing.enableRedundantConstructorBridgeRemoval = true;
+ options.testing.horizontalClassMergingTarget =
+ (appView, candidates, target) ->
+ Iterables.find(
+ candidates,
+ candidate ->
+ candidate.getTypeName().equals(B.class.getTypeName()));
+ })
+ .addHorizontallyMergedClassesInspector(
+ inspector ->
+ inspector.assertMergedInto(A.class, B.class).assertNoOtherClassesMerged())
+ .enableInliningAnnotations()
+ .enableNoVerticalClassMergingAnnotations()
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .inspect(this::inspect)
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines("Hello, world!"));
+ }
+
+ private boolean canHaveNonReboundConstructorInvoke() {
+ return parameters.isDexRuntime()
+ && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.L);
+ }
+
+ private void inspect(CodeInspector inspector) {
+ ClassSubject aSubClassSubject = inspector.clazz(ASub.class);
+ assertThat(aSubClassSubject, isPresent());
+ assertEquals(
+ canHaveNonReboundConstructorInvoke() ? 0 : 1, aSubClassSubject.allMethods().size());
+ }
+
+ public static class Main {
+
+ public static void main(String[] args) {
+ System.out.print(new A(obfuscate("Hello")).getMessageFromA());
+ System.out.print(new ASub(obfuscate(", ")).getMessageFromA());
+ System.out.println(new B(obfuscate("world!")).getMessageFromB());
+ }
+
+ @NeverInline
+ private static String obfuscate(String str) {
+ return System.currentTimeMillis() > 0 ? str : "dead";
+ }
+
+ @NeverInline
+ public static void requireNonNull(Object o) {
+ Objects.requireNonNull(o);
+ }
+ }
+
+ @NoVerticalClassMerging
+ public static class A {
+
+ private final String msg;
+
+ // Will be merged with B.<init>(String). Since B is chosen as the merge target, a new
+ // B.<init>(String, int) method will be generated, and mappings will be created from
+ // A.<init>(String) -> B.init$A(String) and B.<init>(String) -> B.init$B(String).
+ public A(String msg) {
+ // So that A.<init>(String) and B.<init>(String) will not be considered identical during class
+ // merging. The indirection is to ensure that the API level of class A is 1.
+ Main.requireNonNull(msg);
+ this.msg = msg;
+ }
+
+ // Will be moved to B.<init>(String, String) by horizontal class merging, and then be optimized
+ // to B.<init>(String) by unused argument removal.
+ public A(String unused, String msg) {
+ this.msg = msg;
+ }
+
+ @NeverInline
+ public String getMessageFromA() {
+ return msg;
+ }
+ }
+
+ public static class ASub extends A {
+
+ // After horizontal class merging and unused argument removal, this is rewritten to target
+ // B.<init>(String). This constructor can therefore be eliminated by the redundant bridge
+ // removal phase.
+ public ASub(String msg) {
+ super("<unused>", msg);
+ }
+ }
+
+ public static class B {
+
+ private final String msg;
+
+ public B(String msg) {
+ this.msg = msg;
+ }
+
+ @NeverInline
+ public String getMessageFromB() {
+ return msg;
+ }
+ }
+}