Reland "[Retrace] Add retrace api test infrastructure and a simple test"

This reverts commit 802ceb2a500beb7ac8be12d23e73b87522b74633.

Change-Id: I353d7e9d5c08fd2644813f1b5c76b059704457e3
diff --git a/.gitignore b/.gitignore
index 07f90a7..885f76d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -231,6 +231,8 @@
 third_party/r8mappings.tar.gz
 third_party/remapper
 third_party/remapper.tar.gz
+third_party/retrace
+third_party/retrace.tar.gz
 third_party/retrace_benchmark
 third_party/retrace_benchmark.tar.gz
 third_party/retrace_internal
diff --git a/build.gradle b/build.gradle
index 8b0525f..08c9141 100644
--- a/build.gradle
+++ b/build.gradle
@@ -346,6 +346,7 @@
                 "proguard/proguard5.2.1",
                 "proguard/proguard6.0.1",
                 "proguard/proguard-7.0.0",
+                "retrace/binary_compatibility",
                 "r8",
                 "r8-releases/2.0.74",
                 "r8mappings",
diff --git a/src/main/java/com/android/tools/r8/utils/ZipUtils.java b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
index e7b9b00..da688a8 100644
--- a/src/main/java/com/android/tools/r8/utils/ZipUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
@@ -102,19 +102,24 @@
       throws IOException {
     try (ZipOutputStream stream =
         new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(zipFile)))) {
-      for (Path path : filesToZip) {
-        ZipEntry zipEntry =
-            new ZipEntry(
-                StreamSupport.stream(
-                        Spliterators.spliteratorUnknownSize(
-                            basePath.relativize(path).iterator(), Spliterator.ORDERED),
-                        false)
-                    .map(Path::toString)
-                    .collect(Collectors.joining("/")));
-        stream.putNextEntry(zipEntry);
-        Files.copy(path, stream);
-        stream.closeEntry();
-      }
+      zip(stream, basePath, filesToZip);
+    }
+  }
+
+  public static void zip(ZipOutputStream stream, Path basePath, Collection<Path> filesToZip)
+      throws IOException {
+    for (Path path : filesToZip) {
+      ZipEntry zipEntry =
+          new ZipEntry(
+              StreamSupport.stream(
+                      Spliterators.spliteratorUnknownSize(
+                          basePath.relativize(path).iterator(), Spliterator.ORDERED),
+                      false)
+                  .map(Path::toString)
+                  .collect(Collectors.joining("/")));
+      stream.putNextEntry(zipEntry);
+      Files.copy(path, stream);
+      stream.closeEntry();
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 8c52813..2ccb378 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -8,7 +8,9 @@
 import static com.android.tools.r8.ToolHelper.R8_TEST_BUCKET;
 import static com.android.tools.r8.utils.InternalOptions.ASM_VERSION;
 import static com.google.common.collect.Lists.cartesianProduct;
+import static com.google.common.io.ByteStreams.toByteArray;
 import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -93,6 +95,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.common.io.ByteStreams;
+import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -1913,4 +1916,54 @@
     }
     return false;
   }
+
+  public static boolean assertProgramsEqual(Path expectedJar, Path actualJar) throws Exception {
+    if (filesAreEqual(expectedJar, actualJar)) {
+      return true;
+    }
+    ArchiveClassFileProvider expected = new ArchiveClassFileProvider(expectedJar);
+    ArchiveClassFileProvider actual = new ArchiveClassFileProvider(actualJar);
+    assertEquals(getSortedDescriptorList(expected), getSortedDescriptorList(actual));
+    for (String descriptor : expected.getClassDescriptors()) {
+      assertArrayEquals(
+          "Class " + descriptor + " differs",
+          getClassAsBytes(expected, descriptor),
+          getClassAsBytes(actual, descriptor));
+    }
+    return false;
+  }
+
+  public static boolean filesAreEqual(Path file1, Path file2) throws IOException {
+    long size = Files.size(file1);
+    long sizeOther = Files.size(file2);
+    if (size != sizeOther) {
+      return false;
+    }
+    if (size < 4096) {
+      return Arrays.equals(Files.readAllBytes(file1), Files.readAllBytes(file2));
+    }
+    int byteRead1 = 0;
+    int byteRead2 = 0;
+    try (FileInputStream fs1 = new FileInputStream(file1.toString());
+        FileInputStream fs2 = new FileInputStream(file2.toString())) {
+      BufferedInputStream bs1 = new BufferedInputStream(fs1);
+      BufferedInputStream bs2 = new BufferedInputStream(fs2);
+      while (byteRead1 == byteRead2 && byteRead1 != -1) {
+        byteRead1 = bs1.read();
+        byteRead2 = bs2.read();
+      }
+    }
+    return byteRead1 == byteRead2;
+  }
+
+  private static List<String> getSortedDescriptorList(ArchiveClassFileProvider inputJar) {
+    ArrayList<String> descriptorList = new ArrayList<>(inputJar.getClassDescriptors());
+    Collections.sort(descriptorList);
+    return descriptorList;
+  }
+
+  private static byte[] getClassAsBytes(ArchiveClassFileProvider inputJar, String descriptor)
+      throws Exception {
+    return toByteArray(inputJar.getProgramResource(descriptor).getByteStream());
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderGeneratorTest.java b/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderGeneratorTest.java
index 8b3571a..91accca 100644
--- a/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderGeneratorTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderGeneratorTest.java
@@ -18,7 +18,6 @@
 import com.android.tools.r8.TestState;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.apimodel.AndroidApiVersionsXmlParser.ParsedApiClass;
-import com.android.tools.r8.cf.bootstrap.BootstrapCurrentEqualityTest;
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.utils.AndroidApiLevel;
@@ -116,7 +115,7 @@
 
   @Test
   public void testDatabaseGenerationUpToDate() throws Exception {
-    BootstrapCurrentEqualityTest.filesAreEqual(generateJar(), API_DATABASE_JAR);
+    TestBase.filesAreEqual(generateJar(), API_DATABASE_JAR);
   }
 
   /**
diff --git a/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java b/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java
index e41bf8d..1a6717f 100644
--- a/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java
+++ b/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java
@@ -5,15 +5,12 @@
 
 import static com.android.tools.r8.graph.GenericSignatureIdentityTest.testParseSignaturesInJar;
 import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
-import static com.google.common.io.ByteStreams.toByteArray;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 
-import com.android.tools.r8.ArchiveClassFileProvider;
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.ExternalR8TestCompileResult;
 import com.android.tools.r8.TestBase;
@@ -29,15 +26,9 @@
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.Lists;
-import java.io.BufferedInputStream;
-import java.io.FileInputStream;
 import java.io.IOException;
-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 org.junit.BeforeClass;
 import org.junit.ClassRule;
@@ -183,8 +174,7 @@
             .setMode(CompilationMode.RELEASE)
             .compile()
             .outputJar();
-    assert uploadJarsToCloudStorageIfTestFails(
-        BootstrapCurrentEqualityTest::filesAreEqual, runR81, runR82);
+    assert uploadJarsToCloudStorageIfTestFails(TestBase::filesAreEqual, runR81, runR82);
   }
 
   @Test
@@ -247,56 +237,6 @@
         BootstrapCurrentEqualityTest::assertProgramsEqual, result.outputJar(), runR8R8.outputJar());
   }
 
-  public static boolean assertProgramsEqual(Path expectedJar, Path actualJar) throws Exception {
-    if (filesAreEqual(expectedJar, actualJar)) {
-      return true;
-    }
-    ArchiveClassFileProvider expected = new ArchiveClassFileProvider(expectedJar);
-    ArchiveClassFileProvider actual = new ArchiveClassFileProvider(actualJar);
-    assertEquals(getSortedDescriptorList(expected), getSortedDescriptorList(actual));
-    for (String descriptor : expected.getClassDescriptors()) {
-      assertArrayEquals(
-          "Class " + descriptor + " differs",
-          getClassAsBytes(expected, descriptor),
-          getClassAsBytes(actual, descriptor));
-    }
-    return false;
-  }
-
-  public static boolean filesAreEqual(Path file1, Path file2) throws IOException {
-    long size = Files.size(file1);
-    long sizeOther = Files.size(file2);
-    if (size != sizeOther) {
-      return false;
-    }
-    if (size < 4096) {
-      return Arrays.equals(Files.readAllBytes(file1), Files.readAllBytes(file2));
-    }
-    int byteRead1 = 0;
-    int byteRead2 = 0;
-    try (FileInputStream fs1 = new FileInputStream(file1.toString());
-        FileInputStream fs2 = new FileInputStream(file2.toString())) {
-      BufferedInputStream bs1 = new BufferedInputStream(fs1);
-      BufferedInputStream bs2 = new BufferedInputStream(fs2);
-      while (byteRead1 == byteRead2 && byteRead1 != -1) {
-        byteRead1 = bs1.read();
-        byteRead2 = bs2.read();
-      }
-    }
-    return byteRead1 == byteRead2;
-  }
-
-  private static List<String> getSortedDescriptorList(ArchiveClassFileProvider inputJar) {
-    ArrayList<String> descriptorList = new ArrayList<>(inputJar.getClassDescriptors());
-    Collections.sort(descriptorList);
-    return descriptorList;
-  }
-
-  private static byte[] getClassAsBytes(ArchiveClassFileProvider inputJar, String descriptor)
-      throws Exception {
-    return toByteArray(inputJar.getProgramResource(descriptor).getByteStream());
-  }
-
   private static TemporaryFolder newTempFolder() throws IOException {
     TemporaryFolder tempFolder = new TemporaryFolder(testFolder.newFolder());
     tempFolder.create();
diff --git a/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapTest.java b/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapTest.java
index 7dd698b..409236d 100644
--- a/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapTest.java
+++ b/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapTest.java
@@ -4,11 +4,8 @@
 package com.android.tools.r8.cf.bootstrap;
 
 import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
-import static com.google.common.io.ByteStreams.toByteArray;
-import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 
-import com.android.tools.r8.ArchiveClassFileProvider;
 import com.android.tools.r8.ClassFileConsumer;
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.R8Command;
@@ -21,9 +18,6 @@
 import com.google.common.base.Charsets;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -153,27 +147,4 @@
     String pgMap = FileUtils.readTextFile(pgMapFile, Charsets.UTF_8);
     return new R8Result(processResult, outputJar, pgMap);
   }
-
-  private static void assertProgramsEqual(Path expectedJar, Path actualJar) throws Exception {
-    ArchiveClassFileProvider expected = new ArchiveClassFileProvider(expectedJar);
-    ArchiveClassFileProvider actual = new ArchiveClassFileProvider(actualJar);
-    assertEquals(getSortedDescriptorList(expected), getSortedDescriptorList(actual));
-    for (String descriptor : expected.getClassDescriptors()) {
-      assertArrayEquals(
-          "Class " + descriptor + " differs",
-          getClassAsBytes(expected, descriptor),
-          getClassAsBytes(actual, descriptor));
-    }
-  }
-
-  private static List<String> getSortedDescriptorList(ArchiveClassFileProvider inputJar) {
-    ArrayList<String> descriptorList = new ArrayList<>(inputJar.getClassDescriptors());
-    Collections.sort(descriptorList);
-    return descriptorList;
-  }
-
-  private static byte[] getClassAsBytes(ArchiveClassFileProvider inputJar, String descriptor)
-      throws Exception {
-    return toByteArray(inputJar.getProgramResource(descriptor).getByteStream());
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/r8ondex/R8CompiledThroughDexTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/r8ondex/R8CompiledThroughDexTest.java
index bb66f2e..01eb3b8 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/r8ondex/R8CompiledThroughDexTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/r8ondex/R8CompiledThroughDexTest.java
@@ -12,13 +12,13 @@
 import com.android.tools.r8.R8;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.R8Command.Builder;
+import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.TestRuntime;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.cf.bootstrap.BootstrapCurrentEqualityTest;
 import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.Pair;
@@ -166,7 +166,7 @@
               + " differ from the external run which uses "
               + r8jar
               + ". If up-to-date, the likely cause of this error is that R8 is non-deterministic.",
-          BootstrapCurrentEqualityTest.filesAreEqual(outputThroughCf, outputThroughCfExternal));
+          TestBase.filesAreEqual(outputThroughCf, outputThroughCfExternal));
     }
 
     // Finally compile R8 on the ART runtime using the already compiled DEX version of R8.
@@ -194,7 +194,7 @@
       assertEquals(0, artProcessResult.exitCode);
       assertTrue(
           "The output of R8/JVM in-process and R8/ART external differ.",
-          BootstrapCurrentEqualityTest.filesAreEqual(outputThroughCf, outputThroughDex));
+          TestBase.filesAreEqual(outputThroughCf, outputThroughDex));
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/retrace/api/RetraceApiBinaryCompatibilityTest.java b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiBinaryCompatibilityTest.java
new file mode 100644
index 0000000..4557036
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiBinaryCompatibilityTest.java
@@ -0,0 +1,95 @@
+// Copyright (c) 2021, 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.retrace.api;
+
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.ZipUtils;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+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 RetraceApiBinaryCompatibilityTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public RetraceApiBinaryCompatibilityTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  private static final Path BINARY_COMPATIBILITY_JAR =
+      Paths.get(ToolHelper.THIRD_PARTY_DIR, "retrace", "binary_compatibility", "tests.jar");
+
+  private Path generateJar() throws Exception {
+    return RetraceApiTestHelper.generateJarForRetraceBinaryTests(
+        temp, RetraceApiTestHelper.getBinaryCompatibilityTests());
+  }
+
+  @Test
+  public void testBinaryJarIsUpToDate() throws Exception {
+    Path binaryContents = temp.newFolder().toPath();
+    Path generatedContents = temp.newFolder().toPath();
+    ZipUtils.unzip(BINARY_COMPATIBILITY_JAR, binaryContents);
+    ZipUtils.unzip(generateJar(), generatedContents);
+    try (Stream<Path> existingPaths = Files.walk(binaryContents);
+        Stream<Path> generatedPaths = Files.walk(generatedContents)) {
+      List<Path> existing = existingPaths.filter(this::isClassFile).collect(Collectors.toList());
+      List<Path> generated = generatedPaths.filter(this::isClassFile).collect(Collectors.toList());
+      assertEquals(existing.size(), generated.size());
+      assertNotEquals(0, existing.size());
+      for (Path classFile : generated) {
+        Path otherClassFile = binaryContents.resolve(classFile);
+        assertTrue(Files.exists(otherClassFile));
+        assertTrue(TestBase.filesAreEqual(classFile, otherClassFile));
+      }
+    }
+  }
+
+  private boolean isClassFile(Path path) {
+    return path.toString().endsWith(".class");
+  }
+
+  @Test
+  public void runCheckedInBinaryJar() throws Exception {
+    for (CfRuntime cfRuntime : CfRuntime.getCheckedInCfRuntimes()) {
+      RetraceApiTestHelper.runJunitOnTests(
+          cfRuntime,
+          ToolHelper.R8_RETRACE_JAR,
+          BINARY_COMPATIBILITY_JAR,
+          RetraceApiTestHelper.getBinaryCompatibilityTests());
+    }
+  }
+
+  public static void main(String[] args) throws Exception {
+    TemporaryFolder temp = new TemporaryFolder();
+    temp.create();
+    Path generatedJar =
+        RetraceApiTestHelper.generateJarForRetraceBinaryTests(
+            temp, RetraceApiTestHelper.getBinaryCompatibilityTests());
+    Files.move(generatedJar, BINARY_COMPATIBILITY_JAR, REPLACE_EXISTING);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/retrace/api/RetraceApiBinaryTest.java b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiBinaryTest.java
new file mode 100644
index 0000000..eac71c5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiBinaryTest.java
@@ -0,0 +1,7 @@
+// Copyright (c) 2021, 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.retrace.api;
+
+public interface RetraceApiBinaryTest {}
diff --git a/src/test/java/com/android/tools/r8/retrace/api/RetraceApiEmptyTest.java b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiEmptyTest.java
new file mode 100644
index 0000000..45df95c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiEmptyTest.java
@@ -0,0 +1,51 @@
+// Copyright (c) 2021, 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.retrace.api;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.retrace.ProguardMapProducer;
+import com.android.tools.r8.retrace.RetracedClassReference;
+import com.android.tools.r8.retrace.Retracer;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RetraceApiEmptyTest extends RetraceApiTestBase {
+
+  public RetraceApiEmptyTest(TestParameters parameters) {
+    super(parameters);
+  }
+
+  @Override
+  protected Class<? extends RetraceApiBinaryTest> binaryTestClass() {
+    return RetraceTest.class;
+  }
+
+  public static class RetraceTest implements RetraceApiBinaryTest {
+
+    @Test
+    public void testNone() throws Exception {
+      String expected = "hello.World";
+      List<RetracedClassReference> retracedClasses = new ArrayList<>();
+      Retracer.createDefault(ProguardMapProducer.fromString(""), new DiagnosticsHandler() {})
+          .retraceClass(Reference.classFromTypeName(expected))
+          .stream()
+          .forEach(
+              result -> {
+                retracedClasses.add(result.getRetracedClass());
+              });
+      assertEquals(1, retracedClasses.size());
+      RetracedClassReference retracedClass = retracedClasses.get(0);
+      assertEquals(retracedClass.getTypeName(), expected);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/retrace/api/RetraceApiTestBase.java b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiTestBase.java
new file mode 100644
index 0000000..a58102a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiTestBase.java
@@ -0,0 +1,53 @@
+// Copyright (c) 2021, 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.retrace.api;
+
+import static com.android.tools.r8.retrace.api.RetraceApiTestHelper.runJunitOnTests;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersBuilder;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import java.nio.file.Files;
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.JUnitCore;
+import org.junit.runner.Result;
+import org.junit.runner.notification.Failure;
+import org.junit.runners.Parameterized.Parameters;
+
+public abstract class RetraceApiTestBase extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return TestParametersBuilder.builder().withSystemRuntime().build();
+  }
+
+  public RetraceApiTestBase(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  protected abstract Class<? extends RetraceApiBinaryTest> binaryTestClass();
+
+  @Test
+  public void testDirect() {
+    Result result = JUnitCore.runClasses(binaryTestClass());
+    for (Failure failure : result.getFailures()) {
+      System.out.println(failure.toString());
+    }
+    assertTrue(result.wasSuccessful());
+  }
+
+  @Test
+  public void testRetraceLib() throws Exception {
+    Assume.assumeTrue(Files.exists(ToolHelper.R8_RETRACE_JAR));
+    runJunitOnTests(
+        parameters.getRuntime().asCf(), ToolHelper.R8_RETRACE_JAR, binaryTestClass(), temp);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/retrace/api/RetraceApiTestHelper.java b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiTestHelper.java
new file mode 100644
index 0000000..9b2f67d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiTestHelper.java
@@ -0,0 +1,117 @@
+// Copyright (c) 2021, 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.retrace.api;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.Collectors;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.transformers.ClassFileTransformer;
+import com.android.tools.r8.transformers.ClassFileTransformer.InnerClassPredicate;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.ZipUtils;
+import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import com.google.common.collect.ImmutableList;
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import org.junit.rules.TemporaryFolder;
+
+public class RetraceApiTestHelper {
+
+  private static final String JUNIT_JAR = "junit-4.13-beta-2.jar";
+  private static final String HAMCREST = "hamcrest-core-1.3.jar";
+
+  public static List<Class<? extends RetraceApiBinaryTest>> CLASSES_FOR_BINARY_COMPATIBILITY =
+      ImmutableList.of(RetraceApiEmptyTest.RetraceTest.class);
+  public static List<Class<? extends RetraceApiBinaryTest>> CLASSES_PENDING_BINARY_COMPATIBILITY =
+      ImmutableList.of();
+
+  public static void runJunitOnTests(
+      CfRuntime runtime,
+      Path r8Jar,
+      Class<? extends RetraceApiBinaryTest> clazz,
+      TemporaryFolder temp)
+      throws Exception {
+    assertTrue(testIsSpecifiedAsBinaryOrPending(clazz));
+    List<Class<? extends RetraceApiBinaryTest>> testClasses = ImmutableList.of(clazz);
+    runJunitOnTests(
+        runtime, r8Jar, generateJarForRetraceBinaryTests(temp, testClasses), testClasses);
+  }
+
+  public static void runJunitOnTests(
+      CfRuntime runtime,
+      Path r8Jar,
+      Path testJar,
+      Collection<Class<? extends RetraceApiBinaryTest>> tests)
+      throws Exception {
+    List<Path> classPaths = ImmutableList.of(getJunitDependency(), getHamcrest(), r8Jar, testJar);
+    ProcessResult processResult =
+        ToolHelper.runJava(
+            runtime,
+            classPaths,
+            "org.junit.runner.JUnitCore",
+            StringUtils.join(" ", tests, Class::getTypeName));
+    assertEquals(0, processResult.exitCode);
+    assertThat(processResult.stdout, containsString("OK (" + tests.size() + " test"));
+  }
+
+  private static Path getJunitDependency() {
+    String junitPath =
+        Arrays.stream(System.getProperty("java.class.path").split(":"))
+            .filter(cp -> cp.endsWith(JUNIT_JAR))
+            .collect(Collectors.toSingle());
+    return Paths.get(junitPath);
+  }
+
+  private static Path getHamcrest() {
+    String junitPath =
+        Arrays.stream(System.getProperty("java.class.path").split(":"))
+            .filter(cp -> cp.endsWith(HAMCREST))
+            .collect(Collectors.toSingle());
+    return Paths.get(junitPath);
+  }
+
+  public static Path generateJarForRetraceBinaryTests(
+      TemporaryFolder temp, Collection<Class<? extends RetraceApiBinaryTest>> classes)
+      throws Exception {
+    Path jar = File.createTempFile("retrace_api_tests", ".jar", temp.getRoot()).toPath();
+    ZipBuilder zipBuilder = ZipBuilder.builder(jar);
+    for (Class<? extends RetraceApiBinaryTest> retraceApiTest : classes) {
+      zipBuilder.addFilesRelative(
+          ToolHelper.getClassPathForTests(),
+          ToolHelper.getClassFilesForInnerClasses(retraceApiTest));
+      zipBuilder.addBytes(
+          ZipUtils.zipEntryNameForClass(retraceApiTest),
+          ClassFileTransformer.create(retraceApiTest)
+              .removeInnerClasses(
+                  InnerClassPredicate.onName(
+                      DescriptorUtils.getBinaryNameFromJavaType(retraceApiTest.getTypeName())))
+              .transform());
+    }
+    zipBuilder.addFilesRelative(
+        ToolHelper.getClassPathForTests(),
+        ToolHelper.getClassFileForTestClass(RetraceApiBinaryTest.class));
+    return zipBuilder.build();
+  }
+
+  public static Collection<Class<? extends RetraceApiBinaryTest>> getBinaryCompatibilityTests() {
+    return CLASSES_FOR_BINARY_COMPATIBILITY;
+  }
+
+  private static boolean testIsSpecifiedAsBinaryOrPending(Class<?> clazz) {
+    return CLASSES_FOR_BINARY_COMPATIBILITY.contains(clazz)
+        || CLASSES_PENDING_BINARY_COMPATIBILITY.contains(clazz);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
index 2bbcf4a..bc72103 100644
--- a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.transformers;
 
 import static com.android.tools.r8.references.Reference.classFromTypeName;
+import static com.android.tools.r8.transformers.ClassFileTransformer.InnerClassPredicate.always;
 import static com.android.tools.r8.utils.DescriptorUtils.getBinaryNameFromDescriptor;
 import static com.android.tools.r8.utils.StringUtils.replaceAll;
 import static org.objectweb.asm.Opcodes.ASM7;
@@ -31,6 +32,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -556,12 +558,31 @@
     boolean test(String name, String typeDescriptor);
   }
 
+  @FunctionalInterface
+  public interface InnerClassPredicate {
+    boolean test(String name, String outerName, String innerName, int access);
+
+    static InnerClassPredicate always() {
+      return (name, outerName, innerName, access) -> true;
+    }
+
+    static InnerClassPredicate onName(String name) {
+      return (otherName, outerName, innerName, access) -> Objects.equals(name, otherName);
+    }
+  }
+
   public ClassFileTransformer removeInnerClasses() {
+    return removeInnerClasses(always());
+  }
+
+  public ClassFileTransformer removeInnerClasses(InnerClassPredicate predicate) {
     return addClassTransformer(
         new ClassTransformer() {
           @Override
           public void visitInnerClass(String name, String outerName, String innerName, int access) {
-            // Intentionally empty.
+            if (!predicate.test(name, outerName, innerName, access)) {
+              super.visitInnerClass(name, outerName, innerName, access);
+            }
           }
         });
   }
diff --git a/third_party/retrace/binary_compatibility.tar.gz.sha1 b/third_party/retrace/binary_compatibility.tar.gz.sha1
new file mode 100644
index 0000000..3825d93
--- /dev/null
+++ b/third_party/retrace/binary_compatibility.tar.gz.sha1
@@ -0,0 +1 @@
+63dd09b086d0d134219a379720c6b68a94afdf48
\ No newline at end of file