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