| // 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 org.hamcrest.CoreMatchers.containsString; | |
| import static org.hamcrest.MatcherAssert.assertThat; | |
| import static org.junit.Assert.assertEquals; | |
| import static org.junit.Assert.assertFalse; | |
| import static org.junit.Assert.assertNotEquals; | |
| import static org.junit.Assert.assertNotNull; | |
| import static org.junit.Assert.assertTrue; | |
| import static org.junit.Assert.fail; | |
| 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.androidapi.AndroidApiDataAccess; | |
| import com.android.tools.r8.androidapi.AndroidApiModelingOptions; | |
| import com.android.tools.r8.androidresources.AndroidResourceTestingUtils; | |
| import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification; | |
| 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.references.Reference; | |
| import com.android.tools.r8.utils.AndroidApiLevel; | |
| import com.android.tools.r8.utils.ExtractMarkerUtils; | |
| import com.android.tools.r8.utils.InternalOptions; | |
| import com.android.tools.r8.utils.ThreadUtils; | |
| import com.android.tools.r8.utils.UncheckedApiLevel; | |
| import com.android.tools.r8.utils.ZipUtils; | |
| import com.android.tools.r8.utils.internal.FileUtils; | |
| import com.android.tools.r8.utils.internal.StringUtils; | |
| import com.google.common.collect.ImmutableList; | |
| import java.io.File; | |
| 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.function.Predicate; | |
| import java.util.zip.ZipEntry; | |
| import java.util.zip.ZipFile; | |
| import java.util.zip.ZipOutputStream; | |
| import org.junit.Test; | |
| import org.junit.rules.TemporaryFolder; | |
| 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(); | |
| } | |
| static class A {} | |
| static class B {} | |
| private Path getJarWithA() throws Exception { | |
| return writeClassesToJar(A.class); | |
| } | |
| private Path getJarWithB() throws Exception { | |
| return writeClassesToJar(B.class); | |
| } | |
| private Path getTestResources() throws Exception { | |
| Path resourceOutput = temp.newFile("base_resources.ap_").toPath(); | |
| AndroidResourceTestingUtils.writePrecompiledManifestAndResourcePB(resourceOutput); | |
| return resourceOutput; | |
| } | |
| private Path getFeatureTestResources(TemporaryFolder temp) throws Exception { | |
| Path resourceOutput = temp.newFile("feature_resources.ap_").toPath(); | |
| AndroidResourceTestingUtils.writePrecompiledManifestAndResourcePB(resourceOutput); | |
| return resourceOutput; | |
| } | |
| @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); | |
| assertFalse(command.getProguardCompatibility()); | |
| } | |
| @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 = getJarWithA(); | |
| Path library = ToolHelper.getDefaultAndroidJar(); | |
| Path output = working.resolve("classes.dex"); | |
| assertFalse(Files.exists(output)); | |
| ProcessResult result = | |
| ToolHelper.forkR8( | |
| working, | |
| input.toAbsolutePath().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 = getJarWithA(); | |
| Path inputFeature = getJarWithB(); | |
| 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.toAbsolutePath().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 passAndroidResources() throws Throwable { | |
| Path working = temp.getRoot().toPath(); | |
| Path input = getJarWithA(); | |
| Path library = ToolHelper.getDefaultAndroidJar(); | |
| Path output = working.resolve("classes.dex"); | |
| Path resourceInput = getTestResources(); | |
| Path resourceOutput = working.resolve("resources_out.ap_"); | |
| assertFalse(Files.exists(output)); | |
| assertFalse(Files.exists(resourceOutput)); | |
| ProcessResult result = | |
| ToolHelper.forkR8( | |
| working, | |
| input.toAbsolutePath().toString(), | |
| "--lib", | |
| library.toAbsolutePath().toString(), | |
| "--android-resources", | |
| resourceInput.toAbsolutePath().toString(), | |
| resourceOutput.toAbsolutePath().toString(), | |
| "--no-tree-shaking"); | |
| assertEquals("R8 run failed: " + result.stderr, 0, result.exitCode); | |
| assertTrue(Files.exists(output)); | |
| System.out.println(result.stdout); | |
| assertTrue(Files.exists(resourceOutput)); | |
| } | |
| @Test | |
| public void passFeatureResources() throws Throwable { | |
| Path working = temp.getRoot().toPath(); | |
| Path input = getJarWithA(); | |
| Path inputFeature = getJarWithB(); | |
| Path library = ToolHelper.getDefaultAndroidJar(); | |
| Path output = working.resolve("classes.dex"); | |
| Path featureOutput = working.resolve("feature.zip"); | |
| Path resourceInput = getTestResources(); | |
| Path resourceOutput = working.resolve("resources_out.ap_"); | |
| TemporaryFolder featureSplitTemp = ToolHelper.getTemporaryFolderForTest(); | |
| featureSplitTemp.create(); | |
| Path featureReasourceInput = getFeatureTestResources(featureSplitTemp); | |
| Path featureResourceOutput = working.resolve("feature_resources_out.ap_"); | |
| assertFalse(Files.exists(output)); | |
| assertFalse(Files.exists(featureOutput)); | |
| String pathSeparator = File.pathSeparator; | |
| ProcessResult result = | |
| ToolHelper.forkR8( | |
| working, | |
| input.toAbsolutePath().toString(), | |
| "--lib", | |
| library.toAbsolutePath().toString(), | |
| "--android-resources", | |
| resourceInput.toAbsolutePath().toString(), | |
| resourceOutput.toAbsolutePath().toString(), | |
| "--feature", | |
| inputFeature.toAbsolutePath() + pathSeparator + featureReasourceInput.toAbsolutePath(), | |
| featureOutput.toAbsolutePath() + pathSeparator + featureResourceOutput.toAbsolutePath(), | |
| "--no-tree-shaking"); | |
| assertEquals("R8 run failed: " + result.stderr, 0, result.exitCode); | |
| assertTrue(Files.exists(output)); | |
| assertTrue(Files.exists(featureOutput)); | |
| assertTrue(Files.exists(resourceOutput)); | |
| } | |
| @Test | |
| public void passResourceOnlyFeature() throws Throwable { | |
| Path working = temp.getRoot().toPath(); | |
| Path input = getJarWithA(); | |
| Path library = ToolHelper.getDefaultAndroidJar(); | |
| Path output = working.resolve("classes.dex"); | |
| Path resourceInput = getTestResources(); | |
| Path resourceOutput = working.resolve("resources_out.ap_"); | |
| TemporaryFolder featureSplitTemp = ToolHelper.getTemporaryFolderForTest(); | |
| featureSplitTemp.create(); | |
| Path featureReasourceInput = getFeatureTestResources(featureSplitTemp); | |
| Path featureResourceOutput = working.resolve("feature_resources_out.ap_"); | |
| assertFalse(Files.exists(output)); | |
| String pathSeparator = File.pathSeparator; | |
| ProcessResult result = | |
| ToolHelper.forkR8( | |
| working, | |
| input.toAbsolutePath().toString(), | |
| "--lib", | |
| library.toAbsolutePath().toString(), | |
| "--android-resources", | |
| resourceInput.toAbsolutePath().toString(), | |
| resourceOutput.toAbsolutePath().toString(), | |
| "--feature", | |
| pathSeparator + featureReasourceInput.toAbsolutePath(), | |
| pathSeparator + featureResourceOutput.toAbsolutePath(), | |
| "--no-tree-shaking"); | |
| assertEquals("R8 run failed: " + result.stderr, 0, result.exitCode); | |
| assertTrue(Files.exists(output)); | |
| assertTrue(Files.exists(featureResourceOutput)); | |
| } | |
| @Test | |
| public void featureOnlyOneArgument() throws Throwable { | |
| Path working = temp.getRoot().toPath(); | |
| Path input = getJarWithA(); | |
| Path inputFeature = getJarWithB(); | |
| 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); | |
| assertThat(result.stderr, containsString("Missing second argument for")); | |
| } | |
| @Test | |
| public void flagsFile() throws Throwable { | |
| Path working = temp.getRoot().toPath(); | |
| Path library = ToolHelper.getDefaultAndroidJar(); | |
| Path input = getJarWithA(); | |
| 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.toAbsolutePath().toString()); | |
| assertEquals(0, ToolHelper.forkR8(working, "@flags.txt").exitCode); | |
| assertTrue(Files.exists(output)); | |
| Collection<Marker> markers = ExtractMarkerUtils.extractMarkersFromFile(output); | |
| assertEquals(1, markers.size()); | |
| Marker marker = markers.iterator().next(); | |
| assertEquals(new UncheckedApiLevel(24, 0), marker.getMinApi()); | |
| 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 = getJarWithA(); | |
| 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 testHelpMessage() { | |
| assertEquals( | |
| StringUtils.lines( | |
| "Usage: r8 [options] [@<argfile>] <input-files>", | |
| " where <input-files> are any combination class, zip, or jar files", | |
| " and each <argfile> is a file containing additional arguments (one per line)", | |
| " and options are:", | |
| " --release # Compile without debugging information (default).", | |
| " --debug # Compile with debugging information.", | |
| " --dex # Compile program to DEX file format (default).", | |
| " --classfile # Compile program to Java classfile format.", | |
| " --output <path> # Output result in <path>. <path> must be a zip file or" | |
| + " an existing", | |
| " # directory.", | |
| " --lib <path> # Add <path> as a library resource. <path> must be a" | |
| + " jar/zip file or a JDK", | |
| " # home directory.", | |
| " --classpath <file> # Add <file> as a classpath resource.", | |
| " --min-api <number>[.<number>]", | |
| " # Minimum Android API level compatibility (default: 1).", | |
| " --api-database <file> # Use <file> as the Android API database for API modeling," | |
| + " overriding the", | |
| " # default database. <file> must be a .ser file generated by", | |
| " # ApiDatabaseGenerator (it is an unstable and not backwards" | |
| + " compatible", | |
| " # format).", | |
| " --pg-compat # Compile with R8 in Proguard compatibility mode.", | |
| " --pg-conf <file> # Proguard configuration <file>.", | |
| " --pg-conf-output <file> # Output the collective configuration to <file>.", | |
| " --pg-map <file> # Use <file> as a mapping file for distribution and" | |
| + " composition with output", | |
| " # mapping file.", | |
| " --pg-map-output <file> # Output the resulting name and line mapping to <file>.", | |
| " --partition-map-output <file>", | |
| " # Output the resulting mapping to <file>.", | |
| " --desugared-lib <file> # Specify desugared library configuration. <file> is a" | |
| + " desugared library", | |
| " # configuration (json).", | |
| " --no-tree-shaking # Force disable tree shaking of unreachable classes.", | |
| " --no-minification # Force disable minification of names.", | |
| " --no-data-resources # Ignore all data resources.", | |
| " --no-desugaring # Force disable desugaring.", | |
| " --main-dex-rules <file> # Proguard keep rules for classes to place in the primary" | |
| + " dex file.", | |
| " --main-dex-list <file> # List of classes to place in the primary dex file.", | |
| " --android-resources <input> <output>", | |
| " # Add android resource input and output to be used in" | |
| + " resource shrinking.", | |
| " # Both input and output must be specified.", | |
| " --android-resources-usage-log <file>", | |
| " # Write the resource shrinking usage log to <file>.", | |
| " --feature <input>[:|;<res-input>] <output>[:|;<res-output>]", | |
| " # Add feature <input> file to <output> file. Several" | |
| + " occurrences can map to", | |
| " # the same output. If <res-input> and <res-output> are" | |
| + " specified use these as", | |
| " # resource shrinker input and output. Separator is : on" | |
| + " linux/mac, ; on", | |
| " # windows. It is possible to supply resource only features" | |
| + " by using an empty", | |
| " # string for <input> and <output>, e.g." | |
| + " '--feature :in.ap_ :out.ap_'.", | |
| " --isolated-splits # Specifies that the application is using isolated splits," | |
| + " i.e., if split", | |
| " # APKs installed for this application are loaded into their" | |
| + " own Context", | |
| " # objects.", | |
| " --main-dex-list-output <file>", | |
| " # Output the full main-dex list in <file>.", | |
| " --force-enable-assertions[:[<class name>|<package name>...]]", | |
| " --force-ea[:[<class name>|<package name>...]]", | |
| " # Forcefully enable javac generated assertion code.", | |
| " --force-disable-assertions[:[<class name>|<package name>...]]", | |
| " --force-da[:[<class name>|<package name>...]]", | |
| " # Forcefully disable javac generated assertion code. This is" | |
| + " the default", | |
| " # handling of javac assertion code when generating DEX file" | |
| + " format.", | |
| " --force-passthrough-assertions[:[<class name>|<package name>...]]", | |
| " --force-pa[:[<class name>|<package name>...]]", | |
| " # Don't change javac generated assertion code. This is the" | |
| + " default handling", | |
| " # of javac assertion code when generating class file" | |
| + " format.", | |
| " --force-assertions-handler:<handler method>[:[<class name>|<package name>...]]", | |
| " --force-ah:<handler method>[:[<class name>|<package name>...]]", | |
| " # Change javac and kotlinc generated assertion code to" | |
| + " invoke the method", | |
| " # <handler method> with each assertion error instead of" | |
| + " throwing it. The", | |
| " # <handler method> is specified as a class name followed by" | |
| + " a dot and the", | |
| " # method name. The handler method must take a single" | |
| + " argument of type", | |
| " # java.lang.Throwable and have return type void.", | |
| " --thread-count <number> # Use <number> of threads. If not specified the number will" | |
| + " be based on", | |
| " # heuristics taking the number of cores into account.", | |
| " --map-diagnostics[:<type>] <from-level> <to-level>", | |
| " # Map diagnostics of <type> (default any) reported as" | |
| + " <from-level> to", | |
| " # <to-level> where <from-level> and <to-level> are one of" | |
| + " 'none', 'info',", | |
| " # 'warning', or 'error', and the optional <type> is either" | |
| + " the simple or", | |
| " # fully qualified Java type name of a diagnostic. If <type>" | |
| + " is unspecified,", | |
| " # all diagnostics at <from-level> will be mapped. Note that" | |
| + " fatal compiler", | |
| " # errors cannot be mapped.", | |
| " --map-id-template <template>", | |
| " # Set the map-id to <template>. The <template> can reference" | |
| + " the variables:", | |
| " # %MAP_HASH, compiler generated mapping hash.", | |
| " --source-file-template <template>", | |
| " # Set all source-file attributes to <template>. The" | |
| + " <template> can reference", | |
| " # the variables: %MAP_ID, map id (e.g., value of" | |
| + " --map-id-template).", | |
| " # %MAP_HASH, compiler generated mapping hash.", | |
| " --android-platform-build", | |
| " # Compile as a platform build where the" | |
| + " runtime/bootclasspath is assumed to", | |
| " # be the version specified by --min-api.", | |
| " --art-profile <input> <output>", | |
| " # Rewrite human readable ART profile read from <input> and" | |
| + " write to <output>.", | |
| " --startup-profile <file>", | |
| " # Startup profile <file> to use for dex layout.", | |
| " --build-metadata-output <file>", | |
| " # Output build metadata in <file>.", | |
| " --configuration-analysis-data-output <file>", | |
| " # Output configuration analysis data in <file>.", | |
| " --optimize-multidex-for-linearalloc", | |
| " # Optimize class distribution across DEX files for legacy" | |
| + " multidex.", | |
| " --dumpinputtofile <file>", | |
| " # Dump all compiler input to <file> for easy" | |
| + " reproduction.", | |
| " --dumpinputtodirectory <dir>", | |
| " # Dump all compiler input to <dir> for easy" | |
| + " reproduction.", | |
| " --version", | |
| " -v # Print version.", | |
| " --help", | |
| " -h # Print usage information."), | |
| R8CommandParser.getUsageMessage()); | |
| } | |
| @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 proguardCompatMode() throws Throwable { | |
| assertFalse(parse("").getProguardCompatibility()); | |
| assertTrue(parse("--pg-compat").getProguardCompatibility()); | |
| } | |
| @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.getMajor()) | |
| .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.getMajor()) | |
| .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 = getJarWithA(); | |
| 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() { | |
| return Collections.singleton( | |
| ProgramResource.fromFile(Kind.DEX, dexFile)); | |
| } | |
| }) | |
| .build())); | |
| } | |
| @Test | |
| public void dexDataUnsupported() { | |
| 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 = TestDeps.getExamplesPath("shaking1.jar"); | |
| 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 setPgConfOutputFlag() throws Throwable { | |
| Path file = temp.newFolder().toPath().resolve("output.conf"); | |
| R8Command command = parse("--pg-conf-output", file.toString()); | |
| InternalOptions options = command.getInternalOptions(); | |
| assertNotNull(options.configurationConsumer); | |
| } | |
| @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, Predicate<AssertionsConfiguration> x) { | |
| assertEquals(1, entries.size()); | |
| assertTrue(x.test(entries.get(0))); | |
| assertEquals(AssertionTransformationScope.ALL, entries.get(0).getScope()); | |
| } | |
| private void checkSingleForceClassAndPackageAssertion( | |
| List<AssertionsConfiguration> entries, Predicate<AssertionsConfiguration> x) { | |
| assertEquals(2, entries.size()); | |
| assertTrue(x.test(entries.get(0))); | |
| assertEquals(AssertionTransformationScope.CLASS, entries.get(0).getScope()); | |
| assertEquals("ClassName", entries.get(0).getValue()); | |
| assertTrue(x.test(entries.get(1))); | |
| assertEquals(AssertionTransformationScope.PACKAGE, entries.get(1).getScope()); | |
| assertEquals("PackageName", entries.get(1).getValue()); | |
| } | |
| private void checkSingleForceClassAndPackageAssertion( | |
| List<AssertionsConfiguration> entries, | |
| Predicate<AssertionsConfiguration> checkClass, | |
| Predicate<AssertionsConfiguration> checkPackage) { | |
| assertEquals(2, entries.size()); | |
| assertTrue(checkClass.test(entries.get(0))); | |
| assertEquals(AssertionTransformationScope.CLASS, entries.get(0).getScope()); | |
| assertEquals("ClassName", entries.get(0).getValue()); | |
| assertTrue(checkPackage.test(entries.get(1))); | |
| 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(), | |
| AssertionsConfiguration::isCompileTimeEnabled); | |
| checkSingleForceAllAssertion( | |
| parse("--force-disable-assertions").getAssertionsConfiguration(), | |
| AssertionsConfiguration::isCompileTimeDisabled); | |
| checkSingleForceAllAssertion( | |
| parse("--force-passthrough-assertions").getAssertionsConfiguration(), | |
| AssertionsConfiguration::isPassthrough); | |
| checkSingleForceClassAndPackageAssertion( | |
| parse("--force-enable-assertions:ClassName", "--force-enable-assertions:PackageName...") | |
| .getAssertionsConfiguration(), | |
| AssertionsConfiguration::isCompileTimeEnabled); | |
| checkSingleForceClassAndPackageAssertion( | |
| parse("--force-disable-assertions:ClassName", "--force-disable-assertions:PackageName...") | |
| .getAssertionsConfiguration(), | |
| AssertionsConfiguration::isCompileTimeDisabled); | |
| checkSingleForceClassAndPackageAssertion( | |
| parse( | |
| "--force-passthrough-assertions:ClassName", | |
| "--force-passthrough-assertions:PackageName...") | |
| .getAssertionsConfiguration(), | |
| AssertionsConfiguration::isPassthrough); | |
| checkSingleForceAllAssertion( | |
| parse("--force-assertions-handler:com.example.MyHandler.handler") | |
| .getAssertionsConfiguration(), | |
| configuration -> | |
| configuration.isAssertionHandler() | |
| && configuration | |
| .getAssertionHandler() | |
| .getHolderClass() | |
| .equals(Reference.classFromDescriptor("Lcom/example/MyHandler;")) | |
| && configuration.getAssertionHandler().getMethodName().equals("handler") | |
| && configuration | |
| .getAssertionHandler() | |
| .getMethodDescriptor() | |
| .equals("(Ljava/lang/Throwable;)V")); | |
| checkSingleForceClassAndPackageAssertion( | |
| parse( | |
| "--force-assertions-handler:com.example.MyHandler.handler1:ClassName", | |
| "--force-assertions-handler:com.example.MyHandler.handler2:PackageName...") | |
| .getAssertionsConfiguration(), | |
| configuration -> | |
| configuration.isAssertionHandler() | |
| && configuration | |
| .getAssertionHandler() | |
| .getHolderClass() | |
| .equals(Reference.classFromDescriptor("Lcom/example/MyHandler;")) | |
| && configuration.getAssertionHandler().getMethodName().equals("handler1") | |
| && configuration | |
| .getAssertionHandler() | |
| .getMethodDescriptor() | |
| .equals("(Ljava/lang/Throwable;)V"), | |
| configuration -> | |
| configuration.isAssertionHandler() | |
| && configuration | |
| .getAssertionHandler() | |
| .getHolderClass() | |
| .equals(Reference.classFromDescriptor("Lcom/example/MyHandler;")) | |
| && configuration.getAssertionHandler().getMethodName().equals("handler2") | |
| && configuration | |
| .getAssertionHandler() | |
| .getMethodDescriptor() | |
| .equals("(Ljava/lang/Throwable;)V")); | |
| } | |
| @Test(expected = CompilationFailedException.class) | |
| public void missingParameterForLastOption() throws CompilationFailedException { | |
| DiagnosticsChecker.checkErrorsContains( | |
| "Missing argument", handler -> parse(handler, "--output")); | |
| } | |
| @Test | |
| public void desugaredLibrary() throws CompilationFailedException, IOException { | |
| R8Command r8Command = | |
| parse( | |
| "--desugared-lib", | |
| LibraryDesugaringSpecification.JDK11.getSpecification().toString(), | |
| "--lib", | |
| ToolHelper.getAndroidJar(AndroidApiLevel.R).toString()); | |
| InternalOptions options = getOptionsWithLoadedDesugaredLibraryConfiguration(r8Command, false); | |
| assertFalse( | |
| options | |
| .getLibraryDesugaringOptions() | |
| .getMachineDesugaredLibrarySpecification() | |
| .getRewriteType() | |
| .isEmpty()); | |
| } | |
| @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) { | |
| 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"); | |
| } | |
| @Test | |
| public void defaultApiModelingState() throws Exception { | |
| AndroidApiModelingOptions options = parse("").getInternalOptions().apiModelingOptions(); | |
| assertTrue(options.isApiCallerIdentificationEnabled()); | |
| assertTrue(options.isOutliningOfMethodsEnabled()); | |
| assertTrue(options.isStubbingOfClassesEnabled()); | |
| } | |
| @Test | |
| public void androidPlatformBuildFlag() throws Exception { | |
| assertFalse(parse().getAndroidPlatformBuild()); | |
| assertTrue(parse("--android-platform-build").getAndroidPlatformBuild()); | |
| } | |
| @Test | |
| public void androidResourcesUsageLog() throws Throwable { | |
| Path resourcesInput = getTestResources(); | |
| Path resourcesOutput = temp.newFile("resources_out.ap_").toPath(); | |
| Path logOutput = temp.getRoot().toPath().resolve("usage_log.txt"); | |
| Path output = temp.newFile("output.zip").toPath(); | |
| R8Command command = | |
| parse( | |
| "--android-resources", | |
| resourcesInput.toString(), | |
| resourcesOutput.toString(), | |
| "--android-resources-usage-log", | |
| logOutput.toString(), | |
| "--output", | |
| output.toString()); | |
| R8.run(command); | |
| assertTrue(Files.exists(logOutput)); | |
| String logContent = FileUtils.readTextFile(logOutput, StandardCharsets.UTF_8); | |
| assertTrue(logContent.contains("string:app_name")); | |
| assertTrue(logContent.contains("reachable from AndroidManifest.xml")); | |
| } | |
| @Test(expected = CompilationFailedException.class) | |
| public void androidResourcesUsageLogWithoutResourcesError() throws Throwable { | |
| Path logOutput = temp.getRoot().toPath().resolve("usage_log.txt"); | |
| DiagnosticsChecker.checkErrorsContains( | |
| "--android-resources-usage-log requires --android-resources to be set.", | |
| handler -> parse(handler, "--android-resources-usage-log", logOutput.toString())); | |
| } | |
| @Test | |
| public void apiDatabaseFlag() throws Exception { | |
| Path apiDatabase = temp.newFile("api_database.ser").toPath(); | |
| R8Command command = parse("--api-database", apiDatabase.toString()); | |
| assertEquals(apiDatabase, command.getApiDatabasePath()); | |
| assertEquals(apiDatabase, command.getInternalOptions().apiModelingOptions().apiDatabasePath); | |
| } | |
| @Test | |
| public void apiDatabaseDoesNotExistError() throws Exception { | |
| Path apiDatabase = Paths.get("non_existent_api_database.ser"); | |
| R8Command command = parse("--api-database", apiDatabase.toString()); | |
| InternalOptions options = command.getInternalOptions(); | |
| DiagnosticsChecker handler = new DiagnosticsChecker(); | |
| AndroidApiDataAccess.create(options, handler); | |
| handler.checkErrorsContains("API database file does not exist: " + apiDatabase); | |
| } | |
| @Test | |
| public void apiDatabaseDuplicateError() throws Exception { | |
| Path apiDatabase = temp.newFile("api_database.ser").toPath(); | |
| TestDiagnosticMessagesImpl handler = new TestDiagnosticMessagesImpl(); | |
| try { | |
| parse( | |
| handler, | |
| "--api-database", | |
| apiDatabase.toString(), | |
| "--api-database", | |
| apiDatabase.toString()); | |
| fail("Expected failure"); | |
| } catch (CompilationFailedException e) { | |
| handler | |
| .assertOnlyErrors() | |
| .assertErrorsCount(1) | |
| .assertErrorMessageThatMatches( | |
| containsString("Cannot set multiple --api-database options")); | |
| } | |
| } | |
| @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(); | |
| } | |
| } |