Add a test checking D8 API binary compatibility

When changing source in
src/test/apiUsageSample/com/android/tools/apiusagesample, a manual
compilation of the corresponding jar is necessary:
./tools/gradle.py buildApiUsageSample
This is needed to allow the test to detect incompatible changes that
would be solved by a re-compilation of the client classes.

Change-Id: If4c15bf4472bb2d2846925730471cadad22391f3
diff --git a/build.gradle b/build.gradle
index 61360dc..c0a183b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -77,6 +77,12 @@
             ]
         }
     }
+    apiUsageSample {
+        java {
+            srcDirs = ['src/test/apiUsageSample']
+        }
+        output.resourcesDir = 'build/classes/apiUsageSample'
+    }
     debugTestResources {
         java {
             srcDirs = ['src/test/debugTestResources']
@@ -191,6 +197,7 @@
     supportLibs 'com.android.support:support-v4:25.4.0'
     supportLibs 'junit:junit:4.12'
     supportLibs 'com.android.support.test.espresso:espresso-core:3.0.0'
+    apiUsageSampleCompile sourceSets.main.output
     debugTestResourcesKotlinCompileOnly 'org.jetbrains.kotlin:kotlin-stdlib:1.1.4-3'
     apt 'com.google.auto.value:auto-value:1.5'
 }
@@ -630,6 +637,12 @@
     dependsOn createJctfTests
 }
 
+task buildApiUsageSample(type: Jar) {
+    from sourceSets.apiUsageSample.output
+    baseName 'api_usage_sample'
+    destinationDir file('tests')
+}
+
 task buildDebugInfoExamplesDex {
     def examplesDir = file("src/test/java")
     def hostJar = "debuginfo_examples.jar"
diff --git a/src/test/apiUsageSample/com/android/tools/apiusagesample/CachingArchiveClassFileProvider.java b/src/test/apiUsageSample/com/android/tools/apiusagesample/CachingArchiveClassFileProvider.java
new file mode 100644
index 0000000..859876d
--- /dev/null
+++ b/src/test/apiUsageSample/com/android/tools/apiusagesample/CachingArchiveClassFileProvider.java
@@ -0,0 +1,40 @@
+// 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.apiusagesample;
+
+import com.android.tools.r8.ArchiveClassFileProvider;
+import com.android.tools.r8.ClassFileResourceProvider;
+import com.android.tools.r8.Resource;
+import com.android.tools.r8.utils.DirectoryClassFileProvider;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class CachingArchiveClassFileProvider extends ArchiveClassFileProvider {
+
+  private ConcurrentHashMap<String, Resource> resources = new ConcurrentHashMap<>();
+
+  private CachingArchiveClassFileProvider(Path archive) throws IOException {
+    super(archive);
+  }
+
+  @Override
+  public Resource getResource(String descriptor) {
+    return resources.computeIfAbsent(descriptor, desc -> super.getResource(desc));
+  }
+
+  public static ClassFileResourceProvider getProvider(Path entry)
+      throws IOException {
+    if (Files.isRegularFile(entry)) {
+      return new CachingArchiveClassFileProvider(entry);
+    } else if (Files.isDirectory(entry)) {
+      return DirectoryClassFileProvider.fromDirectory(entry);
+    } else {
+      throw new FileNotFoundException(entry.toString());
+    }
+  }
+}
diff --git a/src/test/apiUsageSample/com/android/tools/apiusagesample/D8Compiler.java b/src/test/apiUsageSample/com/android/tools/apiusagesample/D8Compiler.java
new file mode 100644
index 0000000..ff61567
--- /dev/null
+++ b/src/test/apiUsageSample/com/android/tools/apiusagesample/D8Compiler.java
@@ -0,0 +1,136 @@
+// 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.apiusagesample;
+
+import com.android.tools.r8.CompilationException;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.D8;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.utils.OutputMode;
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class D8Compiler {
+  private int minSdkVersion;
+  private Path bootclasspath;
+  private List<Path> classpath;
+  private static ExecutorService pool = Executors.newFixedThreadPool(4);
+
+  private D8Compiler(int minSdkVersion, Path bootclasspath, List<Path> classpath) {
+    this.minSdkVersion = minSdkVersion;
+    this.bootclasspath = bootclasspath;
+    this.classpath = classpath;
+  }
+
+  /**
+   * java ...Compiler output input minSdkVersion mainDexClasses bootclasspath [classpathEntries]+
+   */
+  public static void main(String[] args) throws Throwable {
+    try {
+      int argIndex = 0;
+      Path outputDir = Paths.get(args[argIndex++]);
+      Path input = Paths.get(args[argIndex++]);
+      int minSdkVersion = Integer.parseInt(args[argIndex++]);
+      Path mainDexClasses = Paths.get(args[argIndex++]);
+      Path bootclasspath = Paths.get(args[argIndex++]);
+
+      List<Path> classpath = new ArrayList<>(args.length - argIndex);
+      while (argIndex < args.length) {
+        classpath.add(Paths.get(args[argIndex++]));
+      }
+
+      D8Compiler compiler = new D8Compiler(minSdkVersion, bootclasspath, classpath);
+
+      List<Path> toMerge = new ArrayList<>(3);
+
+      int intermediateIndex = 0;
+      for (Path entry : classpath) {
+        Path output = outputDir.resolve(entry.getFileName() + "." + (intermediateIndex++));
+        Files.createDirectory(output);
+        toMerge.add(output);
+        compiler.compile(output, entry);
+      }
+
+      Path output = outputDir.resolve("main." + (intermediateIndex++));
+      Files.createDirectory(output);
+      toMerge.add(output);
+      compiler.compile(output, input);
+
+      compiler.merge(outputDir, mainDexClasses, toMerge);
+    } finally {
+      // Terminate pool threads to prevent the VM to wait on then before exiting.
+      pool.shutdown();
+    }
+  }
+
+  private void compile(Path output, Path input) throws Throwable {
+    D8Command.Builder builder =
+        D8Command.builder()
+            // Compile in debug and merge in release to assert access to both modes
+            .setMode(CompilationMode.DEBUG)
+            .setMinApiLevel(minSdkVersion)
+            .setIntermediate(true)
+            .setEnableDesugaring(true)
+            .setOutputPath(output);
+
+    builder.addLibraryResourceProvider(CachingArchiveClassFileProvider.getProvider(bootclasspath));
+
+    for (Path entry : classpath) {
+      builder.addClasspathResourceProvider(CachingArchiveClassFileProvider.getProvider(entry));
+    }
+
+    if (Files.isRegularFile(input)) {
+      builder.setOutputMode(OutputMode.Indexed);
+      builder.addProgramFiles(input);
+    } else {
+      builder.setOutputMode(OutputMode.FilePerInputClass);
+      Files.walkFileTree(input, new SimpleFileVisitor<Path>() {
+        @Override
+        public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes)
+            throws IOException {
+          builder.addClassProgramData(Files.readAllBytes(path));
+          return FileVisitResult.CONTINUE;
+        }
+      });
+    }
+
+    D8.run(builder.build(), pool);
+  }
+
+  private void merge(Path outputDir, Path mainDexClasses,
+      List<Path> toMerge) throws IOException, CompilationException {
+    D8Command.Builder merger = D8Command.builder();
+    merger.setEnableDesugaring(false);
+
+    for (Path mergeInput : toMerge) {
+      Files.walkFileTree(mergeInput, new SimpleFileVisitor<Path>() {
+        @Override
+        public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes)
+            throws IOException {
+          merger.addDexProgramData(Files.readAllBytes(path));
+          return FileVisitResult.CONTINUE;
+        }
+      });
+    }
+    if (mainDexClasses != null) {
+      merger.addMainDexListFiles(mainDexClasses);
+    }
+    merger.setMinApiLevel(minSdkVersion)
+        .setMode(CompilationMode.RELEASE)
+        .setOutputPath(outputDir)
+        .setEnableDesugaring(false)
+        .setIntermediate(false);
+    D8.run(merger.build());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/D8APiBinaryCompatibilityTests.java b/src/test/java/com/android/tools/r8/D8APiBinaryCompatibilityTests.java
new file mode 100644
index 0000000..5a3b0f8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/D8APiBinaryCompatibilityTests.java
@@ -0,0 +1,64 @@
+// 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 com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.Files;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class D8APiBinaryCompatibilityTests {
+
+  @Rule
+  public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+
+  @Test
+  public void testCompatibility() throws IOException {
+    Path compilerJar = Paths.get("tests", "api_usage_sample.jar");
+    String compiler = "com.android.tools.apiusagesample.D8Compiler";
+
+    String output = temp.newFolder().getAbsolutePath();
+    int minSdkVersion = AndroidApiLevel.K.getLevel();
+    String androidJar = ToolHelper.getAndroidJar(minSdkVersion);
+    Path lib1 = Paths.get(ToolHelper.EXAMPLES_ANDROID_O_BUILD_DIR,
+        "desugaringwithmissingclasslib1" + JAR_EXTENSION);
+    Path lib2 = Paths.get(ToolHelper.EXAMPLES_ANDROID_O_BUILD_DIR,
+        "desugaringwithmissingclasslib2" + JAR_EXTENSION);
+    Path input = Paths.get(ToolHelper.EXAMPLES_ANDROID_O_BUILD_DIR,
+        "classes", "desugaringwithmissingclasstest1");
+    File mainDexClasses = temp.newFile();
+    Files.asCharSink(mainDexClasses, StandardCharsets.UTF_8)
+        .write("desugaringwithmissingclasstest1/Main.class");
+
+    List<String> command = ImmutableList.of(
+        ToolHelper.getJavaExecutable(),
+        "-cp",
+        compilerJar.toString() + File.pathSeparator + System.getProperty("java.class.path"),
+        compiler,
+        // Compiler arguments.
+        output,
+        input.toString(),
+        Integer.toString(minSdkVersion),
+        mainDexClasses.getAbsolutePath(),
+        androidJar,
+        lib1.toString(),
+        lib2.toString());
+    ProcessBuilder builder = new ProcessBuilder(command);
+    ProcessResult result = ToolHelper.runProcess(builder);
+
+    Assert.assertEquals(result.stderr + "\n" + result.stdout, 0, result.exitCode);
+  }
+}
diff --git a/tests/api_usage_sample.jar b/tests/api_usage_sample.jar
new file mode 100644
index 0000000..b8b9d66
--- /dev/null
+++ b/tests/api_usage_sample.jar
Binary files differ