Add an API for cancelling the compilation before completion.

Bug: b/274628704
Change-Id: Ic52967b32aa2ec4454f5b4ffb4b458674e91b698
diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
index f251ecb..285ef1b 100644
--- a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
@@ -65,6 +65,7 @@
   private final List<ArtProfileForRewriting> artProfilesForRewriting;
   private final List<StartupProfileProvider> startupProfileProviders;
   private final ClassConflictResolver classConflictResolver;
+  private final CancelCompilationChecker cancelCompilationChecker;
 
   BaseCompilerCommand(boolean printHelp, boolean printVersion) {
     super(printHelp, printVersion);
@@ -87,6 +88,7 @@
     artProfilesForRewriting = null;
     startupProfileProviders = null;
     classConflictResolver = null;
+    cancelCompilationChecker = null;
   }
 
   BaseCompilerCommand(
@@ -109,7 +111,8 @@
       boolean isAndroidPlatformBuild,
       List<ArtProfileForRewriting> artProfilesForRewriting,
       List<StartupProfileProvider> startupProfileProviders,
-      ClassConflictResolver classConflictResolver) {
+      ClassConflictResolver classConflictResolver,
+      CancelCompilationChecker cancelCompilationChecker) {
     super(app);
     assert minApiLevel > 0;
     assert mode != null;
@@ -132,6 +135,7 @@
     this.artProfilesForRewriting = artProfilesForRewriting;
     this.startupProfileProviders = startupProfileProviders;
     this.classConflictResolver = classConflictResolver;
+    this.cancelCompilationChecker = cancelCompilationChecker;
   }
 
   /**
@@ -244,6 +248,10 @@
     return classConflictResolver;
   }
 
+  public CancelCompilationChecker getCancelCompilationChecker() {
+    return cancelCompilationChecker;
+  }
+
   DumpInputFlags getDumpInputFlags() {
     return dumpInputFlags;
   }
@@ -287,6 +295,7 @@
     private List<ArtProfileForRewriting> artProfilesForRewriting = new ArrayList<>();
     private List<StartupProfileProvider> startupProfileProviders = new ArrayList<>();
     private ClassConflictResolver classConflictResolver = null;
+    private CancelCompilationChecker cancelCompilationChecker = null;
 
     abstract CompilationMode defaultCompilationMode();
 
@@ -733,6 +742,21 @@
     }
 
     /**
+     * Set a cancellation checker.
+     *
+     * <p>The cancellation checker will be periodically called to check if the compilation should be
+     * cancelled before completion.
+     */
+    public B setCancelCompilationChecker(CancelCompilationChecker checker) {
+      this.cancelCompilationChecker = checker;
+      return self();
+    }
+
+    public CancelCompilationChecker getCancelCompilationChecker() {
+      return cancelCompilationChecker;
+    }
+
+    /**
      * Allow to skip to dump into file and dump into directory instruction, this is primarily used
      * for chained compilation in L8 so there are no duplicated dumps.
      */
diff --git a/src/main/java/com/android/tools/r8/CancelCompilationChecker.java b/src/main/java/com/android/tools/r8/CancelCompilationChecker.java
new file mode 100644
index 0000000..f08cde1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/CancelCompilationChecker.java
@@ -0,0 +1,26 @@
+// Copyright (c) 2023, 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;
+
+/** Client supplied checker to allow cancelling a compilation before completion. */
+@Keep
+public interface CancelCompilationChecker {
+
+  /**
+   * Callback that is called periodically by the compiler to check if the current compilation should
+   * be cancelled before completion. The method should return true or false. The behavior of
+   * throwing an exception from this method will also abort the compilation but the compiler will
+   * treat such cases as an unexpected runtime failure and not as an expected client cancellation.
+   *
+   * <p>If this method returns true at any point before compilation has successfully completed, then
+   * the compiler will exit with a {@link CompilationFailedException}. It is *not* possible to
+   * cancel the cancellation.
+   *
+   * <p>This method may be called from multiple compiler threads. It is up to the client to ensure
+   * thread safety in determining if compilation should be cancelled.
+   *
+   * @return True if the compilation should be cancelled, otherwise false.
+   */
+  boolean cancel();
+}
diff --git a/src/main/java/com/android/tools/r8/CompilationFailedException.java b/src/main/java/com/android/tools/r8/CompilationFailedException.java
index 0078c06..94c7fde 100644
--- a/src/main/java/com/android/tools/r8/CompilationFailedException.java
+++ b/src/main/java/com/android/tools/r8/CompilationFailedException.java
@@ -10,19 +10,15 @@
 @Keep
 public class CompilationFailedException extends Exception {
 
-  public CompilationFailedException() {
-    super("Compilation failed to complete");
-  }
+  private final boolean cancelled;
 
-  public CompilationFailedException(Throwable cause) {
-    this("Compilation failed to complete", cause);
-  }
-
-  public CompilationFailedException(String message, Throwable cause) {
+  CompilationFailedException(String message, Throwable cause, boolean cancelled) {
     super(message, cause);
+    this.cancelled = cancelled;
   }
 
-  public CompilationFailedException(String message) {
-    super(message);
+  /** True if the compilation was cancelled by {@link CancelCompilationChecker} otherwise false. */
+  public boolean wasCancelled() {
+    return cancelled;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 1abfbd2..49f8155 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -491,6 +491,7 @@
           getArtProfilesForRewriting(),
           getStartupProfileProviders(),
           getClassConflictResolver(),
+          getCancelCompilationChecker(),
           factory);
     }
   }
@@ -586,6 +587,7 @@
       List<ArtProfileForRewriting> artProfilesForRewriting,
       List<StartupProfileProvider> startupProfileProviders,
       ClassConflictResolver classConflictResolver,
+      CancelCompilationChecker cancelCompilationChecker,
       DexItemFactory factory) {
     super(
         inputApp,
@@ -607,7 +609,8 @@
         isAndroidPlatformBuild,
         artProfilesForRewriting,
         startupProfileProviders,
-        classConflictResolver);
+        classConflictResolver,
+        cancelCompilationChecker);
     this.intermediate = intermediate;
     this.globalSyntheticsConsumer = globalSyntheticsConsumer;
     this.syntheticInfoConsumer = syntheticInfoConsumer;
@@ -749,6 +752,8 @@
         ProgramClassCollection.wrappedConflictResolver(
             getClassConflictResolver(), internal.reporter);
 
+    internal.cancelCompilationChecker = getCancelCompilationChecker();
+
     internal.tool = Tool.D8;
     internal.setDumpInputFlags(getDumpInputFlags());
     internal.dumpOptions = dumpOptions();
diff --git a/src/main/java/com/android/tools/r8/InternalCompilationFailedExceptionUtils.java b/src/main/java/com/android/tools/r8/InternalCompilationFailedExceptionUtils.java
new file mode 100644
index 0000000..18e96ff
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/InternalCompilationFailedExceptionUtils.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2023, 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;
+
+/** Non-API class to allocate compilation failed exceptions without exposing the constructor. */
+public final class InternalCompilationFailedExceptionUtils {
+
+  private InternalCompilationFailedExceptionUtils() {}
+
+  public static CompilationFailedException createForTesting() {
+    return createForTesting("Compilation failed to complete", null);
+  }
+
+  public static CompilationFailedException createForTesting(String message) {
+    return createForTesting(message, null);
+  }
+
+  public static CompilationFailedException createForTesting(Throwable cause) {
+    return createForTesting("Compilation failed to complete", cause);
+  }
+
+  public static CompilationFailedException createForTesting(String message, Throwable cause) {
+    return create(message, cause, false);
+  }
+
+  public static CompilationFailedException create(
+      String message, Throwable cause, boolean cancelled) {
+    return new CompilationFailedException(message, cause, cancelled);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/L8Command.java b/src/main/java/com/android/tools/r8/L8Command.java
index 6bac5df..7a28ef3 100644
--- a/src/main/java/com/android/tools/r8/L8Command.java
+++ b/src/main/java/com/android/tools/r8/L8Command.java
@@ -102,6 +102,7 @@
       DumpInputFlags dumpInputFlags,
       MapIdProvider mapIdProvider,
       ClassConflictResolver classConflictResolver,
+      CancelCompilationChecker cancelCompilationChecker,
       DexItemFactory factory) {
     super(
         inputApp,
@@ -123,7 +124,8 @@
         false,
         null,
         null,
-        classConflictResolver);
+        classConflictResolver,
+        cancelCompilationChecker);
     this.d8Command = d8Command;
     this.r8Command = r8Command;
     this.desugaredLibrarySpecification = desugaredLibrarySpecification;
@@ -228,6 +230,8 @@
         ProgramClassCollection.wrappedConflictResolver(
             getClassConflictResolver(), internal.reporter);
 
+    internal.cancelCompilationChecker = getCancelCompilationChecker();
+
     if (!DETERMINISTIC_DEBUGGING) {
       assert internal.threadCount == ThreadUtils.NOT_SPECIFIED;
       internal.threadCount = getThreadCount();
@@ -456,6 +460,7 @@
           getDumpInputFlags(),
           getMapIdProvider(),
           getClassConflictResolver(),
+          getCancelCompilationChecker(),
           factory);
     }
   }
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 5fc02c3..6003038 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -659,7 +659,8 @@
               getAndroidPlatformBuild(),
               getArtProfilesForRewriting(),
               getStartupProfileProviders(),
-              getClassConflictResolver());
+              getClassConflictResolver(),
+              getCancelCompilationChecker());
 
       if (inputDependencyGraphConsumer != null) {
         inputDependencyGraphConsumer.finished();
@@ -938,7 +939,8 @@
       boolean isAndroidPlatformBuild,
       List<ArtProfileForRewriting> artProfilesForRewriting,
       List<StartupProfileProvider> startupProfileProviders,
-      ClassConflictResolver classConflictResolver) {
+      ClassConflictResolver classConflictResolver,
+      CancelCompilationChecker cancelCompilationChecker) {
     super(
         inputApp,
         mode,
@@ -959,7 +961,8 @@
         isAndroidPlatformBuild,
         artProfilesForRewriting,
         startupProfileProviders,
-        classConflictResolver);
+        classConflictResolver,
+        cancelCompilationChecker);
     assert proguardConfiguration != null;
     assert mainDexKeepRules != null;
     this.mainDexKeepRules = mainDexKeepRules;
@@ -1176,6 +1179,8 @@
         ProgramClassCollection.wrappedConflictResolver(
             getClassConflictResolver(), internal.reporter);
 
+    internal.cancelCompilationChecker = getCancelCompilationChecker();
+
     if (!DETERMINISTIC_DEBUGGING) {
       assert internal.threadCount == ThreadUtils.NOT_SPECIFIED;
       internal.threadCount = getThreadCount();
diff --git a/src/main/java/com/android/tools/r8/retrace/Retrace.java b/src/main/java/com/android/tools/r8/retrace/Retrace.java
index 8f619ac..3d2740a 100644
--- a/src/main/java/com/android/tools/r8/retrace/Retrace.java
+++ b/src/main/java/com/android/tools/r8/retrace/Retrace.java
@@ -425,7 +425,10 @@
       run(mappedArgs, retraceDiagnosticsHandler);
     } catch (Throwable t) {
       throw failWithFakeEntry(
-          retraceDiagnosticsHandler, t, RetraceFailedException::new, RetraceAbortException.class);
+          retraceDiagnosticsHandler,
+          t,
+          (message, cause, ignore) -> new RetraceFailedException(message, cause),
+          RetraceAbortException.class);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/CancelCompilationException.java b/src/main/java/com/android/tools/r8/utils/CancelCompilationException.java
new file mode 100644
index 0000000..0070efe
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/CancelCompilationException.java
@@ -0,0 +1,6 @@
+// Copyright (c) 2023, 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;
+
+public class CancelCompilationException extends RuntimeException {}
diff --git a/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java b/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java
index ef5c3ca..959a25e 100644
--- a/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.InternalCompilationFailedExceptionUtils;
 import com.android.tools.r8.ResourceException;
 import com.android.tools.r8.StringConsumer;
 import com.android.tools.r8.Version;
@@ -19,7 +20,6 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
-import java.util.function.BiFunction;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
@@ -87,13 +87,16 @@
   private static CompilationFailedException failCompilation(
       Reporter reporter, Throwable topMostException) {
     return failWithFakeEntry(
-        reporter, topMostException, CompilationFailedException::new, AbortException.class);
+        reporter,
+        topMostException,
+        InternalCompilationFailedExceptionUtils::create,
+        AbortException.class);
   }
 
   public static <T extends Exception, A extends Exception> T failWithFakeEntry(
       DiagnosticsHandler diagnosticsHandler,
       Throwable topMostException,
-      BiFunction<String, Throwable, T> newException,
+      TriFunction<String, Throwable, Boolean, T> newException,
       Class<A> abortException) {
     // Find inner-most cause of the failure and compute origin, position and reported for the path.
     boolean hasBeenReported = false;
@@ -122,8 +125,13 @@
       innerMostCause.addSuppressed(topMostException);
     }
 
+    boolean cancelled =
+        topMostException instanceof CancelCompilationException
+            || innerMostCause instanceof CancelCompilationException;
+    assert !cancelled || topMostException == innerMostCause;
+
     // If no abort is seen, the exception is not reported, so report it now.
-    if (!hasBeenReported) {
+    if (!cancelled && !hasBeenReported) {
       diagnosticsHandler.error(new ExceptionDiagnostic(innerMostCause, origin, position));
     }
 
@@ -136,7 +144,7 @@
       message.append(", origin: ").append(origin);
     }
     // Create the final exception object.
-    T rethrow = newException.apply(message.toString(), innerMostCause);
+    T rethrow = newException.apply(message.toString(), innerMostCause, cancelled);
     // Replace its stack by the cause stack and insert version info at the top.
     String filename = "Version_" + Version.LABEL + ".java";
     StackTraceElement versionElement =
@@ -244,6 +252,8 @@
       Origin origin, Position position, Supplier<T> action) {
     try {
       return action.get();
+    } catch (CancelCompilationException e) {
+      throw e;
     } catch (RuntimeException e) {
       throw OriginAttachmentException.wrap(e, origin, position);
     }
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 6c09eb1..8ce6323 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -6,6 +6,7 @@
 import static com.android.tools.r8.utils.AndroidApiLevel.B;
 import static com.android.tools.r8.utils.SystemPropertyUtils.parseSystemPropertyForDevelopmentOrDefault;
 
+import com.android.tools.r8.CancelCompilationChecker;
 import com.android.tools.r8.ClassFileConsumer;
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.DataResourceConsumer;
@@ -167,6 +168,27 @@
     return itemFactory;
   }
 
+  // Internal state signifying that the compilation is cancelled.
+  // The state can only ever transition from false to true.
+  private final AtomicBoolean cancelled = new AtomicBoolean(false);
+  public CancelCompilationChecker cancelCompilationChecker = null;
+
+  public boolean checkIfCancelled() {
+    if (cancelCompilationChecker == null) {
+      assert !cancelled.get();
+      return false;
+    }
+    if (cancelled.get()) {
+      return true;
+    }
+    if (cancelCompilationChecker.cancel()) {
+      cancelled.set(true);
+      return true;
+    }
+    // Return the cancelled value in case another thread has cancelled.
+    return cancelled.get();
+  }
+
   public boolean hasProguardConfiguration() {
     return proguardConfiguration != null;
   }
diff --git a/src/main/java/com/android/tools/r8/utils/Timing.java b/src/main/java/com/android/tools/r8/utils/Timing.java
index 3947cae..dc7db26 100644
--- a/src/main/java/com/android/tools/r8/utils/Timing.java
+++ b/src/main/java/com/android/tools/r8/utils/Timing.java
@@ -62,11 +62,60 @@
     return Timing.EMPTY;
   }
 
+  private static class TimingWithCancellation extends Timing {
+    private final InternalOptions options;
+    private final Timing timing;
+
+    TimingWithCancellation(InternalOptions options, Timing timing) {
+      super("<cancel>", false);
+      this.options = options;
+      this.timing = timing;
+    }
+
+    @Override
+    public TimingMerger beginMerger(String title, int numberOfThreads) {
+      return timing.beginMerger(title, numberOfThreads);
+    }
+
+    @Override
+    public void begin(String title) {
+      if (options.checkIfCancelled()) {
+        throw new CancelCompilationException();
+      }
+      timing.begin(title);
+    }
+
+    @Override
+    public <E extends Exception> void time(String title, ThrowingAction<E> action) throws E {
+      timing.time(title, action);
+    }
+
+    @Override
+    public <T, E extends Exception> T time(String title, ThrowingSupplier<T, E> supplier) throws E {
+      return timing.time(title, supplier);
+    }
+
+    @Override
+    public void end() {
+      timing.end();
+    }
+
+    @Override
+    public void report() {
+      timing.report();
+    }
+  }
+
   public static Timing create(String title, InternalOptions options) {
     // We also create a timer when running assertions to validate wellformedness of the node stack.
-    return options.printTimes || InternalOptions.assertionsEnabled()
-        ? new Timing(title, options.printMemory)
-        : Timing.empty();
+    Timing timing =
+        options.printTimes || InternalOptions.assertionsEnabled()
+            ? new Timing(title, options.printMemory)
+            : Timing.empty();
+    if (options.cancelCompilationChecker != null) {
+      return new TimingWithCancellation(options, timing);
+    }
+    return timing;
   }
 
   public static Timing create(String title, boolean printMemory) {
diff --git a/src/test/java/com/android/tools/r8/CommandTestBase.java b/src/test/java/com/android/tools/r8/CommandTestBase.java
index 6c72b39..1ea2212 100644
--- a/src/test/java/com/android/tools/r8/CommandTestBase.java
+++ b/src/test/java/com/android/tools/r8/CommandTestBase.java
@@ -96,7 +96,7 @@
             try {
               command.getReporter().failIfPendingErrors();
             } catch (RuntimeException e) {
-              throw new CompilationFailedException();
+              throw InternalCompilationFailedExceptionUtils.createForTesting();
             }
           });
       fail("Failure expected");
@@ -144,7 +144,7 @@
             try {
               command.getReporter().failIfPendingErrors();
             } catch (RuntimeException e) {
-              throw new CompilationFailedException();
+              throw InternalCompilationFailedExceptionUtils.createForTesting();
             }
           });
       fail("Failure expected");
@@ -190,7 +190,7 @@
             try {
               command.getReporter().failIfPendingErrors();
             } catch (RuntimeException e) {
-              throw new CompilationFailedException();
+              throw InternalCompilationFailedExceptionUtils.createForTesting();
             }
           });
       fail("Expected failure");
@@ -209,7 +209,7 @@
             try {
               command.getReporter().failIfPendingErrors();
             } catch (RuntimeException e) {
-              throw new CompilationFailedException();
+              throw InternalCompilationFailedExceptionUtils.createForTesting();
             }
           });
       fail("Expected failure");
diff --git a/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java b/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
index 2808f05..444d14b 100644
--- a/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
@@ -197,7 +197,7 @@
       return new ExternalR8TestCompileResult(
           getState(), outputJar, processResult, proguardMap, getMinApiLevel(), getOutputMode());
     } catch (IOException e) {
-      throw new CompilationFailedException(e);
+      throw InternalCompilationFailedExceptionUtils.createForTesting(e);
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/ProguardTestBuilder.java b/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
index d91646a..ac6e335 100644
--- a/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
@@ -114,14 +114,14 @@
       ProcessBuilder pbuilder = new ProcessBuilder(command);
       ProcessResult result = ToolHelper.runProcess(pbuilder, getStdoutForTesting());
       if (result.exitCode != 0) {
-        throw new CompilationFailedException(result.toString());
+        throw InternalCompilationFailedExceptionUtils.createForTesting(result.toString());
       }
       String proguardMap =
           Files.exists(mapFile) ? FileUtils.readTextFile(mapFile, Charsets.UTF_8) : "";
       return new ProguardTestCompileResult(
           result, getState(), outJar, getMinApiLevel(), proguardMap);
     } catch (IOException e) {
-      throw new CompilationFailedException(e);
+      throw InternalCompilationFailedExceptionUtils.createForTesting(e);
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
index 1bbaf31..71c9dc4 100644
--- a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
+++ b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.compilerapi.androidplatformbuild.AndroidPlatformBuildApiTest;
 import com.android.tools.r8.compilerapi.artprofiles.ArtProfilesForRewritingApiTest;
 import com.android.tools.r8.compilerapi.assertionconfiguration.AssertionConfigurationTest;
+import com.android.tools.r8.compilerapi.cancelcompilationchecker.CancelCompilationCheckerTest;
 import com.android.tools.r8.compilerapi.classconflictresolver.ClassConflictResolverTest;
 import com.android.tools.r8.compilerapi.desugardependencies.DesugarDependenciesTest;
 import com.android.tools.r8.compilerapi.diagnostics.ProguardKeepRuleDiagnosticsApiTest;
@@ -62,7 +63,7 @@
           ExtractMarkerApiTest.ApiTest.class);
 
   private static final List<Class<? extends CompilerApiTest>> CLASSES_PENDING_BINARY_COMPATIBILITY =
-      ImmutableList.of();
+      ImmutableList.of(CancelCompilationCheckerTest.ApiTest.class);
 
   private final TemporaryFolder temp;
 
diff --git a/src/test/java/com/android/tools/r8/compilerapi/cancelcompilationchecker/CancelCompilationCheckerTest.java b/src/test/java/com/android/tools/r8/compilerapi/cancelcompilationchecker/CancelCompilationCheckerTest.java
new file mode 100644
index 0000000..8afacb4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/compilerapi/cancelcompilationchecker/CancelCompilationCheckerTest.java
@@ -0,0 +1,123 @@
+// Copyright (c) 2023, 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.compilerapi.cancelcompilationchecker;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.CancelCompilationChecker;
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.D8;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.R8;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.compilerapi.CompilerApiTest;
+import com.android.tools.r8.compilerapi.CompilerApiTestRunner;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.IntBox;
+import java.util.function.BooleanSupplier;
+import org.junit.Test;
+
+public class CancelCompilationCheckerTest extends CompilerApiTestRunner {
+
+  public CancelCompilationCheckerTest(TestParameters parameters) {
+    super(parameters);
+  }
+
+  @Override
+  public Class<? extends CompilerApiTest> binaryTestClass() {
+    return ApiTest.class;
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    // Use an integer box to delay the cancel until some time internal in the compiler.
+    IntBox i = new IntBox();
+    try {
+      new ApiTest(ApiTest.PARAMETERS).runD8(() -> i.incrementAndGet() > 10);
+      fail("excepted cancelled");
+    } catch (CompilationFailedException e) {
+      assertTrue(e.wasCancelled());
+    }
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    // Use an integer box to delay the cancel until some time internal in the compiler.
+    IntBox i = new IntBox();
+    try {
+      new ApiTest(ApiTest.PARAMETERS).runR8(() -> i.incrementAndGet() > 20);
+      fail("excepted cancelled");
+    } catch (CompilationFailedException e) {
+      assertTrue(e.wasCancelled());
+    }
+  }
+
+  public static class ApiTest extends CompilerApiTest {
+
+    public ApiTest(Object parameters) {
+      super(parameters);
+    }
+
+    public void runD8(BooleanSupplier supplier) throws Exception {
+      D8.run(
+          D8Command.builder()
+              .addClassProgramData(getBytesForClass(getMockClass()), Origin.unknown())
+              .addLibraryFiles(getJava8RuntimeJar())
+              .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+              .setCancelCompilationChecker(
+                  new CancelCompilationChecker() {
+                    @Override
+                    public boolean cancel() {
+                      return supplier.getAsBoolean();
+                    }
+                  })
+              .build());
+    }
+
+    public void runR8(BooleanSupplier supplier) throws Exception {
+      R8.run(
+          R8Command.builder()
+              .addClassProgramData(getBytesForClass(getMockClass()), Origin.unknown())
+              .addLibraryFiles(getJava8RuntimeJar())
+              .setDisableTreeShaking(true)
+              .setDisableMinification(true)
+              .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+              .setCancelCompilationChecker(
+                  new CancelCompilationChecker() {
+                    @Override
+                    public boolean cancel() {
+                      return supplier.getAsBoolean();
+                    }
+                  })
+              .build());
+    }
+
+    @Test
+    public void testD8() throws Exception {
+      try {
+        runD8(() -> true);
+      } catch (CompilationFailedException e) {
+        if (e.wasCancelled()) {
+          return;
+        }
+      }
+      throw new AssertionError("expected cancelled");
+    }
+
+    @Test
+    public void testR8() throws Exception {
+      try {
+        runR8(() -> true);
+      } catch (CompilationFailedException e) {
+        if (e.wasCancelled()) {
+          return;
+        }
+      }
+      throw new AssertionError("expected cancelled");
+    }
+  }
+}