Add tests to cover the implicit keeping of default constructors
For Proguard compatibility mode we try to keep the default constructor
in the same situations as Proguard through implicit keep rules. However,
right now we over-approximate, which these tests document.
Also add a couple of junit/hamcrest matchers for DexInspector.
Bug: 74233021
Bug: 74379749
Change-Id: Idf5f0dd8f8f0ee1b2e14c087dbf0880f974f060f
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 5cf4b10..d30b2e2 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -34,6 +34,7 @@
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
@@ -46,6 +47,7 @@
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
+import joptsimple.internal.Strings;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
@@ -191,7 +193,7 @@
/**
* Get the class name generated by javac.
*/
- protected String getJavacGeneratedClassName(Class clazz) {
+ protected static String getJavacGeneratedClassName(Class clazz) {
List<String> parts = Lists.newArrayList(clazz.getCanonicalName().split("\\."));
Class enclosing = clazz;
while (enclosing.getEnclosingClass() != null) {
@@ -353,13 +355,29 @@
* the specified class.
*/
public static String keepMainProguardConfiguration(Class clazz) {
- return keepMainProguardConfiguration(clazz.getCanonicalName());
+ return keepMainProguardConfiguration(clazz, ImmutableList.of());
}
/**
* Generate a Proguard configuration for keeping the "public static void main(String[])" method of
* the specified class.
*/
+ public static String keepMainProguardConfiguration(Class clazz, List<String> additionalLines) {
+ String modifier = (clazz.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC ? "public " : "";
+ return Strings.join(ImmutableList.of(
+ "-keep " + modifier + "class " + getJavacGeneratedClassName(clazz) + " {",
+ " public static void main(java.lang.String[]);",
+ "}",
+ "-printmapping"
+ ), "\n") + (additionalLines.size() > 0 ? ("\n" + Strings.join(additionalLines, "\n")) : "");
+ }
+
+ /**
+ * Generate a Proguard configuration for keeping the "public static void main(String[])" method of
+ * the specified class.
+ *
+ * The class is assumed to be public.
+ */
public static String keepMainProguardConfiguration(String clazz) {
return "-keep public class " + clazz + " {\n"
+ " public static void main(java.lang.String[]);\n"
diff --git a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ProguardCompatabilityTestBase.java b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ProguardCompatabilityTestBase.java
new file mode 100644
index 0000000..d098ef7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ProguardCompatabilityTestBase.java
@@ -0,0 +1,60 @@
+// Copyright (c) 2018, 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.forceproguardcompatibility;
+
+import com.android.tools.r8.CompatProguardCommandBuilder;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.FileUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.File;
+import java.nio.file.Path;
+import java.util.List;
+
+public class ProguardCompatabilityTestBase extends TestBase {
+
+ protected DexInspector runR8Compat(
+ List<Class> programClasses, String proguardConfig) throws Exception {
+ CompatProguardCommandBuilder builder = new CompatProguardCommandBuilder(true);
+ builder.addProguardConfiguration(ImmutableList.of(proguardConfig), Origin.unknown());
+ programClasses.forEach(
+ clazz -> builder.addProgramFiles(ToolHelper.getClassFileForTestClass(clazz)));
+ builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
+ return new DexInspector(ToolHelper.runR8(builder.build()));
+ }
+
+ protected DexInspector runR8CompatKeepingMain(Class mainClass, List<Class> programClasses)
+ throws Exception {
+ return runR8Compat(programClasses, keepMainProguardConfiguration(mainClass));
+ }
+
+ protected DexInspector runProguard(
+ List<Class> programClasses, String proguardConfig) throws Exception {
+ Path proguardedJar = File.createTempFile("proguarded", ".jar", temp.getRoot()).toPath();
+ Path proguardConfigFile = File.createTempFile("proguard", ".config", temp.getRoot()).toPath();
+ FileUtils.writeTextFile(proguardConfigFile, proguardConfig);
+ ToolHelper.runProguard(jarTestClasses(programClasses), proguardedJar, proguardConfigFile, null);
+ return new DexInspector(readJar(proguardedJar));
+ }
+
+ protected DexInspector runProguardAndD8(
+ List<Class> programClasses, String proguardConfig) throws Exception {
+ Path proguardedJar = File.createTempFile("proguarded", ".jar", temp.getRoot()).toPath();
+ Path proguardConfigFile = File.createTempFile("proguard", ".config", temp.getRoot()).toPath();
+ FileUtils.writeTextFile(proguardConfigFile, proguardConfig);
+ ToolHelper.runProguard(jarTestClasses(programClasses), proguardedJar, proguardConfigFile, null);
+ AndroidApp app = ToolHelper.runD8(readJar(proguardedJar));
+ return new DexInspector(app);
+ }
+
+ protected DexInspector runProguardKeepingMain(Class mainClass, List<Class> programClasses)
+ throws Exception {
+ return runProguardAndD8(programClasses, keepMainProguardConfiguration(mainClass));
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/defaultctor/ImplicitlyKeptDefaultConstructorTest.java b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/defaultctor/ImplicitlyKeptDefaultConstructorTest.java
new file mode 100644
index 0000000..39e804a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/defaultctor/ImplicitlyKeptDefaultConstructorTest.java
@@ -0,0 +1,299 @@
+// Copyright (c) 2018, 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.forceproguardcompatibility.defaultctor;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.hasDefaultConstructor;
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.shaking.forceproguardcompatibility.ProguardCompatabilityTestBase;
+import com.android.tools.r8.smali.ConstantFoldingTest.TriConsumer;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+
+class SuperClass {
+
+}
+
+class SubClass extends SuperClass {
+
+}
+
+class MainInstantiationSubClass {
+
+ public static void main(String[] args) {
+ new SubClass();
+ }
+}
+
+class MainGetClassSubClass {
+
+ public static void main(String[] args) {
+ System.out.println(SubClass.class);
+ }
+}
+
+class MainClassForNameSubClass {
+
+ public static void main(String[] args) throws Exception{
+ System.out.println(Class.forName(
+ "com.android.tools.r8.shaking.forceproguardcompatibility.defaultctor.SubClass"));
+ }
+}
+
+class StaticFieldNotInitialized {
+ public static int field;
+}
+
+class MainGetStaticFieldNotInitialized {
+
+ public static void main(String[] args) {
+ System.out.println(StaticFieldNotInitialized.field);
+ }
+}
+
+class StaticMethod {
+ public static int method() {
+ return 1;
+ };
+}
+
+class MainCallStaticMethod {
+
+ public static void main(String[] args) {
+ System.out.println(StaticMethod.method());
+ }
+}
+
+class StaticFieldInitialized {
+ public static int field = 1;
+}
+
+class MainGetStaticFieldInitialized {
+
+ public static void main(String[] args) {
+ System.out.println(StaticFieldInitialized.field);
+ }
+}
+
+public class ImplicitlyKeptDefaultConstructorTest extends ProguardCompatabilityTestBase {
+
+ private void checkPresentWithDefaultConstructor(ClassSubject clazz) {
+ assertThat(clazz, isPresent());
+ assertThat(clazz, hasDefaultConstructor());
+ }
+
+ private void checkPresentWithoutDefaultConstructor(ClassSubject clazz) {
+ assertThat(clazz, isPresent());
+ assertThat(clazz, not(hasDefaultConstructor()));
+ }
+
+ private void checkAllClassesPresentWithDefaultConstructor(
+ Class mainClass, List<Class> programClasses, DexInspector inspector) {
+ assert programClasses.contains(mainClass);
+ assertEquals(programClasses.size(), inspector.allClasses().size());
+ inspector.forAllClasses(this::checkPresentWithDefaultConstructor);
+ }
+
+ private void checkAllClassesPresentOnlyMainWithDefaultConstructor(
+ Class mainClass, List<Class> programClasses, DexInspector inspector) {
+ assert programClasses.contains(mainClass);
+ assertEquals(programClasses.size(), inspector.allClasses().size());
+ checkPresentWithDefaultConstructor(inspector.clazz(mainClass));
+ inspector.allClasses()
+ .stream()
+ .filter(subject -> !subject.getOriginalName().equals(mainClass.getCanonicalName()))
+ .forEach(this::checkPresentWithoutDefaultConstructor);
+ }
+
+ private void checkOnlyMainPresent(
+ Class mainClass, List<Class> programClasses, DexInspector inspector) {
+ assert programClasses.contains(mainClass);
+ assertEquals(1, inspector.allClasses().size());
+ inspector.forAllClasses(this::checkPresentWithDefaultConstructor);
+ }
+
+ private void runTest(
+ Class mainClass, List<Class> programClasses, String proguardConfiguration,
+ TriConsumer<Class, List<Class>, DexInspector> r8Checker,
+ TriConsumer<Class, List<Class>, DexInspector> proguardChecker) throws Exception {
+ DexInspector inspector = runR8Compat(programClasses, proguardConfiguration);
+ r8Checker.accept(mainClass, programClasses, inspector);
+
+ if (isRunProguard()) {
+ inspector = runProguard(programClasses, proguardConfiguration);
+ proguardChecker.accept(mainClass, programClasses, inspector);
+ inspector = runProguardAndD8(programClasses, proguardConfiguration);
+ proguardChecker.accept(mainClass, programClasses, inspector);
+ }
+ }
+
+ private void runTest(
+ Class mainClass, List<Class> programClasses, String proguardConfiguration,
+ TriConsumer<Class, List<Class>, DexInspector> checker) throws Exception {
+ runTest(mainClass, programClasses, proguardConfiguration, checker, checker);
+ }
+
+ @Test
+ public void testInstantiation() throws Exception {
+ // A new instance call keeps the default constructor.
+ Class mainClass = MainInstantiationSubClass.class;
+ runTest(
+ mainClass,
+ ImmutableList.of(mainClass, SuperClass.class, SubClass.class),
+ keepMainProguardConfiguration(mainClass),
+ this::checkAllClassesPresentWithDefaultConstructor);
+ }
+
+ @Test
+ public void testGetClass() throws Exception {
+ // Reference to the class constant keeps the default constructor.
+ Class mainClass = MainGetClassSubClass.class;
+ runTest(
+ mainClass,
+ ImmutableList.of(mainClass, SuperClass.class, SubClass.class),
+ keepMainProguardConfiguration(mainClass),
+ this::checkAllClassesPresentWithDefaultConstructor);
+ }
+
+ @Test
+ public void testClassForName() throws Exception {
+ // Class.forName with a constant string keeps the default constructor.
+ Class mainClass = MainClassForNameSubClass.class;
+ runTest(
+ mainClass,
+ ImmutableList.of(mainClass, SuperClass.class, SubClass.class),
+ keepMainProguardConfiguration(mainClass),
+ this::checkAllClassesPresentWithDefaultConstructor);
+ }
+
+ @Test
+ public void testStaticFieldWithoutInitializationStaticClassKept() throws Exception {
+ // An explicit keep rule keeps the default constructor.
+ Class mainClass = MainGetStaticFieldNotInitialized.class;
+ String proguardConfiguration = keepMainProguardConfiguration(
+ mainClass,
+ ImmutableList.of(
+ "-keep class " + getJavacGeneratedClassName(StaticFieldNotInitialized.class) + " {",
+ "}"));
+ runTest(
+ mainClass,
+ ImmutableList.of(mainClass, StaticFieldNotInitialized.class),
+ proguardConfiguration,
+ this::checkAllClassesPresentWithDefaultConstructor);
+ }
+
+ @Test
+ public void testStaticFieldWithInitializationStaticClassKept() throws Exception {
+ // An explicit keep rule keeps the default constructor.
+ Class mainClass = MainGetStaticFieldInitialized.class;
+ String proguardConfiguration = keepMainProguardConfiguration(
+ mainClass,
+ ImmutableList.of(
+ "-keep class " + getJavacGeneratedClassName(StaticFieldInitialized.class) + " {",
+ "}"));
+ runTest(
+ mainClass,
+ ImmutableList.of(mainClass, StaticFieldInitialized.class),
+ proguardConfiguration,
+ this::checkAllClassesPresentWithDefaultConstructor);
+ }
+
+ @Test
+ public void testStaticMethodStaticClassKept() throws Exception {
+ // An explicit keep rule keeps the default constructor.
+ Class mainClass = MainCallStaticMethod.class;
+ String proguardConfiguration = keepMainProguardConfiguration(
+ mainClass,
+ ImmutableList.of(
+ "-keep class " + getJavacGeneratedClassName(StaticMethod.class) + " {",
+ "}"));
+ runTest(
+ mainClass,
+ ImmutableList.of(mainClass, StaticMethod.class),
+ proguardConfiguration,
+ this::checkAllClassesPresentWithDefaultConstructor);
+ }
+
+ @Test
+ public void testStaticFieldWithoutInitialization() throws Exception {
+ Class mainClass = MainGetStaticFieldNotInitialized.class;
+ runTest(
+ mainClass,
+ ImmutableList.of(mainClass, StaticFieldNotInitialized.class),
+ keepMainProguardConfiguration(mainClass),
+ // TODO(74379749): Proguard does not keep the class with the un-initialized static field.
+ this::checkAllClassesPresentWithDefaultConstructor,
+ this::checkOnlyMainPresent);
+ }
+
+ @Test
+ public void testStaticFieldWithInitialization() throws Exception {
+ Class mainClass = MainGetStaticFieldInitialized.class;
+ runTest(
+ mainClass,
+ ImmutableList.of(mainClass, StaticFieldInitialized.class),
+ keepMainProguardConfiguration(mainClass),
+ // TODO(74233021): Proguard does not keep the default constructor for the class with the
+ // un-initialized static field.
+ this::checkAllClassesPresentWithDefaultConstructor,
+ this::checkAllClassesPresentOnlyMainWithDefaultConstructor);
+ }
+
+ @Test
+ public void testStaticMethodStaticClassNotKept() throws Exception {
+ // Due to inlining only the main method is left.
+ Class mainClass = MainCallStaticMethod.class;
+ runTest(
+ mainClass,
+ ImmutableList.of(mainClass, StaticMethod.class),
+ keepMainProguardConfiguration(mainClass),
+ this::checkOnlyMainPresent);
+ }
+
+ @Test
+ public void testStaticFieldWithoutInitializationWithoutInlining() throws Exception {
+ Class mainClass = MainGetStaticFieldNotInitialized.class;
+ runTest(
+ mainClass,
+ ImmutableList.of(mainClass, StaticFieldNotInitialized.class),
+ keepMainProguardConfiguration(mainClass, ImmutableList.of("-dontoptimize")),
+ // TODO(74233021): Proguard does not keep the default constructor for the class with the
+ // initialized static field.
+ this::checkAllClassesPresentWithDefaultConstructor,
+ this::checkAllClassesPresentOnlyMainWithDefaultConstructor);
+ }
+
+ @Test
+ public void testStaticFieldWithInitializationWithoutInlining() throws Exception {
+ Class mainClass = MainGetStaticFieldInitialized.class;
+ runTest(
+ mainClass,
+ ImmutableList.of(mainClass, StaticFieldInitialized.class),
+ keepMainProguardConfiguration(mainClass, ImmutableList.of("-dontoptimize")),
+ // TODO(74233021): Proguard does not keep the default constructor for the class with the
+ // un-initialized static field.
+ this::checkAllClassesPresentWithDefaultConstructor,
+ this::checkAllClassesPresentOnlyMainWithDefaultConstructor);
+ }
+
+ @Test
+ public void testStaticMethodStaticWithoutInlining() throws Exception {
+ Class mainClass = MainCallStaticMethod.class;
+ runTest(
+ mainClass,
+ ImmutableList.of(mainClass, StaticMethod.class),
+ keepMainProguardConfiguration(mainClass, ImmutableList.of("-dontoptimize")),
+ // TODO(74233021): Proguard does not keep the default constructor for the class with the
+ // initialized static field.
+ this::checkAllClassesPresentWithDefaultConstructor,
+ this::checkAllClassesPresentOnlyMainWithDefaultConstructor);
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/DexInspector.java b/src/test/java/com/android/tools/r8/utils/DexInspector.java
index 885ac3d..f0d5d37 100644
--- a/src/test/java/com/android/tools/r8/utils/DexInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/DexInspector.java
@@ -922,6 +922,11 @@
public DexEncodedField getField() {
return dexField;
}
+
+ @Override
+ public String toString() {
+ return dexField.toSourceString();
+ }
}
public class TypeSubject extends Subject {
diff --git a/src/test/java/com/android/tools/r8/utils/DexInspectorMatchers.java b/src/test/java/com/android/tools/r8/utils/DexInspectorMatchers.java
new file mode 100644
index 0000000..8e1db2c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/DexInspectorMatchers.java
@@ -0,0 +1,54 @@
+// Copyright (c) 2018, 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.utils;
+
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.google.common.collect.ImmutableList;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+public class DexInspectorMatchers {
+
+ public static Matcher<ClassSubject> isPresent() {
+ return new TypeSafeMatcher<ClassSubject>() {
+ @Override
+ public boolean matchesSafely(final ClassSubject clazz) {
+ return clazz.isPresent();
+ }
+
+ @Override
+ public void describeTo(final Description description) {
+ description.appendText("class present");
+ }
+
+ @Override
+ public void describeMismatchSafely(final ClassSubject clazz, Description description) {
+ description
+ .appendText("class ").appendValue(clazz.getOriginalName()).appendText(" was not");
+ }
+ };
+ }
+
+ public static Matcher<ClassSubject> hasDefaultConstructor() {
+ return new TypeSafeMatcher<ClassSubject>() {
+ @Override
+ public boolean matchesSafely(final ClassSubject clazz) {
+ return clazz.init(ImmutableList.of()).isPresent();
+ }
+
+ @Override
+ public void describeTo(final Description description) {
+ description.appendText("class having default constructor");
+ }
+
+ @Override
+ public void describeMismatchSafely(final ClassSubject clazz, Description description) {
+ description
+ .appendText("class ").appendValue(clazz.getOriginalName()).appendText(" did not");
+ }
+ };
+ }
+}