Binary compatibility tests for revised D8 API.

Change-Id: Id9801005791cb47248509830b956ed88b2ec4ae1
diff --git a/build.gradle b/build.gradle
index 418474a..61a8943 100644
--- a/build.gradle
+++ b/build.gradle
@@ -709,6 +709,12 @@
     destinationDir file('tests')
 }
 
+task buildD8ApiUsageSample(type: Jar) {
+    from sourceSets.apiUsageSample.output
+    baseName 'd8_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/D8ApiUsageSample.java b/src/test/apiUsageSample/com/android/tools/apiusagesample/D8ApiUsageSample.java
new file mode 100644
index 0000000..2261866
--- /dev/null
+++ b/src/test/apiUsageSample/com/android/tools/apiusagesample/D8ApiUsageSample.java
@@ -0,0 +1,328 @@
+// 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.CompilationFailedException;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.D8;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.DexFilePerClassFileConsumer;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.origin.ArchiveEntryOrigin;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+public class D8ApiUsageSample {
+
+  private static final Origin origin =
+      new Origin(Origin.root()) {
+        @Override
+        public String part() {
+          return "D8ApiUsageSample";
+        }
+      };
+
+  private static final DiagnosticsHandler handler = new D8DiagnosticsHandler();
+
+  /**
+   * Example invocation:
+   *
+   * <pre>
+   *   java -jar d8-api-uses.jar \
+   *     --output path/to/output/dir \
+   *     --min-api minApiLevel \
+   *     --lib path/to/library.jar \
+   *     --classpath path/to/classpath.jar \
+   *     path/to/input{1,2,3}.{jar,class}
+   * </pre>
+   */
+  public static void main(String[] args) {
+    // Parse arguments with the commandline parser to make use of its API.
+    D8Command.Builder cmd = D8Command.parse(args, origin);
+    CompilationMode mode = cmd.getMode();
+    Path temp = cmd.getOutputPath();
+    int minApiLevel = cmd.getMinApiLevel();
+    // The Builder API does not provide access to the concrete paths
+    // (everything is put into providers) so manually parse them here.
+    List<Path> libraries = new ArrayList<>(1);
+    List<Path> classpath = new ArrayList<>(args.length);
+    List<Path> inputs = new ArrayList<>(args.length);
+    for (int i = 0; i < args.length; i++) {
+      if (args[i].equals("--lib")) {
+        libraries.add(Paths.get(args[++i]));
+      } else if (args[i].equals("--classpath")) {
+        classpath.add(Paths.get(args[++i]));
+      } else if (isArchive(args[i]) || isClassFile(args[i])) {
+        inputs.add(Paths.get(args[i]));
+      }
+    }
+    if (!Files.exists(temp) || !Files.isDirectory(temp)) {
+      throw new RuntimeException("Must supply a temp/output directory");
+    }
+    if (inputs.isEmpty()) {
+      throw new RuntimeException("Must supply program inputs");
+    }
+    if (classpath.isEmpty()) {
+      throw new RuntimeException("Must supply classpath inputs");
+    }
+    if (libraries.isEmpty()) {
+      throw new RuntimeException("Must supply library inputs");
+    }
+
+    useProgramFileBuilder(CompilationMode.DEBUG, minApiLevel, libraries, classpath, inputs);
+    useProgramFileBuilder(CompilationMode.RELEASE, minApiLevel, libraries, classpath, inputs);
+    useProgramDataBuilder(minApiLevel, libraries, classpath, inputs);
+    useLibraryAndClasspathProvider(minApiLevel, libraries, classpath, inputs);
+    incrementalCompileAndMerge(minApiLevel, libraries, classpath, inputs);
+  }
+
+  // Check API support for compiling Java class-files from the file system.
+  private static void useProgramFileBuilder(
+      CompilationMode mode,
+      int minApiLevel,
+      Collection<Path> libraries,
+      Collection<Path> classpath,
+      Collection<Path> inputs) {
+    try {
+      D8.run(
+          D8Command.builder(handler)
+              .setMode(mode)
+              .setMinApiLevel(minApiLevel)
+              .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+              .addLibraryFiles(libraries)
+              .addClasspathFiles(classpath)
+              .addProgramFiles(inputs)
+              .build());
+    } catch (CompilationFailedException e) {
+      throw new RuntimeException("Unexpected compilation exceptions", e);
+    }
+  }
+
+  // Check API support for compiling Java class-files from byte content.
+  private static void useProgramDataBuilder(
+      int minApiLevel,
+      Collection<Path> libraries,
+      Collection<Path> classpath,
+      Collection<Path> inputs) {
+    try {
+      D8Command.Builder builder =
+          D8Command.builder(handler)
+              .setMinApiLevel(minApiLevel)
+              .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+              .addLibraryFiles(libraries)
+              .addClasspathFiles(classpath);
+      for (ClassFileContent classfile : readClassFiles(inputs)) {
+        builder.addClassProgramData(classfile.data, classfile.origin);
+      }
+      D8.run(builder.build());
+    } catch (CompilationFailedException e) {
+      throw new RuntimeException("Unexpected compilation exceptions", e);
+    } catch (IOException e) {
+      throw new RuntimeException("Unexpected IO exception", e);
+    }
+  }
+
+  private static void useLibraryAndClasspathProvider(
+      int minApiLevel,
+      Collection<Path> libraries,
+      Collection<Path> classpath,
+      Collection<Path> inputs) {
+    try {
+      D8Command.Builder builder =
+          D8Command.builder(handler)
+              .setMinApiLevel(minApiLevel)
+              .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+              .addProgramFiles(inputs);
+      for (Path library : libraries) {
+        builder.addLibraryResourceProvider(new ArchiveClassFileProvider(library));
+      }
+      for (Path path : classpath) {
+        builder.addClasspathResourceProvider(new ArchiveClassFileProvider(path));
+      }
+      D8.run(builder.build());
+    } catch (CompilationFailedException e) {
+      throw new RuntimeException("Unexpected compilation exceptions", e);
+    } catch (IOException e) {
+      throw new RuntimeException("Unexpected IO exception", e);
+    }
+  }
+
+  private static void incrementalCompileAndMerge(
+      int minApiLevel,
+      Collection<Path> libraries,
+      Collection<Path> classpath,
+      Collection<Path> inputs) {
+    // Compile and merge via index intermediates.
+    mergeIntermediates(
+        minApiLevel, compileToIndexedIntermediates(minApiLevel, libraries, classpath, inputs));
+    // Compile and merge via per-classfile intermediates.
+    mergeIntermediates(
+        minApiLevel, compileToPerClassFileIntermediates(minApiLevel, libraries, classpath, inputs));
+  }
+
+  private static Collection<byte[]> compileToIndexedIntermediates(
+      int minApiLevel,
+      Collection<Path> libraries,
+      Collection<Path> classpath,
+      Collection<Path> inputs) {
+    IndexIntermediatesConsumer consumer = new IndexIntermediatesConsumer();
+    try {
+      D8.run(
+          D8Command.builder(handler)
+              .setMinApiLevel(minApiLevel)
+              .setIntermediate(true)
+              .setProgramConsumer(consumer)
+              .addClasspathFiles(classpath)
+              .addLibraryFiles(libraries)
+              .addProgramFiles(inputs)
+              .build());
+    } catch (CompilationFailedException e) {
+      throw new RuntimeException("Unexpected compilation exceptions", e);
+    }
+    return consumer.bytes;
+  }
+
+  private static Collection<byte[]> compileToPerClassFileIntermediates(
+      int minApiLevel,
+      Collection<Path> libraries,
+      Collection<Path> classpath,
+      Collection<Path> inputs) {
+    PerClassIntermediatesConsumer consumer = new PerClassIntermediatesConsumer();
+    try {
+      D8.run(
+          D8Command.builder(handler)
+              .setMinApiLevel(minApiLevel)
+              .setProgramConsumer(consumer)
+              .addLibraryFiles(libraries)
+              .addClasspathFiles(classpath)
+              .addProgramFiles(inputs)
+              .build());
+    } catch (CompilationFailedException e) {
+      throw new RuntimeException("Unexpected compilation exceptions", e);
+    }
+    return consumer.bytes;
+  }
+
+  private static void mergeIntermediates(int minApiLevel, Collection<byte[]> intermediates) {
+    D8Command.Builder builder =
+        D8Command.builder(handler)
+            .setMinApiLevel(minApiLevel)
+            .setProgramConsumer(DexIndexedConsumer.emptyConsumer());
+    for (byte[] intermediate : intermediates) {
+      builder.addDexProgramData(intermediate, Origin.unknown());
+    }
+    try {
+      D8.run(builder.build());
+    } catch (CompilationFailedException e) {
+      throw new RuntimeException("Unexpected merging error", e);
+    }
+  }
+
+  // Helpers for tests.
+  // Some of this reimplements stuff in R8 utils, but that is not public API and we should not
+  // rely on it.
+
+  private static List<ClassFileContent> readClassFiles(Collection<Path> files) throws IOException {
+    List<ClassFileContent> classfiles = new ArrayList<>();
+    for (Path file : files) {
+      if (isArchive(file)) {
+        Origin zipOrigin = new PathOrigin(file);
+        ZipInputStream zip = new ZipInputStream(Files.newInputStream(file));
+        ZipEntry entry;
+        while (null != (entry = zip.getNextEntry())) {
+          if (isClassFile(Paths.get(entry.getName()))) {
+            Origin origin = new ArchiveEntryOrigin(entry.getName(), zipOrigin);
+            classfiles.add(new ClassFileContent(origin, readBytes(zip)));
+          }
+        }
+      } else if (isClassFile(file)) {
+        classfiles.add(new ClassFileContent(new PathOrigin(file), Files.readAllBytes(file)));
+      }
+    }
+    return classfiles;
+  }
+
+  private static byte[] readBytes(InputStream stream) throws IOException {
+    try (ByteArrayOutputStream bytes = new ByteArrayOutputStream()) {
+      byte[] buffer = new byte[0xffff];
+      for (int length; (length = stream.read(buffer)) != -1; ) {
+        bytes.write(buffer, 0, length);
+      }
+      return bytes.toByteArray();
+    }
+  }
+
+  private static boolean isClassFile(Path file) {
+    return isClassFile(file.toString());
+  }
+
+  private static boolean isClassFile(String file) {
+    file = file.toLowerCase();
+    return file.endsWith(".class");
+  }
+
+  private static boolean isArchive(Path file) {
+    return isArchive(file.toString());
+  }
+
+  private static boolean isArchive(String file) {
+    file = file.toLowerCase();
+    return file.endsWith(".zip") || file.endsWith(".jar");
+  }
+
+  private static class ClassFileContent {
+    final Origin origin;
+    final byte[] data;
+
+    public ClassFileContent(Origin origin, byte[] data) {
+      this.origin = origin;
+      this.data = data;
+    }
+  }
+
+  private static class IndexIntermediatesConsumer implements DexIndexedConsumer {
+
+    List<byte[]> bytes = new ArrayList<>();
+
+    @Override
+    public synchronized void accept(
+        int fileIndex, byte[] data, Set<String> descriptors, DiagnosticsHandler handler) {
+      bytes.add(data);
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {}
+  }
+
+  private static class PerClassIntermediatesConsumer implements DexFilePerClassFileConsumer {
+
+    List<byte[]> bytes = new ArrayList<>();
+
+    @Override
+    public synchronized void accept(
+        String primaryClassDescriptor,
+        byte[] data,
+        Set<String> descriptors,
+        DiagnosticsHandler handler) {
+      bytes.add(data);
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {}
+  }
+}
diff --git a/src/test/apiUsageSample/com/android/tools/apiusagesample/D8DiagnosticsHandler.java b/src/test/apiUsageSample/com/android/tools/apiusagesample/D8DiagnosticsHandler.java
index d9f11b0..9b08352 100644
--- a/src/test/apiUsageSample/com/android/tools/apiusagesample/D8DiagnosticsHandler.java
+++ b/src/test/apiUsageSample/com/android/tools/apiusagesample/D8DiagnosticsHandler.java
@@ -49,7 +49,7 @@
     Origin origin = diagnostic.getOrigin();
     Position positionInOrigin = diagnostic.getPosition();
     String position;
-    if (origin instanceof PathOrigin) {
+    if (origin.parent() instanceof PathOrigin) {
       if (positionInOrigin instanceof TextRange) {
         TextRange textRange = (TextRange) positionInOrigin;
         position = ((PathOrigin) origin.parent()).getPath().toFile() + ": "
diff --git a/src/test/java/com/android/tools/r8/D8APiBinaryCompatibilityTests.java b/src/test/java/com/android/tools/r8/D8APiBinaryCompatibilityTests.java
deleted file mode 100644
index 5a3b0f8..0000000
--- a/src/test/java/com/android/tools/r8/D8APiBinaryCompatibilityTests.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// 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/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..c5f1a19
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/D8ApiBinaryCompatibilityTests.java
@@ -0,0 +1,118 @@
+// 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 java.util.stream.Collectors;
+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 testCompatibilityDeprecatedApi() 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);
+    Assert.assertTrue(result.stdout, result.stdout.isEmpty());
+    Assert.assertTrue(result.stderr, result.stderr.isEmpty());
+  }
+
+  @Test
+  public void testCompatibilityNewApi() throws IOException {
+    Path jar = Paths.get("tests", "d8_api_usage_sample.jar");
+    String main = "com.android.tools.apiusagesample.D8ApiUsageSample";
+    int minApiLevel = AndroidApiLevel.K.getLevel();
+
+    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 inputDir =
+        Paths.get(
+            ToolHelper.EXAMPLES_ANDROID_O_BUILD_DIR, "classes", "desugaringwithmissingclasstest1");
+    List<Path> input =
+        ImmutableList.of(
+            inputDir.resolve("ImplementMethodsWithDefault.class"), inputDir.resolve("Main.class"));
+    File mainDexClasses = temp.newFile();
+    Files.asCharSink(mainDexClasses, StandardCharsets.UTF_8)
+        .write("desugaringwithmissingclasstest1/Main.class");
+
+    List<String> command =
+        ImmutableList.<String>builder()
+            .addAll(
+                ImmutableList.of(
+                    ToolHelper.getJavaExecutable(),
+                    "-cp",
+                    jar.toString() + File.pathSeparator + System.getProperty("java.class.path"),
+                    main,
+                    // Compiler arguments.
+                    "--output",
+                    temp.newFolder().getAbsolutePath(),
+                    "--min-api",
+                    Integer.toString(minApiLevel),
+                    "--lib",
+                    ToolHelper.getAndroidJar(minApiLevel),
+                    "--classpath",
+                    lib1.toString(),
+                    "--classpath",
+                    lib2.toString()))
+            .addAll(input.stream().map(Path::toString).collect(Collectors.toList()))
+            .build();
+
+    ProcessBuilder builder = new ProcessBuilder(command);
+    ProcessResult result = ToolHelper.runProcess(builder);
+    Assert.assertEquals(result.stderr + "\n" + result.stdout, 0, result.exitCode);
+    Assert.assertTrue(result.stdout, result.stdout.isEmpty());
+    Assert.assertTrue(result.stderr, result.stderr.isEmpty());
+  }
+}
diff --git a/tests/d8_api_usage_sample.jar b/tests/d8_api_usage_sample.jar
new file mode 100644
index 0000000..4346c82
--- /dev/null
+++ b/tests/d8_api_usage_sample.jar
Binary files differ