forked from bazelbuild/bazel
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a shutdown hook for tests that detects
System.exit
.
We can no longer prevent code being tested from exiting when `System.exit` is called, but we can at least show where it happened. We do this by the examining the stack traces of all threads in a shutdown hook, looking for `Runtime.exit`. (`System.exit` calls `Runtime.exit`, and users can also call it directly.) We cannot detect `Runtime.halt` this way, unfortunately. Recent JDK versions can be configured to log where `System.exit` is called, but that logging is fiddly and its configuration may interfere with that of the code under test. Also, we may later add to the shutdown hook so that it also detects which test method is currently executing, in case that isn't in the thread that called `System.exit`. PiperOrigin-RevId: 703190140 Change-Id: Ie4ba4728c41adb868f7fc75b630851b900e7e27d
- Loading branch information
1 parent
3e1fba6
commit 5fc2b01
Showing
8 changed files
with
198 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
68 changes: 68 additions & 0 deletions
68
...runner/java/com/google/testing/junit/runner/internal/SystemExitDetectingShutdownHook.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
// Copyright 2024 The Bazel Authors. All Rights Reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License.package com.google.testing.junit.runner.internal; | ||
|
||
package com.google.testing.junit.runner.internal; | ||
|
||
import static java.util.Arrays.stream; | ||
import static java.util.stream.Collectors.toList; | ||
|
||
import java.io.PrintStream; | ||
import java.util.List; | ||
|
||
/** | ||
* Shutdown hook to detect when the shutdown is due to someone calling {@code System.exit}. Tests | ||
* should never do that. Previously we had a security manager that intercepted such calls. The JDK | ||
* will remove security managers in a future release, so instead we just detect when it happens and | ||
* print a stack trace so users can find and fix the call. | ||
*/ | ||
public class SystemExitDetectingShutdownHook { | ||
public static Thread newShutdownHook(PrintStream testRunnerOut) { | ||
Runnable hook = () -> { | ||
boolean foundRuntimeExit = false; | ||
for (StackTraceElement[] stack : Thread.getAllStackTraces().values()) { | ||
@SuppressWarnings("JdkCollectors") // can't use ImmutableList here | ||
List<String> framesStartingWithRuntimeExit = | ||
stream(stack) | ||
.dropWhile( | ||
frame -> | ||
!frame.getClassName().equals("java.lang.Runtime") | ||
|| !frame.getMethodName().equals("exit")) | ||
.map(SystemExitDetectingShutdownHook::frameString) | ||
.collect(toList()); | ||
if (!framesStartingWithRuntimeExit.isEmpty()) { | ||
foundRuntimeExit = true; | ||
testRunnerOut.println("\nSystem.exit or Runtime.exit was called!"); | ||
testRunnerOut.println(String.join("\n", framesStartingWithRuntimeExit)); | ||
} | ||
} | ||
if (foundRuntimeExit) { | ||
// We must call halt rather than exit, because exit would lead to a deadlock. We use a | ||
// hopefully unique exit code to make it easier to identify this case. | ||
Runtime.getRuntime().halt(121); | ||
} | ||
}; | ||
return new Thread(hook, "SystemExitDetectingShutdownHook"); | ||
} | ||
|
||
private static String frameString(StackTraceElement frame) { | ||
return String.format( | ||
" at %s.%s(%s:%d)", | ||
frame.getClassName(), | ||
frame.getMethodName(), | ||
frame.getFileName(), | ||
frame.getLineNumber()); | ||
} | ||
|
||
private SystemExitDetectingShutdownHook() {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
...ols/junitrunner/javatests/com/google/testing/junit/runner/ProgramThatCallsSystemExit.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
// Copyright 2024 The Bazel Authors. All Rights Reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package com.google.testing.junit.runner; | ||
|
||
import com.google.testing.junit.runner.internal.SystemExitDetectingShutdownHook; | ||
|
||
/** | ||
* A simple program that installs a shutdown hook using {@link SystemExitDetectingShutdownHook} and | ||
* then calls {@code System.exit}. This is used to test that the shutdown hook detects the {@code | ||
* System.exit} call and prints a stack trace. | ||
*/ | ||
public final class ProgramThatCallsSystemExit { | ||
public static void main(String[] args) { | ||
Thread shutdownHook = SystemExitDetectingShutdownHook.newShutdownHook(System.err); | ||
Runtime.getRuntime().addShutdownHook(shutdownHook); | ||
System.exit(0); | ||
} | ||
|
||
private ProgramThatCallsSystemExit() {} | ||
} |
50 changes: 50 additions & 0 deletions
50
...tools/junitrunner/javatests/com/google/testing/junit/runner/system_exit_detecting_test.sh
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
#!/bin/bash | ||
# | ||
# Copyright 2024 The Bazel Authors. All Rights Reserved. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# | ||
# Tests that the SystemExitDetectingShutdownHook correctly detects a call to | ||
# System.exit and prints a stack trace. | ||
|
||
[ -z "$TEST_SRCDIR" ] && { echo "TEST_SRCDIR not set!" >&2; exit 1; } | ||
|
||
# Load the unit-testing framework | ||
source "$1" || \ | ||
{ echo "Failed to load unit-testing framework $1" >&2; exit 1; } | ||
|
||
set +o errexit | ||
|
||
PROGRAM_THAT_CALLS_SYSTEM_EXIT_JAR="$2" | ||
readonly PROGRAM_THAT_CALLS_SYSTEM_EXIT_JAR | ||
JAVA_HOME="$3" | ||
readonly JAVA_HOME | ||
EXPECTED_STACK_FILE="$4" | ||
readonly EXPECTED_STACK_FILE | ||
|
||
function test_prints_stack_trace_on_system_exit() { | ||
local output_file="${TEST_TMPDIR}/output.txt" | ||
|
||
"${JAVA_HOME}/bin/java" -jar "${PROGRAM_THAT_CALLS_SYSTEM_EXIT_JAR}" \ | ||
2> "${output_file}" | ||
assert_equals 121 $? | ||
|
||
# We expect the output to be a stack trace that ends with the main method of | ||
# ProgramThatCallsSystemExit. We use sed to avoid hardcoding the exact line | ||
# numbers in the stack trace. | ||
sed -i 's/:[0-9][0-9]*/:XXX/' "${output_file}" | ||
diff -u "${output_file}" "${EXPECTED_STACK_FILE}" || \ | ||
fail "Stack trace does not match expected stack trace" | ||
} | ||
|
||
run_suite "system_exit_detecting_test" |
5 changes: 5 additions & 0 deletions
5
...r/javatests/com/google/testing/junit/runner/testdata/system_exit_detecting_test_stack.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
|
||
System.exit or Runtime.exit was called! | ||
at java.lang.Runtime.exit(Runtime.java:XXX) | ||
at java.lang.System.exit(System.java:XXX) | ||
at com.google.testing.junit.runner.ProgramThatCallsSystemExit.main(ProgramThatCallsSystemExit.java:XXX) |