Add base tests illustrating merging/inlining issues with DexSplitter
Extracted a new test base that we can use for testing the R8Splitter
Bug: 122902374
Change-Id: I549b113fb35a042857630fb21b73d63c77820989
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplication.java b/src/main/java/com/android/tools/r8/graph/DexApplication.java
index e87ab25..28c5878 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -102,8 +102,12 @@
public Iterable<DexProgramClass> classesWithDeterministicOrder() {
List<DexProgramClass> classes = new ArrayList<>(programClasses());
- // To keep the order deterministic, we sort the classes by their type, which is a unique key.
- classes.sort((a, b) -> a.type.slowCompareTo(b.type));
+ // We never actually sort by anything but the DexType, this is just here in case we ever change
+ // that.
+ if (options.testing.deterministicSortingBasedOnDexType) {
+ // To keep the order deterministic, we sort the classes by their type, which is a unique key.
+ classes.sort((a, b) -> a.type.slowCompareTo(b.type));
+ }
return classes;
}
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 51ea8a0..f459ced 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -981,6 +981,7 @@
public boolean enableStatefulLambdaCreateInstanceMethod = false;
public int basicBlockMuncherIterationLimit = NO_LIMIT;
public boolean dontReportFailingCheckDiscarded = false;
+ public boolean deterministicSortingBasedOnDexType = true;
// Flag to turn on/off JDK11+ nest-access control even when not required (Cf backend)
public boolean enableForceNestBasedAccessDesugaringForTest = false;
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 9d90bf7..9b8abe6 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -394,6 +394,11 @@
return jarTestClasses(Arrays.asList(classes), null);
}
+ /** Create a temporary JAR file containing the specified test classes. */
+ protected Path jarTestClasses(Iterable<Class<?>> classes) throws IOException {
+ return jarTestClasses(classes, null);
+ }
+
/** Create a temporary JAR file containing the specified test classes and data resources. */
protected Path jarTestClasses(Iterable<Class<?>> classes, List<DataResource> dataResources)
throws IOException {
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterInlineRegression.java b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterInlineRegression.java
new file mode 100644
index 0000000..1d26f28
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterInlineRegression.java
@@ -0,0 +1,97 @@
+// 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.dexsplitter;
+
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+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 DexSplitterInlineRegression extends SplitterTestBase {
+
+ public static final String EXPECTED = StringUtils.lines("42");
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection params() {
+ return getTestParameters().withAllRuntimes().build();
+ }
+
+ private final TestParameters parameters;
+
+ public DexSplitterInlineRegression(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void testInliningFromFeature() throws Exception {
+ Predicate<R8TestCompileResult> ensureGetFromFeatureGone =
+ r8TestCompileResult -> {
+ // Ensure that getFromFeature from FeatureClass is inlined into the run method.
+ try {
+ ClassSubject clazz = r8TestCompileResult.inspector().clazz(FeatureClass.class);
+ return clazz.uniqueMethodWithName("getFromFeature").isAbsent();
+ } catch (IOException | ExecutionException ex) {
+ throw new RuntimeException("Found getFromFeature in FeatureClass");
+ }
+ };
+ Consumer<R8FullTestBuilder> configurator =
+ r8FullTestBuilder -> r8FullTestBuilder.enableMergeAnnotations().noMinification();
+ ProcessResult processResult =
+ testDexSplitter(
+ parameters,
+ ImmutableSet.of(BaseSuperClass.class),
+ ImmutableSet.of(FeatureClass.class),
+ FeatureClass.class,
+ EXPECTED,
+ ensureGetFromFeatureGone,
+ configurator);
+ // We expect art to fail on this with the dex splitter, see b/122902374
+ assertNotEquals(processResult.exitCode, 0);
+ assertTrue(processResult.stderr.contains("NoClassDefFoundError"));
+ }
+
+ @NeverMerge
+ public abstract static class BaseSuperClass implements RunInterface {
+ public void run() {
+ System.out.println(getFromFeature());
+ }
+
+ public abstract String getFromFeature();
+ }
+
+ public static class FeatureClass extends BaseSuperClass {
+ String s;
+
+ public FeatureClass() {
+ if (System.currentTimeMillis() < 2) {
+ s = "43";
+ } else {
+ s = "42";
+ }
+ }
+
+ @Override
+ public String getFromFeature() {
+ return s;
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMergeRegression.java b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMergeRegression.java
new file mode 100644
index 0000000..9ea52d0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMergeRegression.java
@@ -0,0 +1,121 @@
+// 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.dexsplitter;
+
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+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 DexSplitterMergeRegression extends SplitterTestBase {
+
+ public static final String EXPECTED = StringUtils.lines("42", "foobar");
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection params() {
+ return getTestParameters().withAllRuntimes().build();
+ }
+
+ private final TestParameters parameters;
+
+ public DexSplitterMergeRegression(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void testInliningFromFeature() throws Exception {
+ // Static merging is based on sorting order, we assert that we merged to the feature.
+ Predicate<R8TestCompileResult> ensureMergingToFeature =
+ r8TestCompileResult -> {
+ try {
+ ClassSubject clazz = r8TestCompileResult.inspector().clazz(AFeatureWithStatic.class);
+ return clazz.allMethods().size() == 2
+ && clazz.uniqueMethodWithName("getBase42").isPresent()
+ && clazz.uniqueMethodWithName("getFoobar").isPresent();
+ } catch (IOException | ExecutionException ex) {
+ throw new RuntimeException("Failed lookup up AFeatureWithStatic");
+ }
+ };
+ Consumer<R8FullTestBuilder> configurator =
+ r8FullTestBuilder ->
+ r8FullTestBuilder
+ .enableMergeAnnotations()
+ .enableInliningAnnotations()
+ .noMinification()
+ .addOptionsModification(o -> o.testing.deterministicSortingBasedOnDexType = true);
+ ProcessResult processResult =
+ testDexSplitter(
+ parameters,
+ ImmutableSet.of(BaseClass.class, BaseWithStatic.class),
+ ImmutableSet.of(FeatureClass.class, AFeatureWithStatic.class),
+ FeatureClass.class,
+ EXPECTED,
+ ensureMergingToFeature,
+ configurator);
+ // We expect art to fail on this with the dex splitter.
+ assertNotEquals(processResult.exitCode, 0);
+ assertTrue(processResult.stderr.contains("NoClassDefFoundError"));
+ }
+
+ @NeverMerge
+ public static class BaseClass implements RunInterface {
+
+ @NeverInline
+ public void run() {
+ System.out.println(BaseWithStatic.getBase42());
+ }
+ }
+
+ public static class BaseWithStatic {
+
+ @NeverInline
+ public static int getBase42() {
+ if (System.currentTimeMillis() < 2) {
+ return 43;
+ } else {
+ return 42;
+ }
+ }
+ }
+
+ public static class FeatureClass extends BaseClass {
+
+ public void run() {
+ super.run();
+ System.out.println(AFeatureWithStatic.getFoobar());
+ }
+ }
+
+ // Name is important, see predicate in tests/
+ public static class AFeatureWithStatic {
+
+ @NeverInline
+ public static String getFoobar() {
+ if (System.currentTimeMillis() < 2) {
+ return "barFoo";
+ } else {
+ return "foobar";
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java b/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
new file mode 100644
index 0000000..05b8a84
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
@@ -0,0 +1,155 @@
+package com.android.tools.r8.dexsplitter;
+
+import static junit.framework.TestCase.assertTrue;
+
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+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.DexSplitter.Options;
+import com.google.common.collect.ImmutableList;
+import dalvik.system.PathClassLoader;
+import java.net.MalformedURLException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+public class SplitterTestBase extends TestBase {
+
+ // Compile the passed in classes plus RunInterface and SplitRunner using R8, then split
+ // based on the base/feature sets. toRun must implement the BaseRunInterface
+ protected ProcessResult testDexSplitter(
+ TestParameters parameters,
+ Set<Class<?>> baseClasses,
+ Set<Class<?>> featureClasses,
+ Class toRun,
+ String expectedOutput,
+ Predicate<R8TestCompileResult> predicate,
+ Consumer<R8FullTestBuilder> r8TestConfigurator)
+ throws Exception {
+ List<Class<?>> baseClassesWithRunner =
+ ImmutableList.<Class<?>>builder()
+ .add(RunInterface.class, SplitRunner.class)
+ .addAll(baseClasses)
+ .build();
+
+ Path baseJar = jarTestClasses(baseClassesWithRunner);
+ Path featureJar = jarTestClasses(featureClasses);
+
+ Path featureOnly =
+ testForR8(parameters.getBackend())
+ .addProgramClasses(featureClasses)
+ .addClasspathClasses(baseClasses)
+ .addClasspathClasses(RunInterface.class)
+ .addKeepAllClassesRule()
+ .setMinApi(parameters.getRuntime())
+ .compile()
+ .writeToZip();
+ if (parameters.isDexRuntime()) {
+ // With D8 this should just work. We compile all of the base classes, then run with the
+ // feature loaded at runtime. Since there is no inlining/class merging we don't
+ // have any issues.
+ testForD8()
+ .addProgramClasses(SplitRunner.class, RunInterface.class)
+ .addProgramClasses(baseClasses)
+ .setMinApi(parameters.getRuntime())
+ .compile()
+ .run(
+ parameters.getRuntime(),
+ SplitRunner.class,
+ toRun.getName(),
+ featureOnly.toAbsolutePath().toString())
+ .assertSuccessWithOutput(expectedOutput);
+ }
+
+ R8FullTestBuilder builder = testForR8(parameters.getBackend());
+ if (parameters.isCfRuntime()) {
+ // Compiling to jar we need to support the same way of loading code at runtime as
+ // android supports.
+ builder
+ .addProgramClasses(PathClassLoader.class)
+ .addKeepClassAndMembersRules(PathClassLoader.class);
+ }
+
+ R8FullTestBuilder r8FullTestBuilder =
+ builder
+ .setMinApi(parameters.getRuntime())
+ .addProgramClasses(SplitRunner.class, RunInterface.class)
+ .addProgramClasses(baseClasses)
+ .addProgramClasses(featureClasses)
+ .addKeepMainRule(SplitRunner.class)
+ .addKeepClassRules(toRun);
+ r8TestConfigurator.accept(r8FullTestBuilder);
+ R8TestCompileResult r8TestCompileResult = r8FullTestBuilder.compile();
+ assertTrue(predicate.test(r8TestCompileResult));
+ Path fullFiles = r8TestCompileResult.writeToZip();
+
+ // Ensure that we can run the program as a unit (i.e., without splitting)
+ r8TestCompileResult
+ .run(parameters.getRuntime(), SplitRunner.class, toRun.getName())
+ .assertSuccessWithOutput(expectedOutput);
+
+ Path splitterOutput = temp.newFolder().toPath();
+ Path splitterBaseDexFile = splitterOutput.resolve("base").resolve("classes.dex");
+ Path splitterFeatureDexFile = splitterOutput.resolve("feature").resolve("classes.dex");
+
+ Options options = new Options();
+ options.setOutput(splitterOutput.toString());
+ options.addBaseJar(baseJar.toString());
+ options.addFeatureJar(featureJar.toString(), "feature");
+
+ options.addInputArchive(fullFiles.toString());
+ DexSplitter.run(options);
+
+ ArtCommandBuilder commandBuilder = new ArtCommandBuilder();
+ commandBuilder.appendClasspath(splitterBaseDexFile.toString());
+ commandBuilder.appendProgramArgument(toRun.getName());
+ commandBuilder.appendProgramArgument(splitterFeatureDexFile.toString());
+ commandBuilder.setMainClass(SplitRunner.class.getName());
+ ProcessResult processResult = ToolHelper.runArtRaw(commandBuilder);
+ return processResult;
+ }
+
+ public interface RunInterface {
+ void run();
+ }
+
+ 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");
+ }
+ }
+ }
+}
diff --git a/src/test/java/dalvik/system/PathClassLoader.java b/src/test/java/dalvik/system/PathClassLoader.java
new file mode 100644
index 0000000..e629fae
--- /dev/null
+++ b/src/test/java/dalvik/system/PathClassLoader.java
@@ -0,0 +1,19 @@
+package dalvik.system;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+
+// Implement the equivalent of the PathClassLoader for CF backend. This simply forwards to the
+// URLClassLoader.
+public class PathClassLoader extends URLClassLoader {
+
+ public PathClassLoader(String dexPath, ClassLoader parent) throws MalformedURLException {
+ super(new URL[] {new File(dexPath).toURI().toURL()}, parent);
+ }
+
+ public PathClassLoader(URL[] urls, ClassLoader parent) {
+ super(urls, parent);
+ }
+}