Skip to content

Commit 1768d08

Browse files
author
Adam Beneschan
committed
fail on timeout displays stack of stuck thread (Issue #727)
1 parent 9e071a4 commit 1768d08

File tree

3 files changed

+158
-5
lines changed

3 files changed

+158
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.junit.internal.runners;
2+
3+
import java.text.MessageFormat;
4+
5+
/**
6+
* An Exception that also carries information about some other relevant thread than
7+
* the one whose stack trace is stored in the exception.
8+
*/
9+
public class ExceptionWithThread extends Exception {
10+
11+
private Thread fThread;
12+
private StackTraceElement[] fStack;
13+
private String fDescription;
14+
15+
/**
16+
* Constructs a new exception with the detail message and relevant thread.
17+
* @param message The detail message (as for an {@link Exception}).
18+
* @param thread The relevant thread.
19+
*/
20+
public ExceptionWithThread (String message, Thread thread) {
21+
this (message, thread, null);
22+
}
23+
24+
/**
25+
* Constructs a new exception with the detail message, relevant thread, and
26+
* a description explaining why the thread is relevant.
27+
* @param message The detail message (as for an {@link Exception}).
28+
* @param thread The relevant thread.
29+
* @param description A format string (used by {@link MessageFormat#format(Object)})
30+
* that describes why the thread is relevant. {@code {0}} in the format string is
31+
* replaced by the thread name.
32+
*/
33+
public ExceptionWithThread (String message, Thread thread, String description) {
34+
super(message);
35+
fThread = thread;
36+
try {
37+
fStack = thread.getStackTrace();
38+
} catch (SecurityException e) {
39+
fStack = new StackTraceElement[0];
40+
}
41+
fDescription = (description == null) ? null :
42+
MessageFormat.format(description, thread.getName());
43+
44+
}
45+
46+
/**
47+
* Returns the relevant thread for the exception.
48+
* @return The relevant thread.
49+
*/
50+
public Thread getThread () { return fThread; }
51+
52+
/**
53+
* Returns the stack trace of the relevant thread.
54+
* @return The stack trace of the relevant thread, at the point when the
55+
* {@link ExceptionWithThread} was constructed; may have length 0 if the
56+
* stack trace could not be determined (e.g. the thread terminated before the
57+
* exception was created).
58+
*/
59+
public StackTraceElement[] getThreadStackTrace() { return fStack; }
60+
61+
/**
62+
* Returns a description of why the thread is relevant.
63+
* @return A description of why the thread is relevant, or {@code null} if the
64+
* exception was created without a description. If a description was provided,
65+
* the sequence {@code {0}} in the description is replaced by the name of the thread.
66+
*/
67+
public String getDescription() { return fDescription; }
68+
69+
}

src/main/java/org/junit/internal/runners/statements/FailOnTimeout.java

+74-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
package org.junit.internal.runners.statements;
22

3+
import java.lang.management.ManagementFactory;
4+
import java.lang.management.ThreadMXBean;
35
import java.util.concurrent.Callable;
46
import java.util.concurrent.ExecutionException;
57
import java.util.concurrent.FutureTask;
68
import java.util.concurrent.TimeUnit;
79
import java.util.concurrent.TimeoutException;
810

11+
import org.junit.internal.runners.ExceptionWithThread;
912
import org.junit.runners.model.Statement;
1013

1114
public class FailOnTimeout extends Statement {
1215
private final Statement fOriginalStatement;
1316
private final TimeUnit fTimeUnit;
1417
private final long fTimeout;
18+
private ThreadGroup fThreadGroup = null;
1519

1620
public FailOnTimeout(Statement originalStatement, long millis) {
1721
this(originalStatement, millis, TimeUnit.MILLISECONDS);
@@ -26,7 +30,8 @@ public FailOnTimeout(Statement originalStatement, long timeout, TimeUnit unit) {
2630
@Override
2731
public void evaluate() throws Throwable {
2832
FutureTask<Throwable> task = new FutureTask<Throwable>(new CallableStatement());
29-
Thread thread = new Thread(task, "Time-limited test");
33+
fThreadGroup = new ThreadGroup ("FailOnTimeoutGroup");
34+
Thread thread = new Thread(fThreadGroup, task, "Time-limited test");
3035
thread.setDaemon(true);
3136
thread.start();
3237
Throwable throwable = getResult(task, thread);
@@ -55,17 +60,82 @@ private Throwable getResult(FutureTask<Throwable> task, Thread thread) {
5560

5661
private Exception createTimeoutException(Thread thread) {
5762
StackTraceElement[] stackTrace = thread.getStackTrace();
58-
Exception exception = new Exception(String.format(
59-
"test timed out after %d %s", fTimeout, fTimeUnit.name().toLowerCase()));
63+
final Thread stuckThread = getStuckThread (thread);
64+
String message = String.format(
65+
"test timed out after %d %s", fTimeout, fTimeUnit.name().toLowerCase());
66+
Exception exception = (stuckThread == null)
67+
? new Exception(message)
68+
: new ExceptionWithThread (message, stuckThread,
69+
"Appears to be stuck in thread {0}");
6070
if (stackTrace != null) {
6171
exception.setStackTrace(stackTrace);
6272
thread.interrupt();
6373
}
6474
return exception;
6575
}
6676

67-
private class CallableStatement implements Callable<Throwable> {
77+
/**
78+
* Determines whether the test appears to be stuck in some thread other than
79+
* the "main thread" (the one created to run the test).
80+
* @param mainThread The main thread created by {@code evaluate()}
81+
* @return The thread which appears to be causing the problem, if different from
82+
* {@code mainThread}, or {@code null} if the main thread appears to be the
83+
* problem or if the thread cannot be determined. The return value is never equal
84+
* to {@code mainThread}.
85+
*/
86+
private Thread getStuckThread (Thread mainThread) {
87+
if (fThreadGroup == null) return null;
88+
final int count = fThreadGroup.activeCount(); // this is just an estimate
89+
int enumSize = Math.max (count * 2, 100);
90+
int enumCount;
91+
Thread[] threads;
92+
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
93+
int loopCount = 0;
94+
while (true) {
95+
threads = new Thread[enumSize];
96+
enumCount = fThreadGroup.enumerate (threads);
97+
// if there are too many threads to fit into the array, enumerate's result
98+
// is >= the array's length; therefore we can't trust that it returned all
99+
// the threads. Try again.
100+
if (enumCount < enumSize) break;
101+
enumSize += 100;
102+
if (++loopCount >= 5) return null;
103+
// threads are proliferating too fast for us. Bail before we get into
104+
// trouble.
105+
}
106+
107+
// Now that we have all the threads in the test's thread group: Assume that
108+
// any thread we're "stuck" in is RUNNABLE. Look for all RUNNABLE threads.
109+
// If just one, we return that (unless it equals threadMain). If there's more
110+
// than one, pick the one that's using the most CPU time, if this feature is
111+
// supported.
112+
Thread firstRunnable = null;
113+
Thread mostCpu = null;
114+
long maxCpuTime = 0;
115+
int runnableCount = 0;
116+
for (int i = 0; i < enumCount; i++) {
117+
if (threads[i].getState() == Thread.State.RUNNABLE) {
118+
runnableCount++;
119+
if (firstRunnable == null) firstRunnable = threads[i];
120+
if (mxBean.isThreadCpuTimeSupported()) {
121+
try {
122+
long cpuTime = mxBean.getThreadCpuTime(threads[i].getId());
123+
if (mostCpu == null || cpuTime > maxCpuTime) {
124+
mostCpu = threads[i];
125+
maxCpuTime = cpuTime;
126+
}
127+
} catch (UnsupportedOperationException e) {
128+
}
129+
}
130+
}
131+
}
132+
Thread stuckThread =
133+
(runnableCount == 1) ? firstRunnable :
134+
((mostCpu != null) ? mostCpu : firstRunnable);
135+
return (stuckThread == mainThread) ? null : stuckThread;
136+
}
68137

138+
private class CallableStatement implements Callable<Throwable> {
69139
public Throwable call() throws Exception {
70140
try {
71141
fOriginalStatement.evaluate();

src/main/java/org/junit/runner/notification/Failure.java

+15-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.io.Serializable;
55
import java.io.StringWriter;
66

7+
import org.junit.internal.runners.ExceptionWithThread;
78
import org.junit.runner.Description;
89

910
/**
@@ -68,7 +69,20 @@ public String toString() {
6869
public String getTrace() {
6970
StringWriter stringWriter = new StringWriter();
7071
PrintWriter writer = new PrintWriter(stringWriter);
71-
getException().printStackTrace(writer);
72+
Throwable exc = getException();
73+
exc.printStackTrace(writer);
74+
if (exc instanceof ExceptionWithThread) {
75+
ExceptionWithThread ewt = (ExceptionWithThread) exc;
76+
if (ewt.getDescription() == null) {
77+
writer.println("Stack for thread " + ewt.getThread().getName() + ":");
78+
} else {
79+
writer.println(ewt.getDescription() + ":");
80+
}
81+
StackTraceElement[] threadTrace = ewt.getThreadStackTrace();
82+
for (StackTraceElement traceElement : threadTrace) {
83+
writer.println("\tat " + traceElement);
84+
}
85+
}
7286
StringBuffer buffer = stringWriter.getBuffer();
7387
return buffer.toString();
7488
}

0 commit comments

Comments
 (0)