blob: 0f0bc2f1d9c4875d440d3bbaa5ab0b78b2e4e715 [file] [log] [blame]
// Copyright (c) 2016, 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.ToolHelper.EXAMPLES_BUILD_DIR;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import com.android.tools.r8.ProgramResource.Kind;
import com.android.tools.r8.ToolHelper.ProcessResult;
import com.android.tools.r8.dex.Marker;
import com.android.tools.r8.dex.Marker.Tool;
import com.android.tools.r8.origin.EmbeddedOrigin;
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.ZipUtils;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
public class R8CommandTest {
@Rule
public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
@Test(expected = CompilationFailedException.class)
public void emptyBuilder() throws Throwable {
// The builder must have a program consumer.
R8Command.builder().build();
}
@Test
public void emptyCommand() throws Throwable {
verifyEmptyCommand(
// In the API we must set a consumer.
R8Command.builder().setProgramConsumer(DexIndexedConsumer.emptyConsumer()).build());
verifyEmptyCommand(parse());
verifyEmptyCommand(parse(""));
verifyEmptyCommand(parse("", ""));
verifyEmptyCommand(parse(" "));
verifyEmptyCommand(parse(" ", " "));
verifyEmptyCommand(parse("\t"));
verifyEmptyCommand(parse("\t", "\t"));
}
private void verifyEmptyCommand(R8Command command) throws Throwable {
assertEquals(0, ToolHelper.getApp(command).getDexProgramResourcesForTesting().size());
assertEquals(0, ToolHelper.getApp(command).getClassProgramResourcesForTesting().size());
assertFalse(command.getEnableMinification());
assertFalse(command.getEnableTreeShaking());
assertEquals(CompilationMode.RELEASE, command.getMode());
assertTrue(command.getProgramConsumer() instanceof DexIndexedConsumer);
}
@Test(expected = CompilationFailedException.class)
public void disallowDexFilePerClassFileBuilder() throws Throwable {
R8Command.builder().setProgramConsumer(DexFilePerClassFileConsumer.emptyConsumer()).build();
}
@Test
public void allowClassFileConsumer() throws Throwable {
assertTrue(
R8Command.builder()
.setProgramConsumer(ClassFileConsumer.emptyConsumer())
.build()
.getProgramConsumer()
instanceof ClassFileConsumer);
}
@Test
public void defaultOutIsCwd() throws Throwable {
Path working = temp.getRoot().toPath();
Path input = Paths.get(EXAMPLES_BUILD_DIR, "arithmetic.jar").toAbsolutePath();
Path library = ToolHelper.getDefaultAndroidJar();
Path output = working.resolve("classes.dex");
assertFalse(Files.exists(output));
ProcessResult result =
ToolHelper.forkR8(working, input.toString(), "--lib", library.toAbsolutePath().toString());
assertEquals("R8 run failed: " + result.stderr, 0, result.exitCode);
assertTrue(Files.exists(output));
}
@Test
public void flagsFile() throws Throwable {
Path working = temp.getRoot().toPath();
Path library = ToolHelper.getDefaultAndroidJar();
Path input = Paths.get(EXAMPLES_BUILD_DIR + "/arithmetic.jar").toAbsolutePath();
Path output = working.resolve("output.zip");
Path flagsFile = working.resolve("flags.txt");
FileUtils.writeTextFile(
flagsFile,
"--output",
"output.zip",
"--min-api",
"24",
"--lib",
library.toAbsolutePath().toString(),
input.toString());
assertEquals(0, ToolHelper.forkR8(working, "@flags.txt").exitCode);
assertTrue(Files.exists(output));
Marker marker = ExtractMarker.extractMarkerFromDexFile(output);
assertEquals(24, marker.getMinApi().intValue());
assertEquals(Tool.R8, marker.getTool());
}
@Test(expected=CompilationFailedException.class)
public void nonExistingFlagsFile() throws Throwable {
Path working = temp.getRoot().toPath();
Path flags = working.resolve("flags.txt").toAbsolutePath();
assertNotEquals(0, ToolHelper.forkR8(working, "@flags.txt").exitCode);
DiagnosticsChecker.checkErrorsContains("File not found", handler ->
R8.run(
R8Command.parse(
new String[] { "@" + flags.toString() },
EmbeddedOrigin.INSTANCE,
handler).build()));
}
@Test
public void printsHelpOnNoInput() throws Throwable {
ProcessResult result = ToolHelper.forkR8(temp.getRoot().toPath());
assertFalse(result.exitCode == 0);
assertTrue(result.stderr.contains("Usage"));
assertFalse(result.stderr.contains("R8_foobar")); // Sanity check
}
@Test
public void validOutputPath() throws Throwable {
Path existingDir = temp.getRoot().toPath();
Path nonExistingZip = existingDir.resolve("a-non-existing-archive.zip");
assertEquals(
existingDir,
getOutputPath(R8Command.builder().setOutput(existingDir, OutputMode.DexIndexed).build()));
assertEquals(
nonExistingZip,
getOutputPath(
R8Command.builder().setOutput(nonExistingZip, OutputMode.DexIndexed).build()));
assertEquals(existingDir, getOutputPath(parse("--output", existingDir.toString())));
assertEquals(nonExistingZip, getOutputPath(parse("--output", nonExistingZip.toString())));
}
static Path getOutputPath(BaseCompilerCommand command) {
ProgramConsumer consumer = command.getProgramConsumer();
if (consumer instanceof InternalProgramOutputPathConsumer) {
return ((InternalProgramOutputPathConsumer) consumer).internalGetOutputPath();
}
return null;
}
@Test
public void classFileOutputModeOption() throws Throwable {
assertTrue(parse("--classfile").getProgramConsumer() instanceof ClassFileConsumer);
}
@Test
public void classFileOutputModeAPI() throws Throwable {
assertTrue(
R8Command.builder()
.setOutput(Paths.get("."), OutputMode.ClassFile)
.build()
.getProgramConsumer()
instanceof ClassFileConsumer);
}
@Test
public void mainDexRules() throws Throwable {
Path mainDexRules1 = temp.newFile("main-dex-1.rules").toPath();
Path mainDexRules2 = temp.newFile("main-dex-2.rules").toPath();
parse("--main-dex-rules", mainDexRules1.toString());
parse("--main-dex-rules", mainDexRules1.toString(), "--main-dex-rules", mainDexRules2.toString());
}
@Test(expected = CompilationFailedException.class)
public void nonExistingMainDexRules() throws Throwable {
Path mainDexRules = temp.getRoot().toPath().resolve("main-dex.rules");
parse("--main-dex-rules", mainDexRules.toString());
}
@Test
public void mainDexList() throws Throwable {
Path mainDexList1 = temp.newFile("main-dex-list-1.txt").toPath();
Path mainDexList2 = temp.newFile("main-dex-list-2.txt").toPath();
parse("--main-dex-list", mainDexList1.toString());
parse("--main-dex-list", mainDexList1.toString(), "--main-dex-list", mainDexList2.toString());
}
@Test(expected = CompilationFailedException.class)
public void nonExistingMainDexList() throws Throwable {
Path mainDexList = temp.getRoot().toPath().resolve("main-dex-list.txt");
parse("--main-dex-list", mainDexList.toString());
}
@Test
public void mainDexListOutput() throws Throwable {
Path mainDexRules = temp.newFile("main-dex.rules").toPath();
Path mainDexList = temp.newFile("main-dex-list.txt").toPath();
Path mainDexListOutput = temp.newFile("main-dex-out.txt").toPath();
parse("--main-dex-rules", mainDexRules.toString(),
"--main-dex-list-output", mainDexListOutput.toString());
parse("--main-dex-list", mainDexList.toString(),
"--main-dex-list-output", mainDexListOutput.toString());
}
@Test(expected = CompilationFailedException.class)
public void mainDexListOutputWithoutAnyMainDexSpecification() throws Throwable {
Path mainDexListOutput = temp.newFile("main-dex-out.txt").toPath();
parse("--main-dex-list-output", mainDexListOutput.toString());
}
@Test
public void existingOutputDirWithDexFiles() throws Throwable {
Path existingDir = temp.newFolder().toPath();
List<Path> classesFiles = ImmutableList.of(
existingDir.resolve("classes.dex"),
existingDir.resolve("classes2.dex"),
existingDir.resolve("Classes3.dex"), // ignore case.
existingDir.resolve("classes10.dex"),
existingDir.resolve("classes999.dex"));
List<Path> otherFiles = ImmutableList.of(
existingDir.resolve("classes0.dex"),
existingDir.resolve("classes1.dex"),
existingDir.resolve("classes010.dex"),
existingDir.resolve("classesN.dex"),
existingDir.resolve("other.dex"));
for (Path file : classesFiles) {
Files.createFile(file);
assertTrue(Files.exists(file));
}
for (Path file : otherFiles) {
Files.createFile(file);
assertTrue(Files.exists(file));
}
Path input = Paths.get(EXAMPLES_BUILD_DIR, "arithmetic.jar");
ProcessResult result =
ToolHelper.forkR8(
Paths.get("."),
input.toString(),
"--output",
existingDir.toString(),
"--lib",
ToolHelper.getDefaultAndroidJar().toString());
assertEquals(0, result.exitCode);
assertTrue(Files.exists(classesFiles.get(0)));
for (int i = 1; i < classesFiles.size(); i++) {
Path file = classesFiles.get(i);
assertFalse("Expected stale file to be gone: " + file, Files.exists(file));
}
for (Path file : otherFiles) {
assertTrue("Expected non-classes file to remain: " + file, Files.exists(file));
}
}
@Test(expected = CompilationFailedException.class)
public void nonExistingOutputDir() throws Throwable {
Path nonExistingDir = temp.getRoot().toPath().resolve("a/path/that/does/not/exist");
R8Command.builder().setOutput(nonExistingDir, OutputMode.DexIndexed).build();
}
@Test
public void existingOutputZip() throws Throwable {
Path existingZip = temp.newFile("an-existing-archive.zip").toPath();
R8Command.builder().setOutput(existingZip, OutputMode.DexIndexed).build();
}
@Test(expected = CompilationFailedException.class)
public void invalidOutputFileType() throws Throwable {
Path invalidType = temp.getRoot().toPath().resolve("an-invalid-output-file-type.foobar");
R8Command.builder().setOutput(invalidType, OutputMode.DexIndexed).build();
}
@Test(expected = CompilationFailedException.class)
public void nonExistingOutputDirParse() throws Throwable {
Path nonExistingDir = temp.getRoot().toPath().resolve("a/path/that/does/not/exist");
parse("--output", nonExistingDir.toString());
}
@Test
public void existingOutputZipParse() throws Throwable {
Path existingZip = temp.newFile("an-existing-archive.zip").toPath();
parse("--output", existingZip.toString());
}
@Test(expected = CompilationFailedException.class)
public void invalidOutputFileTypeParse() throws Throwable {
Path invalidType = temp.getRoot().toPath().resolve("an-invalid-output-file-type.foobar");
parse("--output", invalidType.toString());
}
@Test
public void nonExistingOutputJar() throws Throwable {
Path nonExistingJar = temp.getRoot().toPath().resolve("non-existing-archive.jar");
R8Command.builder().setOutput(nonExistingJar, OutputMode.DexIndexed).build();
}
@Test(expected = CompilationFailedException.class)
public void dexFileUnsupported() throws Throwable {
Path dexFile = temp.newFile("test.dex").toPath();
DiagnosticsChecker.checkErrorsContains("DEX input", handler ->
R8Command
.builder(handler)
.setProgramConsumer(DexIndexedConsumer.emptyConsumer())
.addProgramFiles(dexFile)
.build());
}
@Test(expected = CompilationFailedException.class)
public void dexProviderUnsupported() throws Throwable {
Path dexFile = temp.newFile("test.dex").toPath();
DiagnosticsChecker.checkErrorsContains("DEX input", handler ->
R8.run(R8Command
.builder(handler)
.setProgramConsumer(DexIndexedConsumer.emptyConsumer())
.addProgramResourceProvider(new ProgramResourceProvider() {
@Override
public Collection<ProgramResource> getProgramResources() throws ResourceException {
return Collections.singleton(ProgramResource.fromFile(Kind.DEX, dexFile));
}
})
.build()));
}
@Test
public void dexDataUnsupported() throws Throwable {
for (Method method : R8Command.Builder.class.getMethods()) {
assertNotEquals("addDexProgramData", method.getName());
}
}
@Test(expected = CompilationFailedException.class)
public void vdexFileUnsupported() throws Throwable {
Path vdexFile = temp.newFile("test.vdex").toPath();
R8Command.builder()
.setProgramConsumer(DexIndexedConsumer.emptyConsumer())
.addProgramFiles(vdexFile)
.build();
}
@Test(expected = CompilationFailedException.class)
public void duplicateApiLevel() throws CompilationFailedException {
DiagnosticsChecker.checkErrorsContains(
"multiple --min-api", handler -> parse(handler, "--min-api", "19", "--min-api", "21"));
}
@Test(expected = CompilationFailedException.class)
public void invalidApiLevel() throws CompilationFailedException {
DiagnosticsChecker.checkErrorsContains(
"Invalid argument to --min-api", handler -> parse(handler, "--min-api", "foobar"));
}
@Test(expected = CompilationFailedException.class)
public void negativeApiLevel() throws CompilationFailedException {
DiagnosticsChecker.checkErrorsContains(
"Invalid argument to --min-api", handler -> parse(handler, "--min-api", "-21"));
}
@Test(expected = CompilationFailedException.class)
public void zeroApiLevel() throws CompilationFailedException {
DiagnosticsChecker.checkErrorsContains(
"Invalid argument to --min-api", handler -> parse(handler, "--min-api", "0"));
}
@Test
public void disableDesugaringCli() throws CompilationFailedException {
BaseCompilerCommandTest.assertDesugaringDisabled(parse("--no-desugaring"));
}
@Test
public void disableDesugaringApi() throws CompilationFailedException {
BaseCompilerCommandTest.assertDesugaringDisabled(R8Command.builder()
.setProgramConsumer(DexIndexedConsumer.emptyConsumer())
.setDisableDesugaring(true)
.build());
}
private ProcessResult runR8OnShaking1(Path additionalProguardConfiguration) throws Throwable {
Path input = Paths.get(EXAMPLES_BUILD_DIR, "shaking1.jar").toAbsolutePath();
Path proguardConfiguration =
Paths.get(ToolHelper.EXAMPLES_DIR, "shaking1", "keep-rules.txt").toAbsolutePath();
return ToolHelper.forkR8(temp.getRoot().toPath(),
"--pg-conf", proguardConfiguration.toString(),
"--pg-conf", additionalProguardConfiguration.toString(),
"--lib", ToolHelper.getDefaultAndroidJar().toAbsolutePath().toString(),
input.toString());
}
@Test
public void printsConfigurationOnStdout() throws Throwable {
Path proguardPrintConfigurationConfiguration =
temp.newFile("printconfiguration.txt").toPath().toAbsolutePath();
FileUtils.writeTextFile(
proguardPrintConfigurationConfiguration, ImmutableList.of("-printconfiguration"));
ProcessResult result = runR8OnShaking1(proguardPrintConfigurationConfiguration);
assertEquals("R8 run failed: " + result.stderr, 0, result.exitCode);
assertTrue(result.stdout.contains("-printconfiguration"));
}
@Test
public void printsPrintSeedsOnStdout() throws Throwable {
Path proguardPrintSeedsConfiguration = temp.newFile("printseeds.txt").toPath().toAbsolutePath();
FileUtils.writeTextFile(proguardPrintSeedsConfiguration, ImmutableList.of("-printseeds"));
ProcessResult result = runR8OnShaking1(proguardPrintSeedsConfiguration);
assertEquals("R8 run failed: " + result.stderr, 0, result.exitCode);
assertTrue(result.stdout.contains("void main(java.lang.String[])"));
}
@Test
public void printsPrintUsageOnStdout() throws Throwable {
Path proguardPrintUsageConfiguration = temp.newFile("printusage.txt").toPath().toAbsolutePath();
FileUtils.writeTextFile(proguardPrintUsageConfiguration, ImmutableList.of("-printusage"));
ProcessResult result = runR8OnShaking1(proguardPrintUsageConfiguration);
assertEquals("R8 run failed: " + result.stderr, 0, result.exitCode);
assertTrue(result.stdout.contains("shaking1.Unused"));
}
@Test
public void printsPrintSeedsAndPrintUsageOnStdout() throws Throwable {
Path proguardPrintSeedsConfiguration =
temp.newFile("printseedsandprintusage.txt").toPath().toAbsolutePath();
FileUtils.writeTextFile(
proguardPrintSeedsConfiguration, ImmutableList.of("-printseeds", "-printusage"));
ProcessResult result = runR8OnShaking1(proguardPrintSeedsConfiguration);
assertEquals("R8 run failed: " + result.stderr, 0, result.exitCode);
assertTrue(result.stdout.contains("void main(java.lang.String[])"));
assertTrue(result.stdout.contains("shaking1.Unused"));
}
@Test
public void printsPrintSeedsAndPrintUsageAndPrintConfigurationOnStdout() throws Throwable {
Path proguardPrintSeedsConfiguration =
temp.newFile("printseedsandprintusageandprintconfiguration.txt").toPath().toAbsolutePath();
FileUtils.writeTextFile(proguardPrintSeedsConfiguration,
ImmutableList.of("-printseeds", "-printusage", "-printconfiguration"));
ProcessResult result = runR8OnShaking1(proguardPrintSeedsConfiguration);
assertEquals("R8 run failed: " + result.stderr, 0, result.exitCode);
assertTrue(result.stdout.contains("void main(java.lang.String[])"));
assertTrue(result.stdout.contains("shaking1.Unused"));
assertTrue(result.stdout.contains("-printseeds"));
assertTrue(result.stdout.contains("-printusage"));
assertTrue(result.stdout.contains("-printconfiguration"));
}
@Test
public void noInputOutputsEmptyZip() throws CompilationFailedException, IOException {
Path emptyZip = temp.getRoot().toPath().resolve("empty.zip");
R8.run(
R8Command.builder()
.setOutput(emptyZip, OutputMode.DexIndexed)
.build());
assertTrue(Files.exists(emptyZip));
assertEquals(0, new ZipFile(emptyZip.toFile(), StandardCharsets.UTF_8).size());
}
private Path writeZipWithDataResource(String name) throws Exception {
Path dataResourceZip = temp.newFolder().toPath().resolve(name);
try (ZipOutputStream out =
new ZipOutputStream(
Files.newOutputStream(
dataResourceZip,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING))) {
// Write a directory entry and a normal entry.
ZipUtils.writeToZipStream(out, "org/", new byte[] {}, ZipEntry.STORED);
ZipUtils.writeToZipStream(
out, "org/resource.txt", "Hello world!".getBytes(), ZipEntry.STORED);
}
return dataResourceZip;
}
@Test
public void defaultResourceProcessing() throws Exception {
Path dataResourceZip = writeZipWithDataResource("dataResource.zip");
Path outputZip = temp.getRoot().toPath().resolve("output.zip");
R8.run(
R8Command.builder()
.addProgramFiles(dataResourceZip)
.setOutput(outputZip, OutputMode.ClassFile)
.build());
assertTrue(Files.exists(outputZip));
assertEquals(2, new ZipFile(outputZip.toFile(), StandardCharsets.UTF_8).size());
}
public void runCustomResourceProcessing(boolean includeDataResources, int expectedZipEntries)
throws Exception {
Path dataResourceZip = writeZipWithDataResource("dataResource.zip");
Path outputZip = temp.newFolder().toPath().resolve("output.zip");
R8.run(
R8Command.builder()
.addProgramFiles(dataResourceZip)
.setOutput(outputZip, OutputMode.ClassFile, includeDataResources)
.build());
assertTrue(Files.exists(outputZip));
assertEquals(
expectedZipEntries, new ZipFile(outputZip.toFile(), StandardCharsets.UTF_8).size());
}
private Path simpleProguardConfiguration() throws Exception {
Path proguardConfiguration = temp.newFile("printseedsandprintusage.txt").toPath();
FileUtils.writeTextFile(proguardConfiguration, ImmutableList.of("-keep class A { *; }"));
return proguardConfiguration;
}
@Test
public void noTreeShakingOption() throws Throwable {
// Default "keep all" rule implies no tree shaking.
assertFalse(parse().getEnableTreeShaking());
assertFalse(parse("--no-tree-shaking").getEnableTreeShaking());
// With a Proguard configuration --no-tree-shaking takes effect.
String proguardConfiguration = simpleProguardConfiguration().toAbsolutePath().toString();
assertTrue(parse("--pg-conf", proguardConfiguration).getEnableTreeShaking());
assertFalse(
parse("--no-tree-shaking", "--pg-conf", proguardConfiguration).getEnableTreeShaking());
}
@Test
public void noMinificationOption() throws Throwable {
// Default "keep all" rule implies no tree minification.
assertFalse(parse().getEnableMinification());
assertFalse(parse("--no-minification").getEnableMinification());
// With a Proguard configuration --no-tree-shaking takes effect.
String proguardConfiguration = simpleProguardConfiguration().toAbsolutePath().toString();
assertTrue(parse("--pg-conf", proguardConfiguration).getEnableMinification());
assertFalse(
parse("--no-minification", "--pg-conf", proguardConfiguration).getEnableMinification());
}
@Test
public void defaultDataResourcesOption() throws Throwable {
Path dataResourceZip = writeZipWithDataResource("dataResource.zip");
Path outputZip = temp.newFolder().toPath().resolve("output.zip");
R8.run(
parse(
dataResourceZip.toAbsolutePath().toString(),
"--output",
outputZip.toAbsolutePath().toString()));
assertTrue(Files.exists(outputZip));
assertEquals(2, new ZipFile(outputZip.toFile(), StandardCharsets.UTF_8).size());
}
@Test
public void noDataResourcesOption() throws Throwable {
Path dataResourceZip = writeZipWithDataResource("dataResource.zip");
Path outputZip = temp.newFolder().toPath().resolve("output.zip");
R8.run(
parse(
"--no-data-resources",
dataResourceZip.toAbsolutePath().toString(),
"--output",
outputZip.toAbsolutePath().toString()));
assertTrue(Files.exists(outputZip));
assertEquals(0, new ZipFile(outputZip.toFile(), StandardCharsets.UTF_8).size());
}
@Test
public void customResourceProcessing() throws Exception {
runCustomResourceProcessing(true, 2);
runCustomResourceProcessing(false, 0);
}
private R8Command parse(String... args) throws CompilationFailedException {
return R8Command.parse(args, EmbeddedOrigin.INSTANCE).build();
}
private R8Command parse(DiagnosticsHandler handler, String... args)
throws CompilationFailedException {
return R8Command.parse(args, EmbeddedOrigin.INSTANCE, handler).build();
}
}