blob: 84b667589efae17c1f546ea11f3dd84b9fd70058 [file] [log] [blame]
// Copyright (c) 2017, 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;
import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
import static org.junit.Assert.assertEquals;
import com.android.tools.r8.D8Command.Builder;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.errors.InternalCompilerError;
import com.android.tools.r8.errors.Unimplemented;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.ir.desugar.InterfaceMethodRewriter;
import com.android.tools.r8.ir.desugar.LambdaRewriter;
import com.android.tools.r8.ir.desugar.TwrCloseResourceRewriter;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.OffOrAuto;
import com.beust.jcommander.internal.Lists;
import com.google.common.io.ByteStreams;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.UnaryOperator;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;
public abstract class D8IncrementalRunExamplesAndroidOTest
extends RunExamplesAndroidOTest<D8Command.Builder> {
abstract class D8IncrementalTestRunner extends TestRunner<D8IncrementalTestRunner> {
D8IncrementalTestRunner(String testName, String packageName, String mainClass) {
super(testName, packageName, mainClass);
}
@Override
D8IncrementalTestRunner withMinApiLevel(AndroidApiLevel minApiLevel) {
return withBuilderTransformation(builder -> builder.setMinApiLevel(minApiLevel.getLevel()));
}
@Override
void build(Path testJarFile, Path out, OutputMode mode) throws Throwable {
Map<String, ProgramResource> files = compileClassesTogether(testJarFile, null);
mergeClassFiles(Lists.newArrayList(files.values()), out, mode);
}
// Dex classes separately.
SortedMap<String, ProgramResource> compileClassesSeparately(Path testJarFile) throws Throwable {
TreeMap<String, ProgramResource> fileToResource = new TreeMap<>();
List<String> classFiles = collectClassFiles(testJarFile);
for (String classFile : classFiles) {
AndroidApp app =
compileClassFiles(
testJarFile, Collections.singletonList(classFile), null, OutputMode.DexIndexed);
assert app.getDexProgramResourcesForTesting().size() == 1;
fileToResource.put(
makeRelative(testJarFile, Paths.get(classFile)).toString(),
app.getDexProgramResourcesForTesting().get(0));
}
return fileToResource;
}
// Dex classes in one D8 invocation.
SortedMap<String, ProgramResource> compileClassesTogether(Path testJarFile, Path output)
throws Throwable {
TreeMap<String, ProgramResource> fileToResource = new TreeMap<>();
List<String> classFiles = collectClassFiles(testJarFile);
AndroidApp app =
compileClassFiles(testJarFile, classFiles, output, OutputMode.DexFilePerClassFile);
for (ProgramResource resource : app.getDexProgramResourcesForTesting()) {
Set<String> descriptors = resource.getClassDescriptors();
String mainClassDescriptor = app.getPrimaryClassDescriptor(resource);
Assert.assertNotNull(mainClassDescriptor);
for (String descriptor : descriptors) {
// classes are either lambda classes used by the main class, companion classes of the main
// interface, the main class/interface, or for JDK9, desugaring of try-with-resources.
Assert.assertTrue(descriptor.contains(LambdaRewriter.LAMBDA_CLASS_NAME_PREFIX)
|| descriptor.endsWith(InterfaceMethodRewriter.COMPANION_CLASS_NAME_SUFFIX + ";")
|| descriptor.endsWith(InterfaceMethodRewriter.DISPATCH_CLASS_NAME_SUFFIX + ";")
|| descriptor.equals(TwrCloseResourceRewriter.UTILITY_CLASS_DESCRIPTOR)
|| descriptor.equals(mainClassDescriptor));
}
String classDescriptor =
DescriptorUtils.getClassBinaryNameFromDescriptor(mainClassDescriptor);
String classFilePath = classDescriptor + ".class";
if (File.separatorChar != '/') {
classFilePath = classFilePath.replace('/', File.separatorChar);
}
fileToResource.put(classFilePath, resource);
}
return fileToResource;
}
private Path makeRelative(Path testJarFile, Path classFile) {
Path regularParent =
testJarFile.getParent().resolve(Paths.get("classes"));
Path legacyParent = regularParent.resolve(Paths.get("..",
regularParent.getFileName().toString() + "Legacy", "classes"));
if (classFile.startsWith(regularParent)) {
return regularParent.relativize(classFile);
}
Assert.assertTrue(classFile.startsWith(legacyParent));
return legacyParent.relativize(classFile);
}
private List<String> collectClassFiles(Path testJarFile) {
List<String> result = new ArrayList<>();
// Collect Java 8 classes.
collectClassFiles(getClassesRoot(testJarFile), result);
// Collect legacy classes.
collectClassFiles(getLegacyClassesRoot(testJarFile), result);
Collections.sort(result);
return result;
}
Path getClassesRoot(Path testJarFile) {
Path parent = testJarFile.getParent();
return parent.resolve(Paths.get("classes", packageName));
}
Path getLegacyClassesRoot(Path testJarFile) {
Path parent = testJarFile.getParent();
Path legacyPath = Paths.get("..",
parent.getFileName().toString() + "Legacy", "classes", packageName);
return parent.resolve(legacyPath);
}
private void collectClassFiles(Path dir, List<String> result) {
if (Files.exists(dir)) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
for (Path entry: stream) {
if (Files.isDirectory(entry)) {
collectClassFiles(entry, result);
} else {
result.add(entry.toString());
}
}
} catch (IOException x) {
throw new AssertionError(x);
}
}
}
AndroidApp compileClassFiles(
Path testJarFile, List<String> inputFiles, Path outputPath, OutputMode outputMode)
throws Throwable {
D8Command.Builder builder = D8Command.builder();
addClasspathReference(testJarFile, builder);
for (String inputFile : inputFiles) {
builder = builder.addProgramFiles(Paths.get(inputFile));
}
for (UnaryOperator<D8Command.Builder> transformation : builderTransformations) {
builder = transformation.apply(builder);
}
if (outputPath != null) {
builder.setOutput(outputPath, outputMode);
} else if (outputMode == OutputMode.DexIndexed) {
builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
} else if (outputMode == OutputMode.DexFilePerClassFile) {
builder.setProgramConsumer(DexFilePerClassFileConsumer.emptyConsumer());
} else {
throw new Unreachable("Unexpected output mode " + outputMode);
}
addLibraryReference(builder, ToolHelper.getAndroidJar(
androidJarVersion == null ? builder.getMinApiLevel() : androidJarVersion.getLevel()));
try {
return ToolHelper.runD8(builder, this::combinedOptionConsumer);
} catch (Unimplemented | CompilationError | InternalCompilerError re) {
throw re;
} catch (RuntimeException re) {
throw re.getCause() == null ? re : re.getCause();
}
}
AndroidApp mergeClassFiles(List<ProgramResource> dexFiles, Path out) throws Throwable {
return mergeClassFiles(dexFiles, out, OutputMode.DexIndexed);
}
AndroidApp mergeClassFiles(
List<ProgramResource> dexFiles, Path outputPath, OutputMode outputMode) throws Throwable {
Builder builder = D8Command.builder();
for (ProgramResource dexFile : dexFiles) {
builder.addDexProgramData(readResource(dexFile), dexFile.getOrigin());
}
for (UnaryOperator<Builder> transformation : builderTransformations) {
builder = transformation.apply(builder);
}
if (outputPath != null) {
builder.setOutput(outputPath, outputMode);
} else if (outputMode == OutputMode.DexIndexed) {
builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
} else if (outputMode == OutputMode.DexFilePerClassFile) {
builder.setProgramConsumer(DexFilePerClassFileConsumer.emptyConsumer());
} else {
throw new Unreachable("Unexpected output mode " + outputMode);
}
try {
AndroidApp app = ToolHelper.runD8(builder, this::combinedOptionConsumer);
assert app.getDexProgramResourcesForTesting().size() == 1;
return app;
} catch (Unimplemented | CompilationError | InternalCompilerError re) {
throw re;
} catch (RuntimeException re) {
throw re.getCause() == null ? re : re.getCause();
}
}
abstract void addClasspathReference(
Path testJarFile, D8Command.Builder builder) throws IOException;
abstract void addLibraryReference(Builder builder, Path location) throws IOException;
}
@Test
public void dexPerClassFileNoDesugaring() throws Throwable {
String testName = "dexPerClassFileNoDesugaring";
String testPackage = "incremental";
String mainClass = "IncrementallyCompiled";
Path inputJarFile = Paths.get(EXAMPLE_DIR, testPackage + JAR_EXTENSION);
D8IncrementalTestRunner test = test(testName, testPackage, mainClass);
Map<String, ProgramResource> compiledSeparately = test.compileClassesSeparately(inputJarFile);
Map<String, ProgramResource> compiledTogether = test.compileClassesTogether(inputJarFile, null);
Assert.assertEquals(compiledSeparately.size(), compiledTogether.size());
for (Map.Entry<String, ProgramResource> entry : compiledSeparately.entrySet()) {
ProgramResource otherResource = compiledTogether.get(entry.getKey());
Assert.assertNotNull(otherResource);
Assert.assertArrayEquals(readResource(entry.getValue()), readResource(otherResource));
}
AndroidApp mergedFromCompiledSeparately =
test.mergeClassFiles(Lists.newArrayList(compiledSeparately.values()), null);
AndroidApp mergedFromCompiledTogether =
test.mergeClassFiles(Lists.newArrayList(compiledTogether.values()), null);
// TODO(b/123504206): Add a main method and test the output runs.
Assert.assertArrayEquals(
readResource(mergedFromCompiledSeparately.getDexProgramResourcesForTesting().get(0)),
readResource(mergedFromCompiledTogether.getDexProgramResourcesForTesting().get(0)));
}
@Test
public void dexPerClassFileWithDesugaring() throws Throwable {
String testName = "dexPerClassFileWithDesugaring";
String testPackage = "lambdadesugaringnplus";
String mainClass = "LambdasWithStaticAndDefaultMethods";
Path inputJarFile = Paths.get(EXAMPLE_DIR, testPackage + JAR_EXTENSION);
D8IncrementalTestRunner test = test(testName, testPackage, mainClass);
test.withInterfaceMethodDesugaring(OffOrAuto.Auto);
AndroidApp mergedFromCompiledSeparately =
test.mergeClassFiles(
Lists.newArrayList(test.compileClassesSeparately(inputJarFile).values()), null);
AndroidApp mergedFromCompiledTogether =
test.mergeClassFiles(
Lists.newArrayList(test.compileClassesTogether(inputJarFile, null).values()), null);
Path out1 = temp.newFolder().toPath().resolve("out-together.zip");
mergedFromCompiledTogether.writeToZip(out1, OutputMode.DexIndexed);
ToolHelper.runArtNoVerificationErrors(out1.toString(), testPackage + "." + mainClass);
Path out2 = temp.newFolder().toPath().resolve("out-separate.zip");
mergedFromCompiledSeparately.writeToZip(out2, OutputMode.DexIndexed);
ToolHelper.runArtNoVerificationErrors(out2.toString(), testPackage + "." + mainClass);
Assert.assertArrayEquals(
readResource(mergedFromCompiledSeparately.getDexProgramResourcesForTesting().get(0)),
readResource(mergedFromCompiledTogether.getDexProgramResourcesForTesting().get(0)));
}
@Test
public void dexPerClassFileWithDispatchMethods() throws Throwable {
String testName = "dexPerClassFileWithDispatchMethods";
String testPackage = "interfacedispatchclasses";
String mainClass = "TestInterfaceDispatchClasses";
Path inputJarFile = Paths.get(EXAMPLE_DIR, testPackage + JAR_EXTENSION);
D8IncrementalTestRunner test = test(testName, testPackage, mainClass);
test.withInterfaceMethodDesugaring(OffOrAuto.Auto);
AndroidApp mergedFromCompiledSeparately =
test.mergeClassFiles(
Lists.newArrayList(test.compileClassesSeparately(inputJarFile).values()), null);
AndroidApp mergedFromCompiledTogether =
test.mergeClassFiles(
Lists.newArrayList(test.compileClassesTogether(inputJarFile, null).values()), null);
// TODO(b/123504206): This test throws an index out of bounds exception.
// Re-write or verify running fails in the expected way.
Assert.assertArrayEquals(
readResource(mergedFromCompiledSeparately.getDexProgramResourcesForTesting().get(0)),
readResource(mergedFromCompiledTogether.getDexProgramResourcesForTesting().get(0)));
}
@Test
public void dexPerClassFileOutputFiles() throws Throwable {
String testName = "dexPerClassFileNoDesugaring";
String testPackage = "incremental";
String mainClass = "IncrementallyCompiled";
Path out = temp.getRoot().toPath();
Path inputJarFile = Paths.get(EXAMPLE_DIR, testPackage + JAR_EXTENSION);
D8IncrementalTestRunner test = test(testName, testPackage, mainClass);
test.compileClassesTogether(inputJarFile, out);
String[] topLevelDir = out.toFile().list();
assert topLevelDir != null;
assertEquals(1, topLevelDir.length);
assertEquals("incremental", topLevelDir[0]);
String[] dexFiles = out.resolve(topLevelDir[0]).toFile().list();
assert dexFiles != null;
Arrays.sort(dexFiles);
String[] expectedFileNames = {
"IncrementallyCompiled$A$AB.dex",
"IncrementallyCompiled$A.dex",
"IncrementallyCompiled$B$BA.dex",
"IncrementallyCompiled$B.dex",
"IncrementallyCompiled$C.dex",
"IncrementallyCompiled.dex"
};
Arrays.sort(expectedFileNames);
Assert.assertArrayEquals(expectedFileNames, dexFiles);
}
@Override
abstract D8IncrementalTestRunner test(String testName, String packageName, String mainClass);
@Override
protected void testIntermediateWithMainDexList(String packageName, Path input,
int expectedMainDexListSize, String... mainDexClasses) throws Throwable {
// Skip those tests.
Assume.assumeTrue(false);
}
@Override
protected Path buildDexThroughIntermediate(String packageName, Path input, OutputMode outputMode,
AndroidApiLevel minApi, String... mainDexClasses) throws Throwable {
// tests using this should already been skipped.
throw new Unreachable();
}
static byte[] readResource(ProgramResource resource) throws IOException, ResourceException {
try (InputStream input = resource.getByteStream()) {
return ByteStreams.toByteArray(input);
}
}
}