Add example for soft verification slow down test builders

Bug: 187496508
Change-Id: Id5f9d9f749a606e7c2e536ef9c2ada7d623416af
diff --git a/.gitignore b/.gitignore
index b929f34..b03bb7b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,6 +29,8 @@
 tests/2016-12-19/art.tar.gz
 tests/2017-10-04/art
 tests/2017-10-04/art.tar.gz
+third_party/api-outlining/simple-app-dump
+third_party/api-outlining/simple-app-dump.tar.gz
 third_party/android_cts_baseline
 third_party/android_cts_baseline.tar.gz
 third_party/clank/clank_google3_prebuilt
diff --git a/build.gradle b/build.gradle
index 8e20e6d..3101886 100644
--- a/build.gradle
+++ b/build.gradle
@@ -315,6 +315,7 @@
                 "android_jar/lib-v29",
                 "android_jar/lib-v30",
                 "android_jar/lib-v31",
+                "api-outlining/simple-app-dump",
                 "core-lambda-stubs",
                 "dart-sdk",
                 "ddmlib",
diff --git a/src/test/examplesAndroidApi/softverificationerror/ApiCallerInlined.java b/src/test/examplesAndroidApi/softverificationerror/ApiCallerInlined.java
new file mode 100644
index 0000000..0ef2b5c
--- /dev/null
+++ b/src/test/examplesAndroidApi/softverificationerror/ApiCallerInlined.java
@@ -0,0 +1,41 @@
+// 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.example.softverificationerror;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.os.Build;
+
+public class ApiCallerInlined {
+
+  public static void callApi(android.content.Context context) {
+    // Create the NotificationChannel, but only on API 26+ because
+    // the NotificationChannel class is new and not in the support library
+    if (Build.VERSION.SDK_INT >= 26) {
+      constructUnknownObjectAndCallUnknownMethod(context);
+    }
+  }
+
+  public static void constructUnknownObject(android.content.Context context) {
+    NotificationChannel channel =
+        new NotificationChannel("CHANNEL_ID", "FOO", NotificationManager.IMPORTANCE_DEFAULT);
+    channel.setDescription("This is a test channel");
+  }
+
+  public static void callUnknownMethod(android.content.Context context) {
+    NotificationManager notificationManager =
+        (NotificationManager) context.getSystemService(NotificationManager.class);
+    notificationManager.createNotificationChannel(null);
+  }
+
+  public static void constructUnknownObjectAndCallUnknownMethod(android.content.Context context) {
+    NotificationChannel channel =
+        new NotificationChannel("CHANNEL_ID", "FOO", NotificationManager.IMPORTANCE_DEFAULT);
+    channel.setDescription("This is a test channel");
+    NotificationManager notificationManager =
+        (NotificationManager) context.getSystemService(NotificationManager.class);
+    notificationManager.createNotificationChannel(channel);
+  }
+}
diff --git a/src/test/examplesAndroidApi/softverificationerror/ApiCallerOutlined.java b/src/test/examplesAndroidApi/softverificationerror/ApiCallerOutlined.java
new file mode 100644
index 0000000..47c3e61
--- /dev/null
+++ b/src/test/examplesAndroidApi/softverificationerror/ApiCallerOutlined.java
@@ -0,0 +1,16 @@
+// 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.example.softverificationerror;
+
+import android.os.Build;
+
+public class ApiCallerOutlined {
+
+  public static void callApi(android.content.Context context) {
+    if (Build.VERSION.SDK_INT >= 26) {
+      ApiCallerInlined.constructUnknownObjectAndCallUnknownMethod(context);
+    }
+  }
+}
diff --git a/src/test/examplesAndroidApi/softverificationerror/Main.java b/src/test/examplesAndroidApi/softverificationerror/Main.java
new file mode 100644
index 0000000..f2a4376
--- /dev/null
+++ b/src/test/examplesAndroidApi/softverificationerror/Main.java
@@ -0,0 +1,13 @@
+// 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.example.softverificationerror;
+
+public class Main {
+
+  public static void test(android.content.Context context) {
+    ApiCallerInlined.callApi(context);
+    ApiCallerOutlined.callApi(context);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/SoftVerificationErrorJarGenerator.java b/src/test/java/com/android/tools/r8/SoftVerificationErrorJarGenerator.java
new file mode 100644
index 0000000..50f6e05
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/SoftVerificationErrorJarGenerator.java
@@ -0,0 +1,347 @@
+// 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;
+
+import com.android.tools.r8.utils.IntBox;
+import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.ZipUtils;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.function.Supplier;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+public class SoftVerificationErrorJarGenerator {
+
+  public enum ApiCallerName {
+    CONSTRUCT_UNKNOWN("constructUnknownObject"),
+    CALL_UNKNOWN("callUnknownMethod"),
+    CONSTRUCT_AND_CALL_UNKNOWN("constructUnknownObjectAndCallUnknownMethod");
+
+    private String apiCallerMethodName;
+
+    ApiCallerName(String apiCallerMethodName) {
+      this.apiCallerMethodName = apiCallerMethodName;
+    }
+
+    public String getApiCallerMethodName() {
+      return apiCallerMethodName;
+    }
+  }
+
+  public static String NEW_API_CLASS_NAME = "android/app/NotificationChannel";
+  public static String NEW_API_CLASS_METHOD_NAME = "setDescription";
+  public static String EXISTING_API_METHOD_NAME = "createNotificationChannel";
+
+  public static void createJar(
+      Path archive,
+      int numberOfClasses,
+      boolean isOutlined,
+      ApiCallerName callerName,
+      String newApiClassName,
+      String newApiClassMethodName,
+      String existingApiNewMethodName)
+      throws IOException {
+    OpenOption[] options =
+        new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING};
+    try (ZipOutputStream out =
+        new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(archive, options)))) {
+      IntBox intBox = new IntBox(0);
+      Pair<byte[], String> callerPair =
+          isOutlined
+              ? Dumps.dumpApiCallerInlined(
+                  -1,
+                  callerName.getApiCallerMethodName(),
+                  newApiClassName,
+                  newApiClassMethodName,
+                  existingApiNewMethodName)
+              : null;
+      if (callerPair != null) {
+        // The outlined code will call the apiCallerName on the callerPair code.
+        ZipUtils.writeToZipStream(
+            out, callerPair.getSecond() + ".class", callerPair.getFirst(), ZipEntry.STORED);
+      }
+      byte[] mainBytes =
+          Dumps.dumpMain(
+              () -> {
+                if (intBox.get() > numberOfClasses) {
+                  return null;
+                }
+                Pair<byte[], String> classData =
+                    isOutlined
+                        ? Dumps.dumpApiCallerOutlined(
+                            intBox.getAndIncrement(),
+                            callerPair.getSecond(),
+                            callerName.getApiCallerMethodName())
+                        : Dumps.dumpApiCallerInlined(
+                            intBox.getAndIncrement(),
+                            callerName.getApiCallerMethodName(),
+                            newApiClassName,
+                            newApiClassMethodName,
+                            existingApiNewMethodName);
+                try {
+                  ZipUtils.writeToZipStream(
+                      out, classData.getSecond() + ".class", classData.getFirst(), ZipEntry.STORED);
+                } catch (IOException exception) {
+                  throw new RuntimeException(exception);
+                }
+                return classData.getSecond();
+              });
+      ZipUtils.writeToZipStream(
+          out, "com/example/softverificationsample/TestRunner.class", mainBytes, ZipEntry.STORED);
+    }
+  }
+
+  public static class Dumps implements Opcodes {
+
+    public static byte[] dumpMain(Supplier<String> targetSupplier) {
+
+      ClassWriter classWriter = new ClassWriter(0);
+      MethodVisitor methodVisitor;
+
+      classWriter.visit(
+          V1_8,
+          ACC_PUBLIC | ACC_SUPER,
+          "com/example/softverificationsample/TestRunner",
+          null,
+          "java/lang/Object",
+          null);
+
+      {
+        methodVisitor =
+            classWriter.visitMethod(
+                ACC_PUBLIC | ACC_STATIC, "run", "(Landroid/content/Context;)V", null, null);
+        methodVisitor.visitCode();
+        String target = targetSupplier.get();
+        while (target != null) {
+          methodVisitor.visitVarInsn(ALOAD, 0);
+          methodVisitor.visitMethodInsn(
+              INVOKESTATIC, target, "callApi", "(Landroid/content/Context;)V", false);
+          target = targetSupplier.get();
+        }
+        methodVisitor.visitInsn(RETURN);
+        methodVisitor.visitMaxs(1, 1);
+        methodVisitor.visitEnd();
+      }
+      classWriter.visitEnd();
+
+      return classWriter.toByteArray();
+    }
+
+    public static Pair<byte[], String> dumpApiCallerOutlined(
+        int index, String apiCallerName, String apiMethodCaller) {
+
+      ClassWriter classWriter = new ClassWriter(0);
+      MethodVisitor methodVisitor;
+
+      String binaryName =
+          "com/example/softverificationsample/ApiCallerOutlined" + (index > -1 ? index : "");
+
+      classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, binaryName, null, "java/lang/Object", null);
+
+      classWriter.visitInnerClass(
+          "android/os/Build$VERSION", "android/os/Build", "VERSION", ACC_PUBLIC | ACC_STATIC);
+
+      {
+        methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
+        methodVisitor.visitCode();
+        methodVisitor.visitVarInsn(ALOAD, 0);
+        methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+        methodVisitor.visitInsn(RETURN);
+        methodVisitor.visitMaxs(1, 1);
+        methodVisitor.visitEnd();
+      }
+      {
+        methodVisitor =
+            classWriter.visitMethod(
+                ACC_PUBLIC | ACC_STATIC, "callApi", "(Landroid/content/Context;)V", null, null);
+        methodVisitor.visitCode();
+        methodVisitor.visitFieldInsn(GETSTATIC, "android/os/Build$VERSION", "SDK_INT", "I");
+        methodVisitor.visitIntInsn(BIPUSH, 26);
+        Label label0 = new Label();
+        methodVisitor.visitJumpInsn(IF_ICMPLT, label0);
+        methodVisitor.visitVarInsn(ALOAD, 0);
+        methodVisitor.visitMethodInsn(
+            INVOKESTATIC, apiCallerName, apiMethodCaller, "(Landroid/content/Context;)V", false);
+        methodVisitor.visitLabel(label0);
+        methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+        methodVisitor.visitInsn(RETURN);
+        methodVisitor.visitMaxs(2, 1);
+        methodVisitor.visitEnd();
+      }
+      classWriter.visitEnd();
+
+      return Pair.create(classWriter.toByteArray(), binaryName);
+    }
+
+    public static Pair<byte[], String> dumpApiCallerInlined(
+        int index,
+        String apiMethodCaller,
+        String newApiClassName,
+        String newApiClassMethodName,
+        String existingApiNewMethodName) {
+
+      ClassWriter classWriter = new ClassWriter(0);
+      MethodVisitor methodVisitor;
+
+      String binaryName =
+          "com/example/softverificationsample/ApiCallerInlined" + (index > -1 ? index : "");
+
+      classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, binaryName, null, "java/lang/Object", null);
+
+      classWriter.visitInnerClass(
+          "android/os/Build$VERSION", "android/os/Build", "VERSION", ACC_PUBLIC | ACC_STATIC);
+
+      {
+        methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
+        methodVisitor.visitCode();
+        methodVisitor.visitVarInsn(ALOAD, 0);
+        methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+        methodVisitor.visitInsn(RETURN);
+        methodVisitor.visitMaxs(1, 1);
+        methodVisitor.visitEnd();
+      }
+      {
+        methodVisitor =
+            classWriter.visitMethod(
+                ACC_PUBLIC | ACC_STATIC, "callApi", "(Landroid/content/Context;)V", null, null);
+        methodVisitor.visitCode();
+        methodVisitor.visitFieldInsn(GETSTATIC, "android/os/Build$VERSION", "SDK_INT", "I");
+        methodVisitor.visitIntInsn(BIPUSH, 26);
+        Label label0 = new Label();
+        methodVisitor.visitJumpInsn(IF_ICMPLT, label0);
+        methodVisitor.visitVarInsn(ALOAD, 0);
+        methodVisitor.visitMethodInsn(
+            INVOKESTATIC, binaryName, apiMethodCaller, "(Landroid/content/Context;)V", false);
+        methodVisitor.visitLabel(label0);
+        methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+        methodVisitor.visitInsn(RETURN);
+        methodVisitor.visitMaxs(2, 1);
+        methodVisitor.visitEnd();
+      }
+      {
+        methodVisitor =
+            classWriter.visitMethod(
+                ACC_PUBLIC | ACC_STATIC,
+                "constructUnknownObject",
+                "(Landroid/content/Context;)V",
+                null,
+                null);
+        methodVisitor.visitCode();
+        methodVisitor.visitTypeInsn(NEW, newApiClassName);
+        methodVisitor.visitInsn(DUP);
+        methodVisitor.visitLdcInsn("CHANNEL_ID");
+        methodVisitor.visitLdcInsn("FOO");
+        methodVisitor.visitFieldInsn(
+            GETSTATIC, "android/app/NotificationManager", "IMPORTANCE_DEFAULT", "I");
+        methodVisitor.visitMethodInsn(
+            INVOKESPECIAL,
+            newApiClassName,
+            "<init>",
+            "(Ljava/lang/String;Ljava/lang/String;I)V",
+            false);
+        methodVisitor.visitVarInsn(ASTORE, 1);
+        methodVisitor.visitVarInsn(ALOAD, 1);
+        methodVisitor.visitLdcInsn("This is a test channel");
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL, newApiClassName, newApiClassMethodName, "(Ljava/lang/String;)V", false);
+        methodVisitor.visitInsn(RETURN);
+        methodVisitor.visitMaxs(5, 2);
+        methodVisitor.visitEnd();
+      }
+      {
+        methodVisitor =
+            classWriter.visitMethod(
+                ACC_PUBLIC | ACC_STATIC,
+                "callUnknownMethod",
+                "(Landroid/content/Context;)V",
+                null,
+                null);
+        methodVisitor.visitCode();
+        methodVisitor.visitVarInsn(ALOAD, 0);
+        methodVisitor.visitLdcInsn(Type.getType("Landroid/app/NotificationManager;"));
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL,
+            "android/content/Context",
+            "getSystemService",
+            "(Ljava/lang/Class;)Ljava/lang/Object;",
+            false);
+        methodVisitor.visitTypeInsn(CHECKCAST, "android/app/NotificationManager");
+        methodVisitor.visitVarInsn(ASTORE, 1);
+        methodVisitor.visitVarInsn(ALOAD, 1);
+        methodVisitor.visitInsn(ACONST_NULL);
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL,
+            "android/app/NotificationManager",
+            existingApiNewMethodName,
+            "(L" + newApiClassName + ";)V",
+            false);
+        methodVisitor.visitInsn(RETURN);
+        methodVisitor.visitMaxs(2, 2);
+        methodVisitor.visitEnd();
+      }
+      {
+        methodVisitor =
+            classWriter.visitMethod(
+                ACC_PUBLIC | ACC_STATIC,
+                "constructUnknownObjectAndCallUnknownMethod",
+                "(Landroid/content/Context;)V",
+                null,
+                null);
+        methodVisitor.visitCode();
+        methodVisitor.visitTypeInsn(NEW, newApiClassName);
+        methodVisitor.visitInsn(DUP);
+        methodVisitor.visitLdcInsn("CHANNEL_ID");
+        methodVisitor.visitLdcInsn("FOO");
+        methodVisitor.visitFieldInsn(
+            GETSTATIC, "android/app/NotificationManager", "IMPORTANCE_DEFAULT", "I");
+        methodVisitor.visitMethodInsn(
+            INVOKESPECIAL,
+            newApiClassName,
+            "<init>",
+            "(Ljava/lang/String;Ljava/lang/String;I)V",
+            false);
+        methodVisitor.visitVarInsn(ASTORE, 1);
+        methodVisitor.visitVarInsn(ALOAD, 1);
+        methodVisitor.visitLdcInsn("This is a test channel");
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL, newApiClassName, newApiClassMethodName, "(Ljava/lang/String;)V", false);
+        methodVisitor.visitVarInsn(ALOAD, 0);
+        methodVisitor.visitLdcInsn(Type.getType("Landroid/app/NotificationManager;"));
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL,
+            "android/content/Context",
+            "getSystemService",
+            "(Ljava/lang/Class;)Ljava/lang/Object;",
+            false);
+        methodVisitor.visitTypeInsn(CHECKCAST, "android/app/NotificationManager");
+        methodVisitor.visitVarInsn(ASTORE, 2);
+        methodVisitor.visitVarInsn(ALOAD, 2);
+        methodVisitor.visitVarInsn(ALOAD, 1);
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL,
+            "android/app/NotificationManager",
+            existingApiNewMethodName,
+            "(L" + newApiClassName + ";)V",
+            false);
+        methodVisitor.visitInsn(RETURN);
+        methodVisitor.visitMaxs(5, 3);
+        methodVisitor.visitEnd();
+      }
+      classWriter.visitEnd();
+
+      return Pair.create(classWriter.toByteArray(), binaryName);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/SoftVerificationErrorJarRunner.java b/src/test/java/com/android/tools/r8/SoftVerificationErrorJarRunner.java
new file mode 100644
index 0000000..80a2879
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/SoftVerificationErrorJarRunner.java
@@ -0,0 +1,118 @@
+// 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;
+
+import static com.android.tools.r8.SoftVerificationErrorJarGenerator.EXISTING_API_METHOD_NAME;
+import static com.android.tools.r8.SoftVerificationErrorJarGenerator.NEW_API_CLASS_METHOD_NAME;
+import static com.android.tools.r8.SoftVerificationErrorJarGenerator.NEW_API_CLASS_NAME;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.SoftVerificationErrorJarGenerator.ApiCallerName;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ApkUtils;
+import com.android.tools.r8.utils.BooleanBox;
+import com.android.tools.r8.utils.ZipUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class SoftVerificationErrorJarRunner extends TestBase {
+
+  public static Path DUMP_PATH =
+      Paths.get("third_party", "api-outlining", "simple-app-dump", "simple-app-dump.zip");
+  public static Path APK_PATH =
+      Paths.get("third_party", "api-outlining", "simple-app-dump", "app-release-unsigned.apk");
+
+  private final int numberOfClasses;
+  private final boolean isOutlined;
+
+  public SoftVerificationErrorJarRunner(int numberOfClasses, boolean isOutlined) {
+    this.numberOfClasses = numberOfClasses;
+    this.isOutlined = isOutlined;
+  }
+
+  public static void main(String[] args) throws Exception {
+    new SoftVerificationErrorJarRunner(1000, false).runTest();
+  }
+
+  public void runTest() throws Exception {
+
+    temp.create();
+
+    Path tempFolder = temp.newFolder().toPath();
+    Path outlineJar = tempFolder.resolve("outlined.jar");
+
+    SoftVerificationErrorJarGenerator.createJar(
+        outlineJar,
+        numberOfClasses,
+        isOutlined,
+        ApiCallerName.CONSTRUCT_AND_CALL_UNKNOWN,
+        NEW_API_CLASS_NAME,
+        NEW_API_CLASS_METHOD_NAME,
+        EXISTING_API_METHOD_NAME);
+
+    ZipUtils.unzip(DUMP_PATH.toString(), tempFolder.toFile());
+
+    File filteredProgramFolder = temp.newFolder();
+    BooleanBox seenTestRunner = new BooleanBox();
+    ZipUtils.unzip(
+        tempFolder.resolve("program.jar").toFile().toString(),
+        filteredProgramFolder,
+        zipEntry -> {
+          if (zipEntry.getName().equals("com/example/softverificationsample/TestRunner.class")) {
+            seenTestRunner.set();
+            return false;
+          }
+          return true;
+        });
+
+    assertTrue(seenTestRunner.get());
+
+    Path filteredProgramJar = tempFolder.resolve("filtered_program.jar");
+    ZipUtils.zip(filteredProgramJar, filteredProgramFolder.toPath());
+
+    // Build the app with R8.
+    Path output =
+        testForR8(Backend.DEX)
+            .addProgramFiles(outlineJar, filteredProgramJar)
+            .addClasspathFiles(tempFolder.resolve("classpath.jar"))
+            .addLibraryFiles(tempFolder.resolve("library.jar"))
+            // TODO(b/187496508): Modify keep rules to allow inlining and keep test code.
+            .addKeepRuleFiles(tempFolder.resolve("proguard.config"))
+            .addKeepRules("-keep class com.example.softverificationsample.* { *; }")
+            .setMinApi(AndroidApiLevel.M)
+            .allowUnusedProguardConfigurationRules()
+            .allowUnusedDontWarnPatterns()
+            .allowDiagnosticInfoMessages()
+            .compile()
+            .inspect(this::inspect)
+            .writeToZip();
+
+    Path finalApk = tempFolder.resolve("app-release-final.apk");
+    ProcessResult processResult = ApkUtils.apkMasseur(APK_PATH, output, finalApk);
+    // TODO(mkroghj): Figure out to have this command succeed when installing the apk
+    assertEquals(0, processResult.exitCode);
+  }
+
+  private void inspect(CodeInspector inspector) {
+    String name =
+        "com.example.softverificationsample."
+            + (isOutlined ? "ApiCallerOutlined" : "ApiCallerInlined")
+            + (numberOfClasses - 1);
+    ClassSubject clazz = inspector.clazz(name);
+    assertThat(clazz, isPresent());
+    if (isOutlined) {
+      ClassSubject apiCallerInlined =
+          inspector.clazz("com.example.softverificationsample.ApiCallerInlined");
+      assertThat(apiCallerInlined, isPresent());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/ApkUtils.java b/src/test/java/com/android/tools/r8/utils/ApkUtils.java
new file mode 100644
index 0000000..0fae220
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/ApkUtils.java
@@ -0,0 +1,30 @@
+// 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.utils;
+
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class ApkUtils {
+
+  public static ProcessResult apkMasseur(Path apk, Path dexSources, Path out) throws IOException {
+    ImmutableList.Builder<String> command =
+        new ImmutableList.Builder<String>()
+            .add("tools/apk_masseur.py")
+            .add("--dex")
+            .add(dexSources.toString())
+            .add("--out")
+            .add(out.toString())
+            .add("--install")
+            .add(apk.toString());
+    ProcessBuilder builder = new ProcessBuilder(command.build());
+    builder.directory(Paths.get(ToolHelper.THIRD_PARTY_DIR).toAbsolutePath().getParent().toFile());
+    return ToolHelper.runProcess(builder);
+  }
+}
diff --git a/third_party/api-outlining/simple-app-dump.tar.gz.sha1 b/third_party/api-outlining/simple-app-dump.tar.gz.sha1
new file mode 100644
index 0000000..cf9cc96
--- /dev/null
+++ b/third_party/api-outlining/simple-app-dump.tar.gz.sha1
@@ -0,0 +1 @@
+9913f083a29519e3fdc626a769ad33c7ba2498c1
\ No newline at end of file