In `BugReportTest`, use a custom `SecurityManager` to exercise attempting to halt the JVM.

This is similar to what `GoogleTestSecurityManager` currently does, but that is being turned down. As an added benefit, with this approach we get to check the exit code.

This needs to maintain compatibility with `GoogleTestSecurityManager` which prohibits setting another `SecurityManager`, hence the branching.

PiperOrigin-RevId: 438677602
diff --git a/src/test/java/com/google/devtools/build/lib/bugreport/BUILD b/src/test/java/com/google/devtools/build/lib/bugreport/BUILD
index a95e776..3960682 100644
--- a/src/test/java/com/google/devtools/build/lib/bugreport/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/bugreport/BUILD
@@ -16,6 +16,7 @@
     name = "bugreport_tests_lib",
     srcs = ["BugReportTest.java"],
     deps = [
+        "//src/java_tools/junitrunner/java/com/google/testing/junit/runner/util",
         "//src/main/java/com/google/devtools/build/lib/bugreport",
         "//src/main/java/com/google/devtools/build/lib/events",
         "//src/main/java/com/google/devtools/build/lib/util:custom_exit_code_publisher",
diff --git a/src/test/java/com/google/devtools/build/lib/bugreport/BugReportTest.java b/src/test/java/com/google/devtools/build/lib/bugreport/BugReportTest.java
index 09e50ae..05d736f 100644
--- a/src/test/java/com/google/devtools/build/lib/bugreport/BugReportTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bugreport/BugReportTest.java
@@ -36,23 +36,50 @@
 import com.google.devtools.build.lib.util.DetailedExitCode;
 import com.google.devtools.build.lib.util.ExitCode;
 import com.google.protobuf.ExtensionRegistry;
+import com.google.testing.junit.runner.util.GoogleTestSecurityManager;
 import com.google.testing.junit.testparameterinjector.TestParameter;
 import com.google.testing.junit.testparameterinjector.TestParameterInjector;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.security.Permission;
 import java.util.Arrays;
 import org.junit.After;
+import org.junit.AfterClass;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 
-/** Tests for {@link BugReport}. */
+/**
+ * Tests for {@link BugReport}.
+ *
+ * <p>Assuming that {@link GoogleTestSecurityManager} is not already installed, uses {@link
+ * ExitProhibitingSecurityManager} to exercise attempting to halt the JVM without aborting the whole
+ * test.
+ */
+// TODO(b/222158599): Remove handling for GoogleTestSecurityManager.
 @RunWith(TestParameterInjector.class)
 public final class BugReportTest {
 
+  @BeforeClass
+  public static void installCustomSecurityManager() {
+    if (System.getSecurityManager() == null) {
+      System.setSecurityManager(new ExitProhibitingSecurityManager());
+    } else {
+      assertThat(System.getSecurityManager()).isInstanceOf(GoogleTestSecurityManager.class);
+    }
+  }
+
+  @AfterClass
+  public static void uninstallCustomSecurityManager() {
+    if (System.getSecurityManager() instanceof ExitProhibitingSecurityManager) {
+      System.setSecurityManager(null);
+    }
+  }
+
   private enum CrashType {
     CRASH(ExitCode.BLAZE_INTERNAL_ERROR, Code.CRASH_UNKNOWN) {
       @Override
@@ -111,7 +138,12 @@
     FailureDetail expectedFailureDetail =
         createExpectedFailureDetail(t, crashType.expectedFailureDetailCode);
 
-    assertThrows(SecurityException.class, () -> BugReport.handleCrash(t));
+    // TODO(b/222158599): This should always be ExitException.
+    SecurityException e = assertThrows(SecurityException.class, () -> BugReport.handleCrash(t));
+    if (e instanceof ExitException) {
+      int code = ((ExitException) e).code;
+      assertThat(code).isEqualTo(crashType.expectedExitCode.getNumericExitCode());
+    }
     assertThrows(t.getClass(), BugReport::maybePropagateUnprocessedThrowableIfInTest);
 
     verify(mockRuntime)
@@ -126,8 +158,15 @@
     FailureDetail expectedFailureDetail =
         createExpectedFailureDetail(t, crashType.expectedFailureDetailCode);
 
-    assertThrows(
-        SecurityException.class, () -> BugReport.handleCrash(Crash.from(t), CrashContext.halt()));
+    // TODO(b/222158599): This should always be ExitException.
+    SecurityException e =
+        assertThrows(
+            SecurityException.class,
+            () -> BugReport.handleCrash(Crash.from(t), CrashContext.halt()));
+    if (e instanceof ExitException) {
+      int code = ((ExitException) e).code;
+      assertThat(code).isEqualTo(crashType.expectedExitCode.getNumericExitCode());
+    }
     assertThrows(t.getClass(), BugReport::maybePropagateUnprocessedThrowableIfInTest);
 
     verify(mockRuntime)
@@ -231,4 +270,26 @@
                                 Arrays.asList(t.getStackTrace()), StackTraceElement::toString))))
         .build();
   }
+
+  private static final class ExitException extends SecurityException {
+    private final int code;
+
+    ExitException(int code) {
+      super("Tried to exit JVM with code " + code);
+      this.code = code;
+    }
+  }
+
+  /** Instead of exiting the JVM, throws {@link ExitException} to keep the test alive. */
+  private static final class ExitProhibitingSecurityManager extends SecurityManager {
+
+    @Override
+    public void checkExit(int code) {
+      throw new ExitException(code);
+    }
+
+    @Override
+    public void checkPermission(Permission p) {} // Allow everything else.
+  }
 }
+