Synthesize null checks to allow inlining when the receiver is nullable

Bug: 130202534, 113859358
Change-Id: I1ed3ca3e1e8d8b0c42c654abf9583f985fce1e7f
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 85bc4cb..5c2ad3a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -517,6 +517,12 @@
     return this;
   }
 
+  public IRCode buildEmptyThrowingIRCode(AppView<? extends AppInfo> appView, Origin origin) {
+    DexCode emptyThrowingDexCode = buildEmptyThrowingDexCode();
+    emptyThrowingDexCode.setOwner(this);
+    return emptyThrowingDexCode.buildIR(this, appView, origin);
+  }
+
   /**
    * Generates a {@link DexCode} object for the given instructions.
    * <p>
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionIterator.java
index 63e83d7..f13f95c 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionIterator.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.code.Phi.RegisterReadType;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.IteratorUtils;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -183,6 +184,15 @@
   }
 
   @Override
+  public Value insertConstNullInstruction(IRCode code, InternalOptions options) {
+    ConstNumber constNumberInstruction = code.createConstNull();
+    // Note that we only keep position info for throwing instructions in release mode.
+    constNumberInstruction.setPosition(options.debug ? current.getPosition() : Position.none());
+    add(constNumberInstruction);
+    return constNumberInstruction.outValue();
+  }
+
+  @Override
   public void replaceCurrentInstructionWithThrowNull(
       AppView<? extends AppInfoWithSubtyping> appView,
       IRCode code,
@@ -205,15 +215,11 @@
 
     // Insert constant null before the instruction.
     previous();
-    ConstNumber constNumberInstruction = code.createConstNull();
-    // Note that we only keep position info for throwing instructions in release mode.
-    constNumberInstruction.setPosition(
-        appView.options().debug ? current.getPosition() : Position.none());
-    add(constNumberInstruction);
+    Value nullValue = insertConstNullInstruction(code, appView.options());
     next();
 
     // Replace the instruction by throw.
-    Throw throwInstruction = new Throw(constNumberInstruction.outValue());
+    Throw throwInstruction = new Throw(nullValue);
     for (Value inValue : current.inValues()) {
       if (inValue.hasLocalInfo()) {
         // Add this value as a debug value to avoid changing its live range.
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
index 8186ca2..cd2aaae 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.InternalOptions;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.ListIterator;
@@ -50,6 +51,8 @@
     // Intentionally empty.
   }
 
+  Value insertConstNullInstruction(IRCode code, InternalOptions options);
+
   /**
    * Replace the current instruction with null throwing instructions.
    *
diff --git a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionIterator.java b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionIterator.java
index 12de1ce..9055045 100644
--- a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionIterator.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.InternalOptions;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Set;
@@ -38,6 +39,11 @@
   }
 
   @Override
+  public Value insertConstNullInstruction(IRCode code, InternalOptions options) {
+    return currentBlockIterator.insertConstNullInstruction(code, options);
+  }
+
+  @Override
   public void replaceCurrentInstructionWithThrowNull(
       AppView<? extends AppInfoWithSubtyping> appView,
       IRCode code,
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index eb19549..ec51998 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -290,20 +290,6 @@
       return null;
     }
 
-    // We can only inline an instance method call if we preserve the null check semantic (which
-    // would throw NullPointerException if the receiver is null). Therefore we can inline only if
-    // one of the following conditions is true:
-    // * the candidate inlinee checks null receiver before any side effect
-    // * the receiver is known to be non-null
-    if (invoke.getReceiver().getTypeLattice().isNullable()
-        && !candidate.getOptimizationInfo().checksNullReceiverBeforeAnySideEffect()) {
-      if (info != null) {
-        info.exclude(invoke, "receiver for candidate can be null");
-      }
-      assert !inliner.appView.appInfo().forceInline.contains(candidate.method);
-      return null;
-    }
-
     Reason reason = computeInliningReason(candidate);
     if (!candidate.isInliningCandidate(method, reason, inliner.appView.appInfo())) {
       // Abort inlining attempt if the single target is not an inlining candidate.
@@ -320,7 +306,25 @@
     if (info != null) {
       info.include(invoke.getType(), candidate);
     }
-    return new InlineAction(candidate, invoke, reason);
+
+    InlineAction action = new InlineAction(candidate, invoke, reason);
+
+    if (invoke.getReceiver().getTypeLattice().isDefinitelyNull()) {
+      action.setShouldReturnEmptyThrowingCode();
+    } else {
+      // When inlining an instance method call, we need to preserve the null check for the receiver.
+      // Therefore, if the receiver may be null and the candidate inlinee does not throw if the
+      // receiver is null before any other side effect, then we must synthesize a null check.
+      if (invoke.getReceiver().getTypeLattice().isNullable()
+          && !candidate.getOptimizationInfo().checksNullReceiverBeforeAnySideEffect()) {
+        if (!appView.options().enableInliningOfInvokesWithNullableReceivers) {
+          return null;
+        }
+        action.setShouldSynthesizeNullCheckForReceiver();
+      }
+    }
+
+    return action;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index bf8b690..a08892f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -17,12 +17,14 @@
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Invoke;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.Throw;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueNumberGenerator;
 import com.android.tools.r8.ir.conversion.CallSiteInformation;
@@ -407,22 +409,76 @@
     public final Invoke invoke;
     final Reason reason;
 
+    private boolean shouldReturnEmptyThrowingCode;
+    private boolean shouldSynthesizeNullCheckForReceiver;
+
     InlineAction(DexEncodedMethod target, Invoke invoke, Reason reason) {
       this.target = target;
       this.invoke = invoke;
       this.reason = reason;
     }
 
+    void setShouldReturnEmptyThrowingCode() {
+      shouldReturnEmptyThrowingCode = true;
+    }
+
+    void setShouldSynthesizeNullCheckForReceiver() {
+      shouldSynthesizeNullCheckForReceiver = true;
+    }
+
     public InlineeWithReason buildInliningIR(
         DexEncodedMethod context,
         ValueNumberGenerator generator,
         AppView<? extends AppInfoWithSubtyping> appView,
         Position callerPosition) {
-      // Build the IR for a yet not processed method, and perform minimal IR processing.
       Origin origin = appView.appInfo().originFor(target.method.holder);
-      IRCode code = target.buildInliningIR(context, appView, generator, callerPosition, origin);
-      if (!target.isProcessed()) {
-        new LensCodeRewriter(appView).rewrite(code, target);
+
+      IRCode code;
+      if (shouldReturnEmptyThrowingCode) {
+        code = target.buildEmptyThrowingIRCode(appView, origin);
+      } else {
+        // Build the IR for a yet not processed method, and perform minimal IR processing.
+        code = target.buildInliningIR(context, appView, generator, callerPosition, origin);
+
+        // Insert a null check if this is needed to preserve the implicit null check for the
+        // receiver.
+        if (shouldSynthesizeNullCheckForReceiver) {
+          List<Value> arguments = code.collectArguments();
+          if (!arguments.isEmpty()) {
+            Value receiver = arguments.get(0);
+            assert receiver.isThis();
+
+            BasicBlock entryBlock = code.entryBlock();
+
+            // Insert a new block between the last argument instruction and the first actual
+            // instruction of the method.
+            BasicBlock throwBlock = entryBlock.listIterator(arguments.size()).split(code, 0, null);
+            assert !throwBlock.hasCatchHandlers();
+
+            // Link the entry block to the successor of the newly inserted block.
+            BasicBlock continuationBlock = throwBlock.unlinkSingleSuccessor();
+            entryBlock.link(continuationBlock);
+
+            // Replace the last instruction of the entry block, which is now a goto instruction,
+            // with an `if-eqz` instruction that jumps to the newly inserted block if the receiver
+            // is null.
+            If ifInstruction = new If(If.Type.EQ, receiver);
+            entryBlock.replaceLastInstruction(ifInstruction);
+            assert ifInstruction.getTrueTarget() == throwBlock;
+            assert ifInstruction.fallthroughBlock() == continuationBlock;
+
+            // Replace the single goto instruction in the newly inserted block by `throw null`.
+            InstructionListIterator iterator = throwBlock.listIterator();
+            Value nullValue = iterator.insertConstNullInstruction(code, appView.options());
+            iterator.next();
+            iterator.replaceCurrentInstruction(new Throw(nullValue));
+          } else {
+            assert false : "Unable to synthesize a null check for the receiver";
+          }
+        }
+        if (!target.isProcessed()) {
+          new LensCodeRewriter(appView).rewrite(code, target);
+        }
       }
       return new InlineeWithReason(code, reason);
     }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 2deea8a..8df2e21 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -138,6 +138,7 @@
   public boolean enableNonNullTracking = true;
   public boolean enableInlining =
       !Version.isDev() || System.getProperty("com.android.tools.r8.disableinlining") == null;
+  public boolean enableInliningOfInvokesWithNullableReceivers = true;
   public boolean enableClassInlining = true;
   public boolean enableClassStaticizer = true;
   public boolean enableInitializedClassesAnalysis = true;
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 8cf70a9..0ad4f8e 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -1065,10 +1065,10 @@
   }
 
   public static Path runtimeJar(TestParameters parameters) {
-    if (parameters.getBackend() == Backend.DEX) {
+    if (parameters.isDexRuntime()) {
       return ToolHelper.getAndroidJar(parameters.getRuntime().asDex().getMinApiLevel());
     } else {
-      assert parameters.getBackend() == Backend.CF;
+      assert parameters.isCfRuntime();
       return ToolHelper.getJava8RuntimeJar();
     }
   }
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/MissingClassesJoinTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/MissingClassesJoinTest.java
index 33fa52a..4d01f36 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/type/MissingClassesJoinTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/MissingClassesJoinTest.java
@@ -52,7 +52,7 @@
             .compile()
             .writeToZip();
 
-    if (parameters.getBackend() == Backend.DEX && !allowTypeErrors) {
+    if (parameters.isDexRuntime() && !allowTypeErrors) {
       testForD8()
           // Intentionally not adding ASub2 as a program class.
           .addProgramClasses(A.class, ASub1.class, Box.class, TestClass.class)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/ConstClassCanonicalizationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/ConstClassCanonicalizationTest.java
index 4e8fdc8..08d4273 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/ConstClassCanonicalizationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/ConstClassCanonicalizationTest.java
@@ -107,9 +107,7 @@
 
   @Test
   public void testJVMOutput() throws Exception {
-    assumeTrue(
-        "Only run JVM reference once (for CF backend)",
-        parameters.getBackend() == Backend.CF);
+    assumeTrue("Only run JVM reference once (for CF backend)", parameters.isCfRuntime());
     testForJvm()
         .addTestClasspath()
         .run(parameters.getRuntime(), MAIN)
@@ -118,7 +116,7 @@
 
   @Test
   public void testD8_incremental() throws Exception {
-    assumeTrue("Only run D8 for Dex backend", parameters.getBackend() == Backend.DEX);
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
 
     Path zipA = temp.newFile("a.zip").toPath().toAbsolutePath();
     testForD8()
@@ -173,7 +171,7 @@
 
   @Test
   public void testD8() throws Exception {
-    assumeTrue("Only run D8 for Dex backend", parameters.getBackend() == Backend.DEX);
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
 
     D8TestRunResult result =
         testForD8()
@@ -208,11 +206,11 @@
     // The number of expected const-class instructions differs because constant canonicalization is
     // only enabled for the DEX backend.
     int expectedMainCount =
-        parameters.getBackend() == Backend.CF ? ORIGINAL_MAIN_COUNT : CANONICALIZED_MAIN_COUNT;
+        parameters.isCfRuntime() ? ORIGINAL_MAIN_COUNT : CANONICALIZED_MAIN_COUNT;
     int expectedOuterCount =
-        parameters.getBackend() == Backend.CF ? ORIGINAL_OUTER_COUNT : CANONICALIZED_OUTER_COUNT;
+        parameters.isCfRuntime() ? ORIGINAL_OUTER_COUNT : CANONICALIZED_OUTER_COUNT;
     int expectedInnerCount =
-        parameters.getBackend() == Backend.CF ? ORIGINAL_INNER_COUNT : CANONICALIZED_INNER_COUNT;
+        parameters.isCfRuntime() ? ORIGINAL_INNER_COUNT : CANONICALIZED_INNER_COUNT;
     test(result, expectedMainCount, expectedOuterCount, expectedInnerCount);
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizationTest.java
index 7bb6c66..65336aa 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizationTest.java
@@ -108,9 +108,7 @@
 
   @Test
   public void testJVMOutput() throws Exception {
-    assumeTrue(
-        "Only run JVM reference once (for CF backend)",
-        parameters.getBackend() == Backend.CF);
+    assumeTrue("Only run JVM reference once (for CF backend)", parameters.isCfRuntime());
     testForJvm()
         .addTestClasspath()
         .run(parameters.getRuntime(), MAIN)
@@ -149,7 +147,7 @@
 
   @Test
   public void testD8() throws Exception {
-    assumeTrue("Only run D8 for Dex backend", parameters.getBackend() == Backend.DEX);
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
 
     D8TestRunResult result =
         testForD8()
@@ -180,12 +178,9 @@
             .setMinApi(parameters.getRuntime())
             .run(parameters.getRuntime(), MAIN)
             .assertSuccessWithOutput(JAVA_OUTPUT);
-    int expectedBooleanValueOfCount =
-        parameters.getBackend() == Backend.CF ? 6 : EXPECTED_BOOLEAN_VALUE_OF;
-    int expectedIntValueOfCount =
-        parameters.getBackend() == Backend.CF ? 5 : EXPECTED_INTEGER_VALUE_OF;
-    int expectedLongValueOfCount =
-        parameters.getBackend() == Backend.CF ? 10 : EXPECTED_LONG_VALUE_OF;
+    int expectedBooleanValueOfCount = parameters.isCfRuntime() ? 6 : EXPECTED_BOOLEAN_VALUE_OF;
+    int expectedIntValueOfCount = parameters.isCfRuntime() ? 5 : EXPECTED_INTEGER_VALUE_OF;
+    int expectedLongValueOfCount = parameters.isCfRuntime() ? 10 : EXPECTED_LONG_VALUE_OF;
     test(result, expectedBooleanValueOfCount, expectedIntValueOfCount, expectedLongValueOfCount);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/ObjectsRequireNonNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/ObjectsRequireNonNullTest.java
index 3d80826..9e22ade 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/ObjectsRequireNonNullTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/ObjectsRequireNonNullTest.java
@@ -117,9 +117,7 @@
 
   @Test
   public void testJvmOutput() throws Exception {
-    assumeTrue(
-        "Only run JVM reference once (for CF backend)",
-        parameters.getBackend() == Backend.CF);
+    assumeTrue("Only run JVM reference once (for CF backend)", parameters.isCfRuntime());
     testForJvm()
         .addTestClasspath()
         .run(parameters.getRuntime(), MAIN)
@@ -168,7 +166,7 @@
 
   @Test
   public void testD8() throws Exception {
-    assumeTrue("Only run D8 for Dex backend", parameters.getBackend() == Backend.DEX);
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
     D8TestRunResult result = testForD8()
         .debug()
         .addProgramClassesAndInnerClasses(MAIN)
@@ -188,7 +186,7 @@
 
   @Test
   public void testR8() throws Exception {
-    assumeTrue("CF disables move result optimization", parameters.getBackend() == Backend.DEX);
+    assumeTrue("CF disables move result optimization", parameters.isDexRuntime());
     R8TestRunResult result = testForR8(parameters.getBackend())
         .addProgramClassesAndInnerClasses(MAIN)
         .enableInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
index 78109d5..86245a3 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
@@ -9,6 +9,7 @@
 
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -42,37 +43,27 @@
 @RunWith(Parameterized.class)
 public class R8InliningTest extends TestBase {
 
-  private Backend backend;
-
   private static final String DEFAULT_DEX_FILENAME = "classes.dex";
   private static final String DEFAULT_MAP_FILENAME = "proguard.map";
+  private static final String NAME = "inlining";
+  private static final String KEEP_RULES_FILE = ToolHelper.EXAMPLES_DIR + NAME + "/keep-rules.txt";
 
-  @Parameters(name = "{0}, backend={1}, minification={2}, allowaccessmodification={3}")
+  @Parameters(name = "{1}, allow access modification: {0}")
   public static Collection<Object[]> data() {
-    return buildParameters(
-        ImmutableList.of("Inlining"),
-        ToolHelper.getBackends(),
-        BooleanUtils.values(),
-        BooleanUtils.values());
+    return buildParameters(BooleanUtils.values(), getTestParameters().withAllRuntimes().build());
   }
 
-  private final String name;
-  private final String keepRulesFile;
-  private final boolean minification;
   private final boolean allowAccessModification;
+  private final TestParameters parameters;
   private Path outputDir = null;
 
-  public R8InliningTest(
-      String name, Backend backend, boolean minification, boolean allowAccessModification) {
-    this.name = name.toLowerCase();
-    this.keepRulesFile = ToolHelper.EXAMPLES_DIR + this.name + "/keep-rules.txt";
-    this.backend = backend;
-    this.minification = minification;
+  public R8InliningTest(boolean allowAccessModification, TestParameters parameters) {
     this.allowAccessModification = allowAccessModification;
+    this.parameters = parameters;
   }
 
   private Path getInputFile() {
-    return Paths.get(ToolHelper.EXAMPLES_BUILD_DIR, name + FileUtils.JAR_EXTENSION);
+    return Paths.get(ToolHelper.EXAMPLES_BUILD_DIR, NAME + FileUtils.JAR_EXTENSION);
   }
 
   private Path getGeneratedDexFile() {
@@ -80,14 +71,13 @@
   }
 
   private List<Path> getGeneratedFiles(Path dir) throws IOException {
-    if (backend == Backend.DEX) {
+    if (parameters.isDexRuntime()) {
       return Collections.singletonList(dir.resolve(Paths.get(DEFAULT_DEX_FILENAME)));
-    } else {
-      assert backend == Backend.CF;
-      return Files.walk(dir)
-          .filter(f -> f.toString().endsWith(".class"))
-          .collect(Collectors.toList());
     }
+    assert parameters.isCfRuntime();
+    return Files.walk(dir)
+        .filter(f -> f.toString().endsWith(".class"))
+        .collect(Collectors.toList());
   }
 
   private List<Path> getGeneratedFiles() throws IOException {
@@ -103,18 +93,18 @@
   }
 
   private void generateR8Version(Path out, Path mapFile, boolean inlining) throws Exception {
-    assert backend == Backend.DEX || backend == Backend.CF;
+    assert parameters.isDexRuntime() || parameters.isCfRuntime();
     R8Command.Builder commandBuilder =
         R8Command.builder()
             .addProgramFiles(getInputFile())
-            .setOutput(out, outputMode(backend))
-            .addProguardConfigurationFiles(Paths.get(keepRulesFile))
-            .addLibraryFiles(TestBase.runtimeJar(backend))
+            .setOutput(out, outputMode(parameters.getBackend()))
+            .addProguardConfigurationFiles(Paths.get(KEEP_RULES_FILE))
+            .addLibraryFiles(TestBase.runtimeJar(parameters.getBackend()))
             .setDisableMinification(true);
     if (mapFile != null) {
       commandBuilder.setProguardMapOutputPath(mapFile);
     }
-    if (backend == Backend.DEX) {
+    if (parameters.isDexRuntime()) {
       commandBuilder.setMinApiLevel(AndroidApiLevel.M.getLevel());
     }
     if (allowAccessModification) {
@@ -129,6 +119,7 @@
           // that the class is therefore made abstract.
           o.enableClassInlining = false;
           o.enableInlining = inlining;
+          o.enableInliningOfInvokesWithNullableReceivers = false;
           o.inliningInstructionLimit = 6;
         });
   }
@@ -139,12 +130,12 @@
     Path mapFile = outputDir.resolve(DEFAULT_MAP_FILENAME);
     generateR8Version(outputDir, mapFile, true);
     String output;
-    if (backend == Backend.DEX) {
+    if (parameters.isDexRuntime()) {
       output =
           ToolHelper.runArtNoVerificationErrors(
               outputDir.resolve(DEFAULT_DEX_FILENAME).toString(), "inlining.Inlining");
     } else {
-      assert backend == Backend.CF;
+      assert parameters.isCfRuntime();
       output = ToolHelper.runJavaNoVerify(outputDir, "inlining.Inlining").stdout;
     }
 
@@ -217,7 +208,7 @@
     generateR8Version(nonInlinedOutputDir, null, false);
 
     long nonInlinedSize, inlinedSize;
-    if (backend == Backend.DEX) {
+    if (parameters.isDexRuntime()) {
       Path nonInlinedDexFile = nonInlinedOutputDir.resolve(DEFAULT_DEX_FILENAME);
       nonInlinedSize = Files.size(nonInlinedDexFile);
       inlinedSize = Files.size(getGeneratedDexFile());
@@ -227,7 +218,7 @@
         dump(getGeneratedDexFile(), "Inlining enabled");
       }
     } else {
-      assert backend == Backend.CF;
+      assert parameters.isCfRuntime();
       nonInlinedSize = sumOfClassFileSizes(nonInlinedOutputDir);
       inlinedSize = sumOfClassFileSizes(outputDir);
     }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java b/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java
index 171dac4..695cf0a 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java
@@ -5,25 +5,20 @@
 
 import static org.junit.Assert.assertEquals;
 
-import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ir.optimize.devirtualize.invokeinterface.A;
 import com.android.tools.r8.ir.optimize.devirtualize.invokeinterface.A0;
 import com.android.tools.r8.ir.optimize.devirtualize.invokeinterface.A1;
 import com.android.tools.r8.ir.optimize.devirtualize.invokeinterface.I;
 import com.android.tools.r8.ir.optimize.devirtualize.invokeinterface.Main;
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Streams;
-import java.nio.file.Path;
-import java.util.Collections;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -31,52 +26,40 @@
 @RunWith(Parameterized.class)
 public class InvokeInterfaceToInvokeVirtualTest extends TestBase {
 
-  private Backend backend;
+  private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "Backend: {0}")
-  public static Backend[] data() {
-    return ToolHelper.getBackends();
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().build();
   }
 
-  public InvokeInterfaceToInvokeVirtualTest(Backend backend) {
-    this.backend = backend;
-  }
-
-  private AndroidApp runR8(AndroidApp app, Class main, Path out) throws Exception {
-    R8Command command =
-        ToolHelper.addProguardConfigurationConsumer(
-                ToolHelper.prepareR8CommandBuilder(app, emptyConsumer(backend)),
-                pgConfig -> {
-                  pgConfig.setPrintMapping(true);
-                  pgConfig.setPrintMappingFile(out.resolve(ToolHelper.DEFAULT_PROGUARD_MAP_FILE));
-                })
-            .addProguardConfiguration(
-                ImmutableList.of(keepMainProguardConfiguration(main)), Origin.unknown())
-            .setOutput(out, outputMode(backend))
-            .addLibraryFiles(runtimeJar(backend))
-            .build();
-    return ToolHelper.runR8(command);
+  public InvokeInterfaceToInvokeVirtualTest(TestParameters parameters) {
+    this.parameters = parameters;
   }
 
   @Test
   public void listOfInterface() throws Exception {
-    byte[][] classes = {
-        ToolHelper.getClassAsBytes(I.class),
-        ToolHelper.getClassAsBytes(A.class),
-        ToolHelper.getClassAsBytes(A0.class),
-        ToolHelper.getClassAsBytes(A1.class),
-        ToolHelper.getClassAsBytes(Main.class)
-    };
-    String main = Main.class.getCanonicalName();
-    ProcessResult javaOutput = runOnJavaRaw(main, classes);
-    assertEquals(0, javaOutput.exitCode);
+    String expectedOutput = StringUtils.lines("0");
 
-    AndroidApp originalApp = buildAndroidApp(classes);
-    Path out = temp.getRoot().toPath();
-    AndroidApp processedApp = runR8(originalApp, Main.class, out);
+    if (parameters.isCfRuntime()) {
+      testForJvm()
+          .addTestClasspath()
+          .run(parameters.getRuntime(), Main.class)
+          .assertSuccessWithOutput(expectedOutput);
+    }
 
-    CodeInspector codeInspector = new CodeInspector(processedApp);
-    ClassSubject clazz = codeInspector.clazz(main);
+    CodeInspector inspector =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(I.class, A.class, A0.class, A1.class, Main.class)
+            .addKeepMainRule(Main.class)
+            .addOptionsModification(
+                options -> options.enableInliningOfInvokesWithNullableReceivers = false)
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), Main.class)
+            .assertSuccessWithOutput(expectedOutput)
+            .inspector();
+
+    ClassSubject clazz = inspector.clazz(Main.class);
     MethodSubject m = clazz.method(CodeInspector.MAIN);
     long numOfInvokeInterface =
         Streams.stream(m.iterateInstructions(InstructionSubject::isInvokeInterface)).count();
@@ -89,12 +72,5 @@
     long numOfCast = Streams.stream(m.iterateInstructions(InstructionSubject::isCheckCast)).count();
     // check-cast I ~> check-cast A0
     assertEquals(1, numOfCast);
-
-    ProcessResult output =
-        backend == Backend.DEX
-            ? runOnArtRaw(processedApp, main)
-            : runOnJavaRaw(processedApp, main, Collections.emptyList());
-    assertEquals(0, output.exitCode);
-    assertEquals(javaOutput.stdout.trim(), output.stdout.trim());
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineInvokeWithNullableReceiverTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineInvokeWithNullableReceiverTest.java
index fa613a7..c00b563 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineInvokeWithNullableReceiverTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineInvokeWithNullableReceiverTest.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertTrue;
 
@@ -65,12 +66,15 @@
     MethodSubject methodSubject = classSubject.mainMethod();
     assertThat(methodSubject, isPresent());
 
-    // TODO(b/130202534): A `throw` instruction should have been synthesized into main().
-    assertTrue(methodSubject.streamInstructions().noneMatch(InstructionSubject::isThrow));
+    // A `throw` instruction should have been synthesized into main().
+    assertTrue(methodSubject.streamInstructions().anyMatch(InstructionSubject::isThrow));
 
-    // TODO(b/130202534): Class A should be absent.
+    // Class A is still present because the instance flows into a phi that has a null-check.
     ClassSubject otherClassSubject = inspector.clazz(A.class);
     assertThat(otherClassSubject, isPresent());
+
+    // Method A.m() should no longer be present due to inlining.
+    assertThat(otherClassSubject.uniqueMethodWithName("m"), not(isPresent()));
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetClassTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetClassTest.java
index c78f4b9..d1bd6c0 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetClassTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetClassTest.java
@@ -137,9 +137,7 @@
 
   @Test
   public void testJVMOutput() throws Exception {
-    assumeTrue(
-        "Only run JVM reference once (for CF backend)",
-        parameters.getBackend() == Backend.CF);
+    assumeTrue("Only run JVM reference once (for CF backend)", parameters.isCfRuntime());
     testForJvm()
         .addTestClasspath()
         .run(parameters.getRuntime(), MAIN)
@@ -194,7 +192,7 @@
 
     @Test
   public void testD8() throws Exception {
-    assumeTrue("Only run D8 for Dex backend", parameters.getBackend() == Backend.DEX);
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
 
     // D8 debug.
     D8TestRunResult result =
@@ -234,7 +232,7 @@
 
     // The number of expected const-class instructions differs because constant canonicalization is
     // only enabled for the DEX backend.
-    int expectedConstClassCount = parameters.getBackend() == Backend.CF ? 7 : 5;
+    int expectedConstClassCount = parameters.isCfRuntime() ? 7 : 5;
 
     // R8 release, no minification.
     result =
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameInClassInitializerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameInClassInitializerTest.java
index 1ed8793..8ebc848 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameInClassInitializerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameInClassInitializerTest.java
@@ -53,7 +53,7 @@
   public void testJVMOutput() throws Exception {
     assumeTrue(
         "Only run JVM reference once (for CF backend)",
-        parameters.getBackend() == Backend.CF && !enableMinification);
+        parameters.isCfRuntime() && !enableMinification);
     testForJvm()
         .addTestClasspath()
         .run(parameters.getRuntime(), MAIN)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTest.java
index 4c80fca..6c21b28 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTest.java
@@ -219,7 +219,7 @@
   public void testJVMOutput() throws Exception {
     assumeTrue(
         "Only run JVM reference once (for CF backend)",
-        parameters.getBackend() == Backend.CF && !enableMinification);
+        parameters.isCfRuntime() && !enableMinification);
     testForJvm()
         .addTestClasspath()
         .run(parameters.getRuntime(), MAIN)
@@ -237,9 +237,7 @@
 
   @Test
   public void testD8() throws Exception {
-    assumeTrue(
-        "Only run D8 for Dex backend)",
-        parameters.getBackend() == Backend.DEX && !enableMinification);
+    assumeTrue("Only run D8 for Dex backend)", parameters.isDexRuntime() && !enableMinification);
 
     D8TestRunResult result =
         testForD8()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java
index 5e33843..0d9c034 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java
@@ -165,7 +165,7 @@
   public void testJVMOutput() throws Exception {
     assumeTrue(
         "Only run JVM reference once (for CF backend)",
-        parameters.getBackend() == Backend.CF && !enableMinification);
+        parameters.isCfRuntime() && !enableMinification);
     testForJvm()
         .addTestClasspath()
         .run(parameters.getRuntime(), MAIN)
@@ -182,9 +182,7 @@
 
   @Test
   public void testD8() throws Exception {
-    assumeTrue(
-        "Only run D8 for Dex backend",
-        parameters.getBackend() == Backend.DEX && !enableMinification);
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime() && !enableMinification);
 
     D8TestRunResult result =
         testForD8()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/NameThenLengthTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/NameThenLengthTest.java
index 85c935b..e030780 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/NameThenLengthTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/NameThenLengthTest.java
@@ -81,9 +81,7 @@
 
   @Test
   public void testJVMOutput() throws Exception {
-    assumeTrue(
-        "Only run JVM reference once (for CF backend)",
-        parameters.getBackend() == Backend.CF);
+    assumeTrue("Only run JVM reference once (for CF backend)", parameters.isCfRuntime());
     testForJvm()
         .addTestClasspath()
         .run(parameters.getRuntime(), MAIN)
@@ -135,7 +133,7 @@
 
   @Test
   public void testD8() throws Exception {
-    assumeTrue("Only run D8 for Dex backend", parameters.getBackend() == Backend.DEX);
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
 
     D8TestRunResult result =
         testForD8()
@@ -172,7 +170,7 @@
             .run(parameters.getRuntime(), MAIN)
             .assertSuccessWithOutput(JAVA_OUTPUT);
     // No canonicalization in CF.
-    int expectedConstNumber = parameters.getBackend() == Backend.CF ? 2 : 1;
+    int expectedConstNumber = parameters.isCfRuntime() ? 2 : 1;
     // TODO(b/125303292): NAME_LENGTH is still not computed at compile time.
     test(result, 1, 0, 0, expectedConstNumber);
   }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringCanonicalizationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringCanonicalizationTest.java
index 1eb5649..08dbd8c 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringCanonicalizationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringCanonicalizationTest.java
@@ -203,7 +203,7 @@
 
   @Test
   public void testD8() throws Exception {
-    assumeTrue("Only run D8 for Dex backend", parameters.getBackend() == Backend.DEX);
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
 
     D8TestCompileResult result =
         testForD8()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTest.java
index e0811eb..af13cb9 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTest.java
@@ -52,9 +52,7 @@
 
   @Test
   public void testJVMOutput() throws Exception {
-    assumeTrue(
-        "Only run JVM reference once (for CF backend)",
-        parameters.getBackend() == Backend.CF);
+    assumeTrue("Only run JVM reference once (for CF backend)", parameters.isCfRuntime());
     testForJvm()
         .addTestClasspath()
         .run(parameters.getRuntime(), MAIN)
@@ -118,7 +116,7 @@
 
   @Test
   public void testD8() throws Exception {
-    assumeTrue("Only run D8 for Dex backend", parameters.getBackend() == Backend.DEX);
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
 
     D8TestRunResult result =
         testForD8()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringContentCheckTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringContentCheckTest.java
index 9b77cbc..153e6b0 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringContentCheckTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringContentCheckTest.java
@@ -187,9 +187,7 @@
 
   @Test
   public void testJVMOutput() throws Exception {
-    assumeTrue(
-        "Only run JVM reference once (for CF backend)",
-        parameters.getBackend() == Backend.CF);
+    assumeTrue("Only run JVM reference once (for CF backend)", parameters.isCfRuntime());
     testForJvm()
         .addTestClasspath()
         .run(parameters.getRuntime(), MAIN)
@@ -240,7 +238,7 @@
 
   @Test
   public void testD8() throws Exception {
-    assumeTrue("Only run D8 for Dex backend", parameters.getBackend() == Backend.DEX);
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
 
     TestRunResult result =
         testForD8()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringInMonitorTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringInMonitorTest.java
index 833f2dc..c4ec59a 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringInMonitorTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringInMonitorTest.java
@@ -95,9 +95,7 @@
 
   @Test
   public void testJVMOutput() throws Exception {
-    assumeTrue(
-        "Only run JVM reference once (for CF backend)",
-        parameters.getBackend() == Backend.CF);
+    assumeTrue("Only run JVM reference once (for CF backend)", parameters.isCfRuntime());
     testForJvm()
         .addTestClasspath()
         .run(parameters.getRuntime(), MAIN)
@@ -119,7 +117,7 @@
     assertEquals(expectedConstStringCount1, count);
 
     // TODO(b/122302789): CfInstruction#getOffset()
-    if (parameters.getBackend() == Backend.DEX) {
+    if (parameters.isDexRuntime()) {
       Iterator<InstructionSubject> constStringIterator =
           mainMethod.iterateInstructions(i -> i.isConstString(JumboStringMode.ALLOW));
       // All const-string's in main(...) should be covered by try (or synthetic catch-all) region.
@@ -148,7 +146,7 @@
     assertEquals(expectedConstStringCount2, count);
 
     // In CF, we don't explicitly add monitor-{enter|exit} and catch-all for synchronized methods.
-    if (parameters.getBackend() == Backend.DEX) {
+    if (parameters.isDexRuntime()) {
       Iterator<InstructionSubject> constStringIterator =
           sync.iterateInstructions(i -> i.isConstString(JumboStringMode.ALLOW));
       // All const-string's in sync() should be covered by the synthetic catch-all regions.
@@ -178,7 +176,7 @@
 
   @Test
   public void testD8() throws Exception {
-    assumeTrue("Only run D8 for Dex backend", parameters.getBackend() == Backend.DEX);
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
 
     D8TestRunResult result =
         testForD8()
@@ -210,7 +208,7 @@
             .run(parameters.getRuntime(), MAIN)
             .assertSuccessWithOutput(JAVA_OUTPUT);
     // Due to the different behavior regarding constant canonicalization.
-    int expectedConstStringCount3 = parameters.getBackend() == Backend.CF ? 2 : 1;
+    int expectedConstStringCount3 = parameters.isCfRuntime() ? 2 : 1;
     test(result, 2, 2, expectedConstStringCount3);
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringIsEmptyTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringIsEmptyTest.java
index fba4ced..ffa3966 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringIsEmptyTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringIsEmptyTest.java
@@ -74,9 +74,7 @@
 
   @Test
   public void testJVMOutput() throws Exception {
-    assumeTrue(
-        "Only run JVM reference once (for CF backend)",
-        parameters.getBackend() == Backend.CF);
+    assumeTrue("Only run JVM reference once (for CF backend)", parameters.isCfRuntime());
     testForJvm()
         .addTestClasspath()
         .run(parameters.getRuntime(), MAIN)
@@ -116,7 +114,7 @@
 
   @Test
   public void testD8() throws Exception {
-    assumeTrue("Only run D8 for Dex backend", parameters.getBackend() == Backend.DEX);
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
 
     D8TestRunResult result =
         testForD8()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthTest.java
index 2d0bdea..735645c 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthTest.java
@@ -105,9 +105,7 @@
 
   @Test
   public void testJVMOutput() throws Exception {
-    assumeTrue(
-        "Only run JVM reference once (for CF backend)",
-        parameters.getBackend() == Backend.CF);
+    assumeTrue("Only run JVM reference once (for CF backend)", parameters.isCfRuntime());
     // TODO(b/119097175)
     if (!ToolHelper.isWindows()) {
       testForJvm()
@@ -148,7 +146,7 @@
 
   @Test
   public void testD8() throws Exception {
-    assumeTrue("Only run D8 for Dex backend", parameters.getBackend() == Backend.DEX);
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
 
     D8TestRunResult result =
         testForD8()
@@ -188,6 +186,6 @@
     if (!ToolHelper.isWindows()) {
       result.assertSuccessWithOutput(JAVA_OUTPUT);
     }
-    test(result, 0, parameters.getBackend() == Backend.DEX ? 5 : 6);
+    test(result, 0, parameters.isDexRuntime() ? 5 : 6);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringToStringTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringToStringTest.java
index ea664a8..d6af1d2 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringToStringTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringToStringTest.java
@@ -72,9 +72,7 @@
 
   @Test
   public void testJVMOutput() throws Exception {
-    assumeTrue(
-        "Only run JVM reference once (for CF backend)",
-        parameters.getBackend() == Backend.CF);
+    assumeTrue("Only run JVM reference once (for CF backend)", parameters.isCfRuntime());
     testForJvm()
         .addTestClasspath()
         .run(parameters.getRuntime(), MAIN)
@@ -108,7 +106,7 @@
 
   @Test
   public void testD8() throws Exception {
-    assumeTrue("Only run D8 for Dex backend", parameters.getBackend() == Backend.DEX);
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
 
     D8TestRunResult result =
         testForD8()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringValueOfTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringValueOfTest.java
index 512c54d..4d8dddb 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringValueOfTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringValueOfTest.java
@@ -148,9 +148,7 @@
 
   @Test
   public void testJVMOutput() throws Exception {
-    assumeTrue(
-        "Only run JVM reference once (for CF backend)",
-        parameters.getBackend() == Backend.CF);
+    assumeTrue("Only run JVM reference once (for CF backend)", parameters.isCfRuntime());
     testForJvm()
         .addTestClasspath()
         .run(parameters.getRuntime(), MAIN)
@@ -211,7 +209,7 @@
 
   @Test
   public void testD8() throws Exception {
-    assumeTrue("Only run D8 for Dex backend", parameters.getBackend() == Backend.DEX);
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
 
     TestRunResult result =
         testForD8()
@@ -246,8 +244,8 @@
             .run(parameters.getRuntime(), MAIN)
             .assertSuccessWithOutput(JAVA_OUTPUT);
     // Due to the different behavior regarding constant canonicalization.
-    int expectedNullCount = parameters.getBackend() == Backend.CF ? 2 : 1;
-    int expectedNullStringCount = parameters.getBackend() == Backend.CF ? 2 : 1;
+    int expectedNullCount = parameters.isCfRuntime() ? 2 : 1;
+    int expectedNullStringCount = parameters.isCfRuntime() ? 2 : 1;
     test(result, 3, expectedNullCount, expectedNullStringCount, 0, 1);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/NestedInterfaceMethodTest.java b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/NestedInterfaceMethodTest.java
index a1e2578..b5752cc 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/NestedInterfaceMethodTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/NestedInterfaceMethodTest.java
@@ -40,7 +40,7 @@
   public void test() throws Exception {
     String expectedOutput = StringUtils.lines("In A.m()", "In A.m()");
 
-    if (parameters.getBackend() == Backend.CF) {
+    if (parameters.isCfRuntime()) {
       testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expectedOutput);
     }
 
@@ -50,7 +50,11 @@
             .addKeepMainRule(TestClass.class)
             .enableInliningAnnotations()
             .enableMergeAnnotations()
-            .addOptionsModification(options -> options.enableDevirtualization = false)
+            .addOptionsModification(
+                options -> {
+                  options.enableDevirtualization = false;
+                  options.enableInliningOfInvokesWithNullableReceivers = false;
+                })
             .setMinApi(AndroidApiLevel.B)
             .run(parameters.getRuntime(), TestClass.class)
             .assertSuccessWithOutput(expectedOutput)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsCollisionTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsCollisionTest.java
index 9ed87ef..003a06f 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsCollisionTest.java
@@ -46,7 +46,7 @@
   public void test() throws Exception {
     String expectedOutput = StringUtils.lines("Hello world", "Hello world");
 
-    if (parameters.getBackend() == Backend.CF && !minification) {
+    if (parameters.isCfRuntime() && !minification) {
       testForJvm()
           .addTestClasspath()
           .run(parameters.getRuntime(), TestClass.class)
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
index 0f32501..11572ef 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
@@ -18,13 +18,13 @@
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Move;
+import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Set;
-
 import org.junit.Test;
 
 public class RegisterMoveSchedulerTest {
@@ -48,6 +48,11 @@
     }
 
     @Override
+    public Value insertConstNullInstruction(IRCode code, InternalOptions options) {
+      throw new Unimplemented();
+    }
+
+    @Override
     public void replaceCurrentInstructionWithThrowNull(
       AppView<? extends AppInfoWithSubtyping> appView,
       IRCode code,
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java
index 3bc1270..b76a4fb 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java
@@ -6,8 +6,8 @@
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
@@ -35,7 +35,7 @@
         "class_staticizer",
         mainClassName,
         false,
-        (app) -> {
+        app -> {
           CodeInspector inspector = new CodeInspector(app);
           assertThat(inspector.clazz("class_staticizer.Regular$Companion"), isPresent());
           assertThat(inspector.clazz("class_staticizer.Derived$Companion"), isPresent());
@@ -57,7 +57,7 @@
         "class_staticizer",
         mainClassName,
         true,
-        (app) -> {
+        app -> {
           CodeInspector inspector = new CodeInspector(app);
           assertThat(inspector.clazz("class_staticizer.Regular$Companion"), not(isPresent()));
           assertThat(inspector.clazz("class_staticizer.Derived$Companion"), not(isPresent()));
@@ -71,10 +71,14 @@
   protected void runTest(String folder, String mainClass,
       boolean enabled, AndroidAppInspector inspector) throws Exception {
     runTest(
-        folder, mainClass, null,
+        folder,
+        mainClass,
+        null,
         options -> {
           options.enableClassInlining = false;
           options.enableClassStaticizer = enabled;
-        }, inspector);
+          options.enableInliningOfInvokesWithNullableReceivers = false;
+        },
+        inspector);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinDuplicateAnnotationTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinDuplicateAnnotationTest.java
index c4979bc..8eb6a2e 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinDuplicateAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinDuplicateAnnotationTest.java
@@ -52,7 +52,7 @@
 
   @Test
   public void test_dex() {
-    assumeTrue("test DEX", parameters.getBackend() == Backend.DEX);
+    assumeTrue("test DEX", parameters.isDexRuntime());
     try {
       testForR8(parameters.getBackend())
           .addProgramFiles(getKotlinJarFile(FOLDER))
@@ -70,7 +70,7 @@
 
   @Test
   public void test_cf() throws Exception {
-    assumeTrue("test CF", parameters.getBackend() == Backend.CF);
+    assumeTrue("test CF", parameters.isCfRuntime());
     testForR8(parameters.getBackend())
         .addProgramFiles(getKotlinJarFile(FOLDER))
         .addKeepMainRule(MAIN)
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
index 76c9ba8..7f038cc 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
@@ -89,6 +89,9 @@
         o.enableClassStaticizer = false;
       };
 
+  private Consumer<InternalOptions> disableInliningOfInvokesWithNullableReceivers =
+      o -> o.enableInliningOfInvokesWithNullableReceivers = false;
+
   public R8KotlinPropertiesTest(
       KotlinTargetVersion targetVersion, boolean allowAccessModification) {
     super(targetVersion, allowAccessModification);
@@ -413,7 +416,7 @@
     runTest(
         PACKAGE_NAME,
         mainClass,
-        disableAggressiveClassOptimizations,
+        disableAggressiveClassOptimizations.andThen(disableInliningOfInvokesWithNullableReceivers),
         app -> {
           CodeInspector codeInspector = new CodeInspector(app);
           ClassSubject outerClass =
@@ -486,7 +489,7 @@
     runTest(
         PACKAGE_NAME,
         mainClass,
-        disableAggressiveClassOptimizations,
+        disableAggressiveClassOptimizations.andThen(disableInliningOfInvokesWithNullableReceivers),
         app -> {
           CodeInspector codeInspector = new CodeInspector(app);
           ClassSubject outerClass =
@@ -521,7 +524,7 @@
     runTest(
         PACKAGE_NAME,
         mainClass,
-        disableAggressiveClassOptimizations,
+        disableAggressiveClassOptimizations.andThen(disableInliningOfInvokesWithNullableReceivers),
         app -> {
           CodeInspector codeInspector = new CodeInspector(app);
           ClassSubject outerClass =
@@ -593,7 +596,7 @@
     runTest(
         PACKAGE_NAME,
         mainClass,
-        disableAggressiveClassOptimizations,
+        disableAggressiveClassOptimizations.andThen(disableInliningOfInvokesWithNullableReceivers),
         app -> {
           CodeInspector codeInspector = new CodeInspector(app);
           ClassSubject outerClass =
@@ -622,7 +625,7 @@
     runTest(
         PACKAGE_NAME,
         mainClass,
-        disableAggressiveClassOptimizations,
+        disableAggressiveClassOptimizations.andThen(disableInliningOfInvokesWithNullableReceivers),
         app -> {
           CodeInspector codeInspector = new CodeInspector(app);
           ClassSubject outerClass =
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/sourcelibrary/MemberResolutionTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/sourcelibrary/MemberResolutionTest.java
index dc1fe84..fd2fef0 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/sourcelibrary/MemberResolutionTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/sourcelibrary/MemberResolutionTest.java
@@ -108,7 +108,7 @@
             "AbstractChecker#check:PrivateInitialTag_AbstractChecker",
             "ConcreteChecker#check:NewTag");
 
-    if (parameters.getBackend() == Backend.CF) {
+    if (parameters.isCfRuntime()) {
       testForJvm()
           .addTestClasspath()
           .run(parameters.getRuntime(), MemberResolutionTestMain.class)
diff --git a/src/test/java/com/android/tools/r8/peephole/suffixsharing/IdenticalBlockSuffixSharingWithArrayTypesTest.java b/src/test/java/com/android/tools/r8/peephole/suffixsharing/IdenticalBlockSuffixSharingWithArrayTypesTest.java
index 5f664bf..3f89628 100644
--- a/src/test/java/com/android/tools/r8/peephole/suffixsharing/IdenticalBlockSuffixSharingWithArrayTypesTest.java
+++ b/src/test/java/com/android/tools/r8/peephole/suffixsharing/IdenticalBlockSuffixSharingWithArrayTypesTest.java
@@ -65,7 +65,7 @@
 
   @Test
   public void testD8() throws Exception {
-    assumeTrue("Only run D8 for DEX backend", parameters.getBackend() == Backend.DEX);
+    assumeTrue("Only run D8 for DEX backend", parameters.isDexRuntime());
 
     String expectedOutput = StringUtils.lines("42");
     testForD8()
diff --git a/src/test/java/com/android/tools/r8/shaking/ServiceLoaderTest.java b/src/test/java/com/android/tools/r8/shaking/ServiceLoaderTest.java
index 09567f2..c9251af 100644
--- a/src/test/java/com/android/tools/r8/shaking/ServiceLoaderTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ServiceLoaderTest.java
@@ -7,15 +7,15 @@
 import static com.android.tools.r8.references.Reference.classFromClass;
 import static com.android.tools.r8.references.Reference.methodFromMethod;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.core.IsNot.not;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.DataEntryResource;
 import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.graph.AppServices;
 import com.android.tools.r8.naming.AdaptResourceFileContentsTest.DataResourceConsumerForTesting;
 import com.android.tools.r8.origin.Origin;
@@ -38,21 +38,21 @@
 @RunWith(Parameterized.class)
 public class ServiceLoaderTest extends TestBase {
 
-  private final Backend backend;
   private final boolean includeWorldGreeter;
+  private final TestParameters parameters;
 
   private DataResourceConsumerForTesting dataResourceConsumer;
   private static final String LINE_COMMENT = "# This is a comment.";
   private static final String POSTFIX_COMMENT = "# POSTFIX_COMMENT";
 
-  @Parameters(name = "Backend: {0}, include WorldGreeter: {1}")
+  @Parameters(name = "{1}, include WorldGreeter: {0}")
   public static List<Object[]> data() {
-    return buildParameters(ToolHelper.getBackends(), BooleanUtils.values());
+    return buildParameters(BooleanUtils.values(), getTestParameters().withAllRuntimes().build());
   }
 
-  public ServiceLoaderTest(Backend backend, boolean includeWorldGreeter) {
-    this.backend = backend;
+  public ServiceLoaderTest(boolean includeWorldGreeter, TestParameters parameters) {
     this.includeWorldGreeter = includeWorldGreeter;
+    this.parameters = parameters;
   }
 
   @Test
@@ -72,7 +72,7 @@
     }
 
     R8TestRunResult result =
-        testForR8(backend)
+        testForR8(parameters.getBackend())
             .addInnerClasses(ServiceLoaderTest.class)
             .addKeepMainRule(TestClass.class)
             .addDataEntryResources(
@@ -85,9 +85,11 @@
                   dataResourceConsumer =
                       new DataResourceConsumerForTesting(options.dataResourceConsumer);
                   options.dataResourceConsumer = dataResourceConsumer;
+                  options.enableInliningOfInvokesWithNullableReceivers = false;
                 })
             .enableGraphInspector()
-            .run(TestClass.class)
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), TestClass.class)
             .assertSuccessWithOutput(expectedOutput);
 
     CodeInspector inspector = result.inspector();
@@ -163,7 +165,7 @@
     }
 
     CodeInspector inspector =
-        testForR8(backend)
+        testForR8(parameters.getBackend())
             .addInnerClasses(ServiceLoaderTest.class)
             .addKeepMainRule(OtherTestClass.class)
             .addDataEntryResources(
@@ -177,7 +179,8 @@
                       new DataResourceConsumerForTesting(options.dataResourceConsumer);
                   options.dataResourceConsumer = dataResourceConsumer;
                 })
-            .run(OtherTestClass.class)
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), OtherTestClass.class)
             .assertSuccessWithOutput(expectedOutput)
             .inspector();
 
diff --git a/src/test/java/com/android/tools/r8/shaking/proxy/ProxiesTest.java b/src/test/java/com/android/tools/r8/shaking/proxy/ProxiesTest.java
index 0fe8d7e..a955e0a 100644
--- a/src/test/java/com/android/tools/r8/shaking/proxy/ProxiesTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/proxy/ProxiesTest.java
@@ -6,20 +6,16 @@
 
 import static org.junit.Assert.assertEquals;
 
-import com.android.tools.r8.ClassFileConsumer;
-import com.android.tools.r8.DexIndexedConsumer;
-import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.graph.invokesuper.Consumer;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
-import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.proxy.testclasses.BaseInterface;
 import com.android.tools.r8.shaking.proxy.testclasses.Main;
 import com.android.tools.r8.shaking.proxy.testclasses.SubClass;
 import com.android.tools.r8.shaking.proxy.testclasses.SubInterface;
 import com.android.tools.r8.shaking.proxy.testclasses.TestClass;
-import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
@@ -29,6 +25,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Streams;
 import java.util.List;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -36,55 +33,47 @@
 
 @RunWith(Parameterized.class)
 public class ProxiesTest extends TestBase {
-  private Backend backend;
 
-  @Parameterized.Parameters(name = "Backend: {0}")
-  public static Backend[] data() {
-    return ToolHelper.getBackends();
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().build();
   }
 
-  public ProxiesTest(Backend backend) {
-    this.backend = backend;
+  public ProxiesTest(TestParameters parameters) {
+    this.parameters = parameters;
   }
 
-  private void runTest(List<String> additionalKeepRules, Consumer<CodeInspector> inspection,
-      String expectedResult)
+  private void runTest(
+      List<String> additionalKeepRules, Consumer<CodeInspector> inspection, String expectedResult)
       throws Exception {
-    Class mainClass = Main.class;
-    R8Command.Builder builder = R8Command.builder();
-    builder.addProgramFiles(ToolHelper.getClassFilesForTestPackage(mainClass.getPackage()));
-    builder.addProguardConfiguration(ImmutableList.of(
-        "-keep class " + mainClass.getCanonicalName() + " {",
-        // Keep x, y and z to avoid them being inlined into main.
-        "  private void x(com.android.tools.r8.shaking.proxy.testclasses.BaseInterface);",
-        "  private void y(com.android.tools.r8.shaking.proxy.testclasses.SubInterface);",
-        "  private void z(com.android.tools.r8.shaking.proxy.testclasses.TestClass);",
-        "  private void z(com.android.tools.r8.shaking.proxy.testclasses.SubClass);",
-        "  public static void main(java.lang.String[]);",
-        "}",
-        "-dontobfuscate"),
-        Origin.unknown()
-    );
-    builder.addProguardConfiguration(additionalKeepRules, Origin.unknown());
-    if (backend == Backend.DEX) {
-      builder
-          .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
-          .addLibraryFiles(ToolHelper.getDefaultAndroidJar());
-    } else {
-      assert backend == Backend.CF;
-      builder
-          .setProgramConsumer(ClassFileConsumer.emptyConsumer())
-          .addLibraryFiles(ToolHelper.getJava8RuntimeJar());
-    }
-    AndroidApp app = ToolHelper.runR8(builder.build(), o -> {
-      o.enableDevirtualization = false;
-      // Tests indirectly check if a certain method is inlined or not, where the target method has
-      // at least 4 instructions.
-      o.inliningInstructionLimit = 4;
-    });
-    inspection.accept(new CodeInspector(app));
-    String result = backend == Backend.DEX ? runOnArt(app, mainClass) : runOnJava(app, mainClass);
-    assertEquals(StringUtils.withNativeLineSeparator(expectedResult), result);
+    testForR8(parameters.getBackend())
+        .addProgramFiles(ToolHelper.getClassFilesForTestPackage(Main.class.getPackage()))
+        .addKeepMainRule(Main.class)
+        .addKeepRules(
+            "-keep class " + Main.class.getCanonicalName() + " {",
+            // Keep x, y and z to avoid them being inlined into main.
+            "  private void x(com.android.tools.r8.shaking.proxy.testclasses.BaseInterface);",
+            "  private void y(com.android.tools.r8.shaking.proxy.testclasses.SubInterface);",
+            "  private void z(com.android.tools.r8.shaking.proxy.testclasses.TestClass);",
+            "  private void z(com.android.tools.r8.shaking.proxy.testclasses.SubClass);",
+            "}")
+        .addKeepRules(additionalKeepRules)
+        .addOptionsModification(
+            o -> {
+              o.enableDevirtualization = false;
+              o.enableInliningOfInvokesWithNullableReceivers = false;
+              // Tests indirectly check if a certain method is inlined or not, where the target
+              // method has at least 4 instructions.
+              o.inliningInstructionLimit = 4;
+            })
+        .noMinification()
+        .setMinApi(parameters.getRuntime())
+        .compile()
+        .inspect(inspection)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(StringUtils.withNativeLineSeparator(expectedResult));
   }
 
   private int countInstructionInX(CodeInspector inspector, Predicate<InstructionSubject> invoke) {
diff --git a/src/test/java/com/android/tools/r8/shaking/reflection/ReflectiveNewInstanceTest.java b/src/test/java/com/android/tools/r8/shaking/reflection/ReflectiveNewInstanceTest.java
index f62d2d9..544a7ee 100644
--- a/src/test/java/com/android/tools/r8/shaking/reflection/ReflectiveNewInstanceTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/reflection/ReflectiveNewInstanceTest.java
@@ -41,7 +41,7 @@
     String expectedOutput =
         StringUtils.lines("Success", "Success", "Success", "Success", "Success");
 
-    if (parameters.getBackend() == Backend.CF) {
+    if (parameters.isCfRuntime()) {
       testForJvm()
           .addTestClasspath()
           .run(parameters.getRuntime(), TestClass.class)