1
2 package org.junit.internal.runners.statements;
3
4 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
5 import java.lang.management.ManagementFactory;
6 import java.lang.management.ThreadMXBean;
7 import java.util.Arrays;
8 import java.util.concurrent.Callable;
9 import java.util.concurrent.CountDownLatch;
10 import java.util.concurrent.ExecutionException;
11 import java.util.concurrent.FutureTask;
12 import java.util.concurrent.TimeUnit;
13 import java.util.concurrent.TimeoutException;
14 import javax.annotation.Nullable;
15 import javax.annotation.ParametersAreNonnullByDefault;
16
17 import org.junit.runners.model.MultipleFailureException;
18 import org.junit.runners.model.Statement;
19 import org.junit.runners.model.TestTimedOutException;
20 import org.spf4j.base.ExecutionContext;
21 import org.spf4j.base.ExecutionContexts;
22 import org.spf4j.test.log.TestUtils;
23
24 @ParametersAreNonnullByDefault
25 @SuppressWarnings("checkstyle")
26 public final class FailOnTimeout extends Statement {
27 private final Statement originalStatement;
28 private final TimeUnit timeUnit;
29 private final long timeout;
30 private final boolean lookForStuckThread;
31 private volatile ThreadGroup threadGroup = null;
32
33
34
35
36
37
38 public static Builder builder() {
39 return new Builder();
40 }
41
42
43
44
45
46
47
48
49 @Deprecated
50 public FailOnTimeout(final Statement statement, final long timeoutMillis) {
51 this(builder().withTimeout(timeoutMillis, TimeUnit.MILLISECONDS), statement);
52 }
53
54 private FailOnTimeout(final Builder builder, final Statement statement) {
55 originalStatement = statement;
56 timeout = builder.timeout;
57 timeUnit = builder.unit;
58 lookForStuckThread = builder.lookForStuckThread;
59 }
60
61
62
63
64
65
66 public static final class Builder {
67 private boolean lookForStuckThread = false;
68 private long timeout = 0;
69 private TimeUnit unit = TimeUnit.SECONDS;
70
71 private Builder() {
72 }
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87 public Builder withTimeout(final long timeout, final TimeUnit unit) {
88 if (timeout < 0) {
89 throw new IllegalArgumentException("timeout must be non-negative " + timeout);
90 }
91 if (unit == null) {
92 throw new NullPointerException("TimeUnit cannot be null for " + timeout);
93 }
94 this.timeout = timeout;
95 this.unit = unit;
96 return this;
97 }
98
99
100
101
102
103
104
105
106
107
108 public Builder withLookingForStuckThread(final boolean enable) {
109 this.lookForStuckThread = enable;
110 return this;
111 }
112
113
114
115
116
117
118
119 public FailOnTimeout build(final Statement statement) {
120 return new FailOnTimeout(this, statement);
121 }
122 }
123
124 @Override
125 public void evaluate() throws Throwable {
126 if (TestUtils.isExecutedWithDebuggerAgent()) {
127 originalStatement.evaluate();
128 return;
129 }
130 CallableStatement callable = new CallableStatement();
131 callable.setCtx(ExecutionContexts.current());
132 FutureTask<Throwable> task = new FutureTask<Throwable>(callable);
133 threadGroup = new ThreadGroup("FailOnTimeoutGroup");
134 Thread thread = new Thread(threadGroup, task, "Time-limited test");
135 thread.setDaemon(true);
136 thread.start();
137 callable.awaitStarted();
138 Throwable throwable = getResult(task, thread);
139 if (throwable != null) {
140 throw throwable;
141 }
142 }
143
144
145
146
147
148
149 private Throwable getResult(FutureTask<Throwable> task, Thread thread) {
150 try {
151 if (timeout > 0) {
152 return task.get(timeout, timeUnit);
153 } else {
154 return task.get();
155 }
156 } catch (InterruptedException e) {
157 return e;
158 } catch (ExecutionException e) {
159
160 return e.getCause();
161 } catch (TimeoutException e) {
162 return createTimeoutException(thread);
163 }
164 }
165
166 private Exception createTimeoutException(Thread thread) {
167 StackTraceElement[] stackTrace = thread.getStackTrace();
168 final Thread stuckThread = lookForStuckThread ? getStuckThread(thread) : null;
169 Exception currThreadException = new TestTimedOutException(timeout, timeUnit);
170 if (stackTrace != null) {
171 currThreadException.setStackTrace(stackTrace);
172 thread.interrupt();
173 }
174 if (stuckThread != null) {
175 Exception stuckThreadException =
176 new Exception ("Appears to be stuck in thread " +
177 stuckThread.getName());
178 stuckThreadException.setStackTrace(getStackTrace(stuckThread));
179 return new MultipleFailureException(
180 Arrays.<Throwable>asList(currThreadException, stuckThreadException));
181 } else {
182 return currThreadException;
183 }
184 }
185
186
187
188
189
190
191
192 private StackTraceElement[] getStackTrace(Thread thread) {
193 try {
194 return thread.getStackTrace();
195 } catch (SecurityException e) {
196 return new StackTraceElement[0];
197 }
198 }
199
200
201
202
203
204
205
206
207
208
209
210 @Nullable
211 private Thread getStuckThread(Thread mainThread) {
212 if (threadGroup == null) {
213 return null;
214 }
215 Thread[] threadsInGroup = getThreadArray(threadGroup);
216 if (threadsInGroup == null) {
217 return null;
218 }
219
220
221
222
223
224
225 Thread stuckThread = null;
226 long maxCpuTime = 0;
227 for (Thread thread : threadsInGroup) {
228 if (thread.getState() == Thread.State.RUNNABLE) {
229 long threadCpuTime = cpuTime(thread);
230 if (stuckThread == null || threadCpuTime > maxCpuTime) {
231 stuckThread = thread;
232 maxCpuTime = threadCpuTime;
233 }
234 }
235 }
236 return (stuckThread == mainThread) ? null : stuckThread;
237 }
238
239
240
241
242
243
244
245
246
247 @Nullable
248 @SuppressFBWarnings("PZLA_PREFER_ZERO_LENGTH_ARRAYS")
249 private Thread[] getThreadArray(final ThreadGroup group) {
250 final int count = group.activeCount();
251 int enumSize = Math.max(count * 2, 100);
252 int enumCount;
253 Thread[] threads;
254 int loopCount = 0;
255 while (true) {
256 threads = new Thread[enumSize];
257 enumCount = group.enumerate(threads);
258 if (enumCount < enumSize) {
259 break;
260 }
261
262
263
264 enumSize += 100;
265 if (++loopCount >= 5) {
266 return null;
267 }
268
269
270 }
271 return copyThreads(threads, enumCount);
272 }
273
274
275
276
277
278
279
280
281 private Thread[] copyThreads(Thread[] threads, int count) {
282 int length = Math.min(count, threads.length);
283 return Arrays.copyOf(threads, length);
284 }
285
286
287
288
289
290
291 private long cpuTime (Thread thr) {
292 ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
293 if (mxBean.isThreadCpuTimeSupported()) {
294 try {
295 return mxBean.getThreadCpuTime(thr.getId());
296 } catch (UnsupportedOperationException e) {
297 }
298 }
299 return 0;
300 }
301
302 private class CallableStatement implements Callable<Throwable> {
303 private final CountDownLatch startLatch = new CountDownLatch(1);
304
305 private ExecutionContext ctx;
306
307 public void setCtx(ExecutionContext ctx) {
308 this.ctx = ctx;
309 }
310
311 @Nullable
312 public Throwable call() throws Exception {
313
314 try (ExecutionContext aCtx = ExecutionContexts.start("asyncTest", ctx)) {
315 startLatch.countDown();
316 originalStatement.evaluate();
317 } catch (Exception e) {
318 throw e;
319 } catch (Throwable e) {
320 return e;
321 }
322 return null;
323 }
324
325 public void awaitStarted() throws InterruptedException {
326 startLatch.await();
327 }
328 }
329
330 @Override
331 public String toString() {
332 return "FailOnTimeout{" + "originalStatement=" + originalStatement
333 + ", timeUnit=" + timeUnit + ", timeout=" + timeout + ", lookForStuckThread="
334 + lookForStuckThread + ", threadGroup=" + threadGroup + '}';
335 }
336
337
338 }