|  | // 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.DiagnosticsMatcher.diagnosticMessage; | 
|  | import static com.android.tools.r8.ToolHelper.EXAMPLES_BUILD_DIR; | 
|  | import static org.hamcrest.CoreMatchers.containsString; | 
|  | import static org.junit.Assert.assertEquals; | 
|  | import static org.junit.Assert.assertFalse; | 
|  | import static org.junit.Assert.assertNotEquals; | 
|  | import static org.junit.Assert.assertSame; | 
|  | import static org.junit.Assert.assertTrue; | 
|  | import static org.junit.Assert.fail; | 
|  |  | 
|  | import com.android.tools.r8.AssertionsConfiguration.AssertionTransformation; | 
|  | import com.android.tools.r8.AssertionsConfiguration.AssertionTransformationScope; | 
|  | 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.origin.Origin; | 
|  | import com.android.tools.r8.utils.AndroidApiLevel; | 
|  | import com.android.tools.r8.utils.FileUtils; | 
|  | import com.android.tools.r8.utils.ThreadUtils; | 
|  | 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.Test; | 
|  | import org.junit.runner.RunWith; | 
|  | import org.junit.runners.Parameterized; | 
|  | import org.junit.runners.Parameterized.Parameters; | 
|  |  | 
|  | @RunWith(Parameterized.class) | 
|  | public class R8CommandTest extends CommandTestBase<R8Command> { | 
|  |  | 
|  | @Parameters(name = "{0}") | 
|  | public static TestParametersCollection data() { | 
|  | return getTestParameters().withNoneRuntime().build(); | 
|  | } | 
|  |  | 
|  | public R8CommandTest(TestParameters parameters) { | 
|  | parameters.assertNoneRuntime(); | 
|  | } | 
|  |  | 
|  | @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()); | 
|  | assertTrue(command.getEnableMinification()); | 
|  | assertTrue(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 desugaredLibraryKeepRuleConsumer() throws Exception { | 
|  | StringConsumer stringConsumer = StringConsumer.emptyConsumer(); | 
|  | D8Command command = | 
|  | D8Command.builder() | 
|  | .setProgramConsumer(DexIndexedConsumer.emptyConsumer()) | 
|  | .setDesugaredLibraryKeepRuleConsumer(stringConsumer) | 
|  | .build(); | 
|  | assertSame(command.getInternalOptions().desugaredLibraryKeepRuleConsumer, stringConsumer); | 
|  | } | 
|  |  | 
|  | @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(), | 
|  | "--no-tree-shaking"); | 
|  | assertEquals("R8 run failed: " + result.stderr, 0, result.exitCode); | 
|  | assertTrue(Files.exists(output)); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void passFeatureSplit() throws Throwable { | 
|  | Path working = temp.getRoot().toPath(); | 
|  | Path input = Paths.get(EXAMPLES_BUILD_DIR, "arithmetic.jar").toAbsolutePath(); | 
|  | Path inputFeature = Paths.get(EXAMPLES_BUILD_DIR, "arrayaccess.jar").toAbsolutePath(); | 
|  | Path library = ToolHelper.getDefaultAndroidJar(); | 
|  | Path output = working.resolve("classes.dex"); | 
|  | Path featureOutput = working.resolve("feature.zip"); | 
|  | assertFalse(Files.exists(output)); | 
|  | assertFalse(Files.exists(featureOutput)); | 
|  | ProcessResult result = | 
|  | ToolHelper.forkR8( | 
|  | working, | 
|  | input.toString(), | 
|  | "--lib", | 
|  | library.toAbsolutePath().toString(), | 
|  | "--feature", | 
|  | inputFeature.toAbsolutePath().toString(), | 
|  | featureOutput.toAbsolutePath().toString(), | 
|  | "--no-tree-shaking"); | 
|  | assertEquals("R8 run failed: " + result.stderr, 0, result.exitCode); | 
|  | assertTrue(Files.exists(output)); | 
|  | assertTrue(Files.exists(featureOutput)); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void featureOnlyOneArgument() throws Throwable { | 
|  | Path working = temp.getRoot().toPath(); | 
|  | Path input = Paths.get(EXAMPLES_BUILD_DIR, "arithmetic.jar").toAbsolutePath(); | 
|  | Path inputFeature = Paths.get(EXAMPLES_BUILD_DIR, "arrayaccess.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(), | 
|  | "--no-tree-shaking", | 
|  | "--feature", | 
|  | inputFeature.toAbsolutePath().toString()); | 
|  | assertNotEquals("R8 run failed: " + result.stderr, 0, result.exitCode); | 
|  | assertTrue(result.stderr.contains("Missing parameter for")); | 
|  | } | 
|  |  | 
|  | @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(), | 
|  | "--no-tree-shaking", | 
|  | input.toString()); | 
|  | assertEquals(0, ToolHelper.forkR8(working, "@flags.txt").exitCode); | 
|  | assertTrue(Files.exists(output)); | 
|  | Collection<Marker> markers = ExtractMarker.extractMarkerFromDexFile(output); | 
|  | assertEquals(1, markers.size()); | 
|  | Marker marker = markers.iterator().next(); | 
|  | 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( | 
|  | "NoSuchFileException", | 
|  | handler -> | 
|  | R8.run( | 
|  | R8Command.parse( | 
|  | new String[] {"@" + flags.toString()}, EmbeddedOrigin.INSTANCE, handler) | 
|  | .build())); | 
|  | } | 
|  |  | 
|  | @Test(expected = CompilationFailedException.class) | 
|  | public void recursiveFlagsFile() throws Throwable { | 
|  | Path working = temp.getRoot().toPath(); | 
|  | Path flagsFile = working.resolve("flags.txt"); | 
|  | Path recursiveFlagsFile = working.resolve("recursive_flags.txt"); | 
|  | Path input = Paths.get(EXAMPLES_BUILD_DIR + "/arithmetic.jar").toAbsolutePath(); | 
|  | FileUtils.writeTextFile(recursiveFlagsFile, "--output", "output.zip"); | 
|  | FileUtils.writeTextFile( | 
|  | flagsFile, "--min-api", "24", input.toString(), "@" + recursiveFlagsFile); | 
|  | DiagnosticsChecker.checkErrorsContains( | 
|  | "Recursive @argfiles are not supported", | 
|  | handler -> | 
|  | R8.run( | 
|  | R8Command.parse( | 
|  | new String[] {"@" + flagsFile.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(expected = CompilationFailedException.class) | 
|  | public void mainDexRulesWithNonLegacyMinApi() throws Throwable { | 
|  | Path mainDexRules = temp.newFile("main-dex.rules").toPath(); | 
|  | DiagnosticsChecker.checkErrorsContains( | 
|  | "does not support main-dex", | 
|  | (handler) -> | 
|  | R8Command.builder(handler) | 
|  | .setProgramConsumer(DexIndexedConsumer.emptyConsumer()) | 
|  | .setMinApiLevel(AndroidApiLevel.L.getLevel()) | 
|  | .addMainDexRulesFiles(mainDexRules) | 
|  | .build()); | 
|  | } | 
|  |  | 
|  | @Test(expected = CompilationFailedException.class) | 
|  | public void mainDexListWithNonLegacyMinApi() throws Throwable { | 
|  | Path mainDexList = temp.newFile("main-dex-list.txt").toPath(); | 
|  | DiagnosticsChecker.checkErrorsContains( | 
|  | "does not support main-dex", | 
|  | (handler) -> | 
|  | R8Command.builder(handler) | 
|  | .setProgramConsumer(DexIndexedConsumer.emptyConsumer()) | 
|  | .setMinApiLevel(AndroidApiLevel.L.getLevel()) | 
|  | .addMainDexListFiles(mainDexList) | 
|  | .build()); | 
|  | } | 
|  |  | 
|  | @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("."), | 
|  | "--no-tree-shaking", | 
|  | "--no-minification", | 
|  | 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(1, new ZipFile(outputZip.toFile(), StandardCharsets.UTF_8).size()); | 
|  | } | 
|  |  | 
|  | public void runCustomResourceProcessing( | 
|  | boolean includeDataResources, boolean keepDirectories, 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) | 
|  | .addProguardConfiguration( | 
|  | ImmutableList.of(keepDirectories ? "-keepdirectories" : ""), Origin.unknown()) | 
|  | .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. | 
|  | assertTrue(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. | 
|  | assertTrue(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(1, 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, true, 2); | 
|  | runCustomResourceProcessing(true, false, 1); | 
|  | runCustomResourceProcessing(false, false, 0); | 
|  | } | 
|  |  | 
|  | private void checkSingleForceAllAssertion( | 
|  | List<AssertionsConfiguration> entries, AssertionTransformation transformation) { | 
|  | assertEquals(1, entries.size()); | 
|  | assertEquals(transformation, entries.get(0).getTransformation()); | 
|  | assertEquals(AssertionTransformationScope.ALL, entries.get(0).getScope()); | 
|  | } | 
|  |  | 
|  | private void checkSingleForceClassAndPackageAssertion( | 
|  | List<AssertionsConfiguration> entries, AssertionTransformation transformation) { | 
|  | assertEquals(2, entries.size()); | 
|  | assertEquals(transformation, entries.get(0).getTransformation()); | 
|  | assertEquals(AssertionTransformationScope.CLASS, entries.get(0).getScope()); | 
|  | assertEquals("ClassName", entries.get(0).getValue()); | 
|  | assertEquals(transformation, entries.get(1).getTransformation()); | 
|  | assertEquals(AssertionTransformationScope.PACKAGE, entries.get(1).getScope()); | 
|  | assertEquals("PackageName", entries.get(1).getValue()); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void forceAssertionOption() throws Exception { | 
|  | checkSingleForceAllAssertion( | 
|  | parse("--force-enable-assertions").getAssertionsConfiguration(), | 
|  | AssertionTransformation.ENABLE); | 
|  | checkSingleForceAllAssertion( | 
|  | parse("--force-disable-assertions").getAssertionsConfiguration(), | 
|  | AssertionTransformation.DISABLE); | 
|  | checkSingleForceAllAssertion( | 
|  | parse("--force-passthrough-assertions").getAssertionsConfiguration(), | 
|  | AssertionTransformation.PASSTHROUGH); | 
|  | checkSingleForceClassAndPackageAssertion( | 
|  | parse("--force-enable-assertions:ClassName", "--force-enable-assertions:PackageName...") | 
|  | .getAssertionsConfiguration(), | 
|  | AssertionTransformation.ENABLE); | 
|  | checkSingleForceClassAndPackageAssertion( | 
|  | parse("--force-disable-assertions:ClassName", "--force-disable-assertions:PackageName...") | 
|  | .getAssertionsConfiguration(), | 
|  | AssertionTransformation.DISABLE); | 
|  | checkSingleForceClassAndPackageAssertion( | 
|  | parse( | 
|  | "--force-passthrough-assertions:ClassName", | 
|  | "--force-passthrough-assertions:PackageName...") | 
|  | .getAssertionsConfiguration(), | 
|  | AssertionTransformation.PASSTHROUGH); | 
|  | } | 
|  |  | 
|  | @Test(expected = CompilationFailedException.class) | 
|  | public void missingParameterForLastOption() throws CompilationFailedException { | 
|  | DiagnosticsChecker.checkErrorsContains( | 
|  | "Missing parameter", handler -> parse(handler, "--output")); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void desugaredLibrary() throws CompilationFailedException { | 
|  | R8Command r8Command = parse("--desugared-lib", "src/library_desugar/desugar_jdk_libs.json"); | 
|  | assertFalse( | 
|  | r8Command.getInternalOptions().desugaredLibraryConfiguration.getRewritePrefix().isEmpty()); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void desugaredLibraryWithOutputConf() throws CompilationFailedException { | 
|  | Path pgout = temp.getRoot().toPath().resolve("pgout.conf"); | 
|  | R8Command r8Command = | 
|  | parse( | 
|  | "--desugared-lib", | 
|  | "src/library_desugar/desugar_jdk_libs.json", | 
|  | "--desugared-lib-pg-conf-output", | 
|  | pgout.toString()); | 
|  | assertFalse( | 
|  | r8Command.getInternalOptions().desugaredLibraryConfiguration.getRewritePrefix().isEmpty()); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void desugaredLibraryWithOutputConfMissingArg() { | 
|  | TestDiagnosticMessagesImpl diagnostics = new TestDiagnosticMessagesImpl(); | 
|  | try { | 
|  | parse( | 
|  | diagnostics, | 
|  | "--desugared-lib", | 
|  | "src/library_desugar/desugar_jdk_libs.json", | 
|  | "--desugared-lib-pg-conf-output"); | 
|  | fail("Expected parse error"); | 
|  | } catch (CompilationFailedException e) { | 
|  | diagnostics.assertErrorsMatch(diagnosticMessage(containsString("Missing parameter"))); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void numThreadsOption() throws Exception { | 
|  | assertEquals(ThreadUtils.NOT_SPECIFIED, parse().getThreadCount()); | 
|  | assertEquals(1, parse("--thread-count", "1").getThreadCount()); | 
|  | assertEquals(2, parse("--thread-count", "2").getThreadCount()); | 
|  | assertEquals(10, parse("--thread-count", "10").getThreadCount()); | 
|  | } | 
|  |  | 
|  | private void numThreadsOptionInvalid(String value) throws Exception { | 
|  | final String expectedErrorContains = "Invalid argument to --thread-count"; | 
|  | try { | 
|  | DiagnosticsChecker.checkErrorsContains( | 
|  | expectedErrorContains, handler -> parse(handler, "--thread-count", value)); | 
|  | fail("Expected failure"); | 
|  | } catch (CompilationFailedException e) { | 
|  | // Expected. | 
|  | } | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void numThreadsOptionInvalid() throws Exception { | 
|  | numThreadsOptionInvalid("0"); | 
|  | numThreadsOptionInvalid("-1"); | 
|  | numThreadsOptionInvalid("two"); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | String[] requiredArgsForTest() { | 
|  | return new String[0]; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | R8Command parse(String... args) throws CompilationFailedException { | 
|  | return R8Command.parse(args, EmbeddedOrigin.INSTANCE).build(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | R8Command parse(DiagnosticsHandler handler, String... args) throws CompilationFailedException { | 
|  | return R8Command.parse(args, EmbeddedOrigin.INSTANCE, handler).build(); | 
|  | } | 
|  | } |