Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#727 Fail on timeout displays stack of stuck thread #742

Merged
merged 9 commits into from
Oct 31, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions .settings/org.eclipse.jdt.core.prefs
Expand Up @@ -247,7 +247,7 @@ org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not in
org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
Expand Down Expand Up @@ -351,7 +351,7 @@ org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1
org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true
org.eclipse.jdt.core.formatter.tabulation.char=tab
org.eclipse.jdt.core.formatter.tabulation.char=space
org.eclipse.jdt.core.formatter.tabulation.size=4
org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
131 changes: 124 additions & 7 deletions src/main/java/org/junit/internal/runners/statements/FailOnTimeout.java
@@ -1,37 +1,49 @@
package org.junit.internal.runners.statements;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.junit.runners.model.MultipleFailureException;
import org.junit.runners.model.Statement;

public class FailOnTimeout extends Statement {
private final Statement fOriginalStatement;
private final TimeUnit fTimeUnit;
private final long fTimeout;
private final boolean fLookForStuckThread;
private ThreadGroup fThreadGroup = null;

public FailOnTimeout(Statement originalStatement, long millis) {
this(originalStatement, millis, TimeUnit.MILLISECONDS);
}

public FailOnTimeout(Statement originalStatement, long timeout, TimeUnit unit) {
this(originalStatement, timeout, unit, false);
}

public FailOnTimeout(Statement originalStatement, long timeout, TimeUnit unit, boolean lookForStuckThread) {
fOriginalStatement = originalStatement;
fTimeout = timeout;
fTimeUnit = unit;
fLookForStuckThread = lookForStuckThread;
}

@Override
public void evaluate() throws Throwable {
FutureTask<Throwable> task = new FutureTask<Throwable>(new CallableStatement());
Thread thread = new Thread(task, "Time-limited test");
fThreadGroup = new ThreadGroup("FailOnTimeoutGroup");
Thread thread = new Thread(fThreadGroup, task, "Time-limited test");
thread.setDaemon(true);
thread.start();
Throwable throwable = getResult(task, thread);
if (throwable != null) {
throw throwable;
throw throwable;
}
}

Expand All @@ -55,17 +67,122 @@ private Throwable getResult(FutureTask<Throwable> task, Thread thread) {

private Exception createTimeoutException(Thread thread) {
StackTraceElement[] stackTrace = thread.getStackTrace();
Exception exception = new Exception(String.format(
final Thread stuckThread = fLookForStuckThread ? getStuckThread(thread) : null;
Exception currThreadException = new Exception(String.format(
"test timed out after %d %s", fTimeout, fTimeUnit.name().toLowerCase()));
if (stackTrace != null) {
exception.setStackTrace(stackTrace);
currThreadException.setStackTrace(stackTrace);
thread.interrupt();
}
return exception;
if (stuckThread != null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we move this below the if (stackTrace != null) check below, then we can just use return statements, rather than the resultException local

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;
}
}

private class CallableStatement implements Callable<Throwable> {
/**
* 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) {
if (fThreadGroup == null)
return null;
Thread[] threadsInGroup = getThreadArray(fThreadGroup);
if (threadsInGroup == null)
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 {@code null}
* if this cannot be determined, e.g. because new threads are being created at an
* extremely fast rate.
*/
private Thread[] getThreadArray(ThreadGroup group) {
final int count = group.activeCount(); // this is just an estimate
int enumSize = Math.max(count * 2, 100);
int enumCount;
Thread[] threads;
int loopCount = 0;
while (true) {
threads = new Thread[enumSize];
enumCount = group.enumerate(threads);
if (enumCount < enumSize) break;
// 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.
enumSize += 100;
if (++loopCount >= 5)
return null;
// threads are proliferating too fast for us. Bail before we get into
// trouble.
}
return Arrays.copyOf(threads, enumCount);
}

/**
* 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> {
public Throwable call() throws Exception {
try {
fOriginalStatement.evaluate();
Expand All @@ -77,4 +194,4 @@ public Throwable call() throws Exception {
return null;
}
}
}
}
17 changes: 16 additions & 1 deletion src/main/java/org/junit/rules/Timeout.java
Expand Up @@ -36,6 +36,7 @@
public class Timeout implements TestRule {
private final long fTimeout;
private final TimeUnit fTimeUnit;
private boolean fLookForStuckThread;

/**
* Create a {@code Timeout} instance with the timeout specified
Expand Down Expand Up @@ -66,6 +67,7 @@ public Timeout(int millis) {
public Timeout(long timeout, TimeUnit unit) {
fTimeout = timeout;
fTimeUnit = unit;
fLookForStuckThread = false;
}

/**
Expand All @@ -84,8 +86,21 @@ public static Timeout seconds(long seconds) {
return new Timeout(seconds, TimeUnit.SECONDS);
}

/**
* 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 This object
* @since 4.12
*/
public Timeout lookForStuckThread(boolean enable) {
fLookForStuckThread = enable;
return this;
}

public Statement apply(Statement base, Description description) {
return new FailOnTimeout(base, fTimeout, fTimeUnit);
return new FailOnTimeout(base, fTimeout, fTimeUnit, fLookForStuckThread);
}
}