aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Srbecky <dsrbecky@google.com>2021-02-24 15:52:50 +0000
committerDavid Srbecky <dsrbecky@google.com>2021-02-24 15:52:50 +0000
commit2a0687c8905d9f9d1c8d4e1dee255a8ceabba5e5 (patch)
tree6d7beb9cc06ae2c1f5743ea63c6c4c6c8f63a970
parent7db56fab8af99e12a7644349d14109235ec584b9 (diff)
downloadjunit-2a0687c8905d9f9d1c8d4e1dee255a8ceabba5e5.tar.gz
Revert "Remove support for stuck threads"
Revert submission 1601635 Reason for revert: b/181123058 Reverted Changes: I8f5cd1266:Remove support for stuck threads Ifdb59336d:Remove DisableOnDebug (new in 4.12) as it is not s... I6abae5aed:Extra generic type information to aid certain java... I5ec909df6:Upgrade external/junit to 4.13.2 Change-Id: Icf0a7b88b0004e60cd4139cb10be62af5bdc68d2
-rw-r--r--README.version1
-rw-r--r--src/main/java/org/junit/internal/runners/statements/FailOnTimeout.java155
-rw-r--r--src/main/java/org/junit/rules/Timeout.java33
3 files changed, 186 insertions, 3 deletions
diff --git a/README.version b/README.version
index 577b0dd..d7a4bf1 100644
--- a/README.version
+++ b/README.version
@@ -5,4 +5,3 @@ BugComponent: 40416
Local Changes:
Extra generic type information to aid certain javacs.
Remove DisableOnDebug (new in 4.12) as it is not supported on Android
- Remove support for stuck threads
diff --git a/src/main/java/org/junit/internal/runners/statements/FailOnTimeout.java b/src/main/java/org/junit/internal/runners/statements/FailOnTimeout.java
index 9fad35b..9362cc1 100644
--- a/src/main/java/org/junit/internal/runners/statements/FailOnTimeout.java
+++ b/src/main/java/org/junit/internal/runners/statements/FailOnTimeout.java
@@ -1,5 +1,8 @@
package org.junit.internal.runners.statements;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
@@ -7,6 +10,9 @@ import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import org.junit.internal.management.ManagementFactory;
+import org.junit.internal.management.ThreadMXBean;
+import org.junit.runners.model.MultipleFailureException;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestTimedOutException;
@@ -14,6 +20,7 @@ public class FailOnTimeout extends Statement {
private final Statement originalStatement;
private final TimeUnit timeUnit;
private final long timeout;
+ private final boolean lookForStuckThread;
/**
* Returns a new builder for building an instance.
@@ -40,6 +47,7 @@ public class FailOnTimeout extends Statement {
originalStatement = statement;
timeout = builder.timeout;
timeUnit = builder.unit;
+ lookForStuckThread = builder.lookForStuckThread;
}
/**
@@ -48,6 +56,7 @@ public class FailOnTimeout extends Statement {
* @since 4.12
*/
public static class Builder {
+ private boolean lookForStuckThread = false;
private long timeout = 0;
private TimeUnit unit = TimeUnit.SECONDS;
@@ -80,6 +89,20 @@ public class FailOnTimeout extends Statement {
}
/**
+ * Specifies whether to look for a stuck thread. If a timeout occurs and this
+ * feature is enabled, the test will look for a thread that appears to be stuck
+ * and dump its backtrace. This feature is experimental. Behavior may change
+ * after the 4.12 release in response to feedback.
+ *
+ * @param enable {@code true} to enable the feature
+ * @return {@code this} for method chaining.
+ */
+ public Builder withLookingForStuckThread(boolean enable) {
+ this.lookForStuckThread = enable;
+ return this;
+ }
+
+ /**
* Builds a {@link FailOnTimeout} instance using the values in this builder,
* wrapping the given statement.
*
@@ -97,7 +120,8 @@ public class FailOnTimeout extends Statement {
public void evaluate() throws Throwable {
CallableStatement callable = new CallableStatement();
FutureTask<Throwable> task = new FutureTask<Throwable>(callable);
- Thread thread = new Thread(task, "Time-limited test");
+ ThreadGroup threadGroup = threadGroupForNewThread();
+ Thread thread = new Thread(threadGroup, task, "Time-limited test");
thread.setDaemon(true);
thread.start();
callable.awaitStarted();
@@ -107,6 +131,31 @@ public class FailOnTimeout extends Statement {
}
}
+ private ThreadGroup threadGroupForNewThread() {
+ if (!lookForStuckThread) {
+ // Use the default ThreadGroup (usually the one from the current
+ // thread).
+ return null;
+ }
+
+ // Create the thread in a new ThreadGroup, so if the time-limited thread
+ // becomes stuck, getStuckThread() can find the thread likely to be the
+ // culprit.
+ ThreadGroup threadGroup = new ThreadGroup("FailOnTimeoutGroup");
+ if (!threadGroup.isDaemon()) {
+ // Mark the new ThreadGroup as a daemon thread group, so it will be
+ // destroyed after the time-limited thread completes. By ensuring the
+ // ThreadGroup is destroyed, any data associated with the ThreadGroup
+ // (ex: via java.beans.ThreadGroupContext) is destroyed.
+ try {
+ threadGroup.setDaemon(true);
+ } catch (SecurityException e) {
+ // Swallow the exception to keep the same behavior as in JUnit 4.12.
+ }
+ }
+ return threadGroup;
+ }
+
/**
* Wait for the test task, returning the exception thrown by the test if the
* test failed, an exception indicating a timeout if the test timed out, or
@@ -131,12 +180,114 @@ public class FailOnTimeout extends Statement {
private Exception createTimeoutException(Thread thread) {
StackTraceElement[] stackTrace = thread.getStackTrace();
+ final Thread stuckThread = lookForStuckThread ? getStuckThread(thread) : null;
Exception currThreadException = new TestTimedOutException(timeout, timeUnit);
if (stackTrace != null) {
currThreadException.setStackTrace(stackTrace);
thread.interrupt();
}
- return currThreadException;
+ if (stuckThread != null) {
+ Exception stuckThreadException =
+ new Exception("Appears to be stuck in thread " +
+ stuckThread.getName());
+ stuckThreadException.setStackTrace(getStackTrace(stuckThread));
+ return new MultipleFailureException(
+ Arrays.<Throwable>asList(currThreadException, stuckThreadException));
+ } else {
+ return currThreadException;
+ }
+ }
+
+ /**
+ * Retrieves the stack trace for a given thread.
+ * @param thread The thread whose stack is to be retrieved.
+ * @return The stack trace; returns a zero-length array if the thread has
+ * terminated or the stack cannot be retrieved for some other reason.
+ */
+ private StackTraceElement[] getStackTrace(Thread thread) {
+ try {
+ return thread.getStackTrace();
+ } catch (SecurityException e) {
+ return new StackTraceElement[0];
+ }
+ }
+
+ /**
+ * Determines whether the test appears to be stuck in some thread other than
+ * the "main thread" (the one created to run the test). This feature is experimental.
+ * Behavior may change after the 4.12 release in response to feedback.
+ * @param mainThread The main thread created by {@code evaluate()}
+ * @return The thread which appears to be causing the problem, if different from
+ * {@code mainThread}, or {@code null} if the main thread appears to be the
+ * problem or if the thread cannot be determined. The return value is never equal
+ * to {@code mainThread}.
+ */
+ private Thread getStuckThread(Thread mainThread) {
+ List<Thread> threadsInGroup = getThreadsInGroup(mainThread.getThreadGroup());
+ if (threadsInGroup.isEmpty()) {
+ return null;
+ }
+
+ // Now that we have all the threads in the test's thread group: Assume that
+ // any thread we're "stuck" in is RUNNABLE. Look for all RUNNABLE threads.
+ // If just one, we return that (unless it equals threadMain). If there's more
+ // than one, pick the one that's using the most CPU time, if this feature is
+ // supported.
+ Thread stuckThread = null;
+ long maxCpuTime = 0;
+ for (Thread thread : threadsInGroup) {
+ if (thread.getState() == Thread.State.RUNNABLE) {
+ long threadCpuTime = cpuTime(thread);
+ if (stuckThread == null || threadCpuTime > maxCpuTime) {
+ stuckThread = thread;
+ maxCpuTime = threadCpuTime;
+ }
+ }
+ }
+ return (stuckThread == mainThread) ? null : stuckThread;
+ }
+
+ /**
+ * Returns all active threads belonging to a thread group.
+ * @param group The thread group.
+ * @return The active threads in the thread group. The result should be a
+ * complete list of the active threads at some point in time. Returns an empty list
+ * if this cannot be determined, e.g. because new threads are being created at an
+ * extremely fast rate.
+ */
+ private List<Thread> getThreadsInGroup(ThreadGroup group) {
+ final int activeThreadCount = group.activeCount(); // this is just an estimate
+ int threadArraySize = Math.max(activeThreadCount * 2, 100);
+ for (int loopCount = 0; loopCount < 5; loopCount++) {
+ Thread[] threads = new Thread[threadArraySize];
+ int enumCount = group.enumerate(threads);
+ if (enumCount < threadArraySize) {
+ return Arrays.asList(threads).subList(0, enumCount);
+ }
+ // if there are too many threads to fit into the array, enumerate's result
+ // is >= the array's length; therefore we can't trust that it returned all
+ // the threads. Try again.
+ threadArraySize += 100;
+ }
+ // threads are proliferating too fast for us. Bail before we get into
+ // trouble.
+ return Collections.emptyList();
+ }
+
+ /**
+ * Returns the CPU time used by a thread, if possible.
+ * @param thr The thread to query.
+ * @return The CPU time used by {@code thr}, or 0 if it cannot be determined.
+ */
+ private long cpuTime(Thread thr) {
+ ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
+ if (mxBean.isThreadCpuTimeSupported()) {
+ try {
+ return mxBean.getThreadCpuTime(thr.getId());
+ } catch (UnsupportedOperationException e) {
+ }
+ }
+ return 0;
}
private class CallableStatement implements Callable<Throwable> {
diff --git a/src/main/java/org/junit/rules/Timeout.java b/src/main/java/org/junit/rules/Timeout.java
index 5cf905a..334a923 100644
--- a/src/main/java/org/junit/rules/Timeout.java
+++ b/src/main/java/org/junit/rules/Timeout.java
@@ -40,6 +40,7 @@ import java.util.concurrent.TimeUnit;
public class Timeout implements TestRule {
private final long timeout;
private final TimeUnit timeUnit;
+ private final boolean lookForStuckThread;
/**
* Returns a new builder for building an instance.
@@ -79,6 +80,7 @@ public class Timeout implements TestRule {
public Timeout(long timeout, TimeUnit timeUnit) {
this.timeout = timeout;
this.timeUnit = timeUnit;
+ lookForStuckThread = false;
}
/**
@@ -90,6 +92,7 @@ public class Timeout implements TestRule {
protected Timeout(Builder builder) {
timeout = builder.getTimeout();
timeUnit = builder.getTimeUnit();
+ lookForStuckThread = builder.getLookingForStuckThread();
}
/**
@@ -122,6 +125,16 @@ public class Timeout implements TestRule {
}
/**
+ * Gets whether this {@code Timeout} will look for a stuck thread
+ * when the test times out.
+ *
+ * @since 4.12
+ */
+ protected final boolean getLookingForStuckThread() {
+ return lookForStuckThread;
+ }
+
+ /**
* Creates a {@link Statement} that will run the given
* {@code statement}, and timeout the operation based
* on the values configured in this rule. Subclasses
@@ -133,6 +146,7 @@ public class Timeout implements TestRule {
Statement statement) throws Exception {
return FailOnTimeout.builder()
.withTimeout(timeout, timeUnit)
+ .withLookingForStuckThread(lookForStuckThread)
.build(statement);
}
@@ -191,6 +205,25 @@ public class Timeout implements TestRule {
}
/**
+ * Specifies whether to look for a stuck thread. If a timeout occurs and this
+ * feature is enabled, the rule will look for a thread that appears to be stuck
+ * and dump its backtrace. This feature is experimental. Behavior may change
+ * after the 4.12 release in response to feedback.
+ *
+ * @param enable {@code true} to enable the feature
+ * @return {@code this} for method chaining.
+ */
+ public Builder withLookingForStuckThread(boolean enable) {
+ this.lookForStuckThread = enable;
+ return this;
+ }
+
+ protected boolean getLookingForStuckThread() {
+ return lookForStuckThread;
+ }
+
+
+ /**
* Builds a {@link Timeout} instance using the values in this builder.,
*/
public Timeout build() {