diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index 01259879ea0e..d54f4c25de99 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -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 @@ -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 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 abf4d0afbfcf..3375711230e6 100644 --- a/src/main/java/org/junit/internal/runners/statements/FailOnTimeout.java +++ b/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 task = new FutureTask(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; } } @@ -55,17 +67,122 @@ private Throwable getResult(FutureTask 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) { + Exception stuckThreadException = + new Exception ("Appears to be stuck in thread " + + stuckThread.getName()); + stuckThreadException.setStackTrace(getStackTrace(stuckThread)); + return new MultipleFailureException + (Arrays.asList(currThreadException, stuckThreadException)); + } else { + return currThreadException; + } } - private class CallableStatement implements Callable { + /** + * 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 { public Throwable call() throws Exception { try { fOriginalStatement.evaluate(); @@ -77,4 +194,4 @@ public Throwable call() throws Exception { return null; } } -} \ No newline at end of file +} diff --git a/src/main/java/org/junit/rules/Timeout.java b/src/main/java/org/junit/rules/Timeout.java index ac11026125ec..a4b500985bcf 100644 --- a/src/main/java/org/junit/rules/Timeout.java +++ b/src/main/java/org/junit/rules/Timeout.java @@ -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 @@ -66,6 +67,7 @@ public Timeout(int millis) { public Timeout(long timeout, TimeUnit unit) { fTimeout = timeout; fTimeUnit = unit; + fLookForStuckThread = false; } /** @@ -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); } } \ No newline at end of file diff --git a/src/test/java/org/junit/tests/running/methods/TimeoutTest.java b/src/test/java/org/junit/tests/running/methods/TimeoutTest.java index 2eccadaedf84..efd713c8fc6b 100644 --- a/src/test/java/org/junit/tests/running/methods/TimeoutTest.java +++ b/src/test/java/org/junit/tests/running/methods/TimeoutTest.java @@ -1,6 +1,7 @@ package org.junit.tests.running.methods; import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; @@ -10,18 +11,22 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; +import java.util.concurrent.TimeUnit; import junit.framework.JUnit4TestAdapter; import junit.framework.TestResult; import org.junit.After; import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.rules.Timeout; import org.junit.runner.JUnitCore; import org.junit.runner.Result; public class TimeoutTest { - static public class FailureWithTimeoutTest { + public static class FailureWithTimeoutTest { @Test(timeout = 1000) public void failure() { fail(); @@ -37,7 +42,7 @@ public void failureWithTimeout() throws Exception { assertEquals(AssertionError.class, result.getFailures().get(0).getException().getClass()); } - static public class FailureWithTimeoutRunTimeExceptionTest { + public static class FailureWithTimeoutRunTimeExceptionTest { @Test(timeout = 1000) public void failure() { throw new NullPointerException(); @@ -53,7 +58,7 @@ public void failureWithTimeoutRunTimeException() throws Exception { assertEquals(NullPointerException.class, result.getFailures().get(0).getException().getClass()); } - static public class SuccessWithTimeoutTest { + public static class SuccessWithTimeoutTest { @Test(timeout = 1000) public void success() { } @@ -67,7 +72,7 @@ public void successWithTimeout() throws Exception { assertEquals(0, result.getFailureCount()); } - static public class TimeoutFailureTest { + public static class TimeoutFailureTest { @Test(timeout = 100) public void success() throws InterruptedException { Thread.sleep(40000); @@ -84,7 +89,7 @@ public void timeoutFailure() throws Exception { assertEquals(InterruptedException.class, result.getFailures().get(0).getException().getClass()); } - static public class InfiniteLoopTest { + public static class InfiniteLoopTest { @Test(timeout = 100) public void failure() { infiniteLoop(); @@ -110,7 +115,7 @@ public void infiniteLoop() throws Exception { assertTrue(exception.getMessage().contains("test timed out after 100 milliseconds")); } - static public class ImpatientLoopTest { + public static class ImpatientLoopTest { @Test(timeout = 1) public void failure() { infiniteLoop(); @@ -141,6 +146,13 @@ private long runAndTime(Class clazz) { return totalTime; } + private String stackForException(Throwable exception) { + Writer buffer = new StringWriter(); + PrintWriter writer = new PrintWriter(buffer); + exception.printStackTrace(writer); + return buffer.toString(); + } + @Test public void stalledThreadAppearsInStackTrace() throws Exception { JUnitCore core = new JUnitCore(); @@ -148,10 +160,86 @@ public void stalledThreadAppearsInStackTrace() throws Exception { assertEquals(1, result.getRunCount()); assertEquals(1, result.getFailureCount()); Throwable exception = result.getFailures().get(0).getException(); - Writer buffer = new StringWriter(); - PrintWriter writer = new PrintWriter(buffer); - exception.printStackTrace(writer); - assertThat(buffer.toString(), containsString("infiniteLoop")); // Make sure we have the stalled frame on the stack somewhere + assertThat(stackForException(exception), containsString("infiniteLoop")); // Make sure we have the stalled frame on the stack somewhere + } + + public static class InfiniteLoopMultithreaded { + + private static class ThreadTest implements Runnable { + private boolean fStall; + + public ThreadTest(boolean stall) { + fStall = stall; + } + + public void run() { + if (fStall) + for (; ; ) ; + try { + Thread.sleep(500); + } catch (InterruptedException e) { + } + } + } + + public void failure(boolean mainThreadStalls) throws Exception { + Thread t1 = new Thread(new ThreadTest(false), "timeout-thr1"); + Thread t2 = new Thread(new ThreadTest(!mainThreadStalls), "timeout-thr2"); + Thread t3 = new Thread(new ThreadTest(false), "timeout-thr3"); + t1.start(); + t2.start(); + t3.start(); + if (mainThreadStalls) + for (; ; ) ; + t1.join(); + t2.join(); + t3.join(); + } + } + + public static class InfiniteLoopWithStuckThreadTest { + @Rule + public TestRule globalTimeout = new Timeout(100, TimeUnit.MILLISECONDS).lookForStuckThread(true); + + @Test + public void failure() throws Exception { + (new InfiniteLoopMultithreaded()).failure(false); + } + } + + public static class InfiniteLoopStuckInMainThreadTest { + @Rule + public TestRule globalTimeout = new Timeout(100, TimeUnit.MILLISECONDS).lookForStuckThread(true); + + @Test + public void failure() throws Exception { + (new InfiniteLoopMultithreaded()).failure(true); + } + } + + @Test + public void timeoutFailureMultithreaded() throws Exception { + JUnitCore core = new JUnitCore(); + Result result = core.run(InfiniteLoopWithStuckThreadTest.class); + assertEquals(1, result.getRunCount()); + assertEquals(2, result.getFailureCount()); + Throwable exception[] = new Throwable[2]; + for (int i = 0; i < 2; i++) + exception[i] = result.getFailures().get(i).getException(); + assertThat(exception[0].getMessage(), containsString("test timed out after 100 milliseconds")); + assertThat(stackForException(exception[0]), containsString("Thread.join")); + assertThat(exception[1].getMessage(), containsString("Appears to be stuck in thread timeout-thr2")); + } + + @Test + public void timeoutFailureMultithreadedStuckInMain() throws Exception { + JUnitCore core = new JUnitCore(); + Result result = core.run(InfiniteLoopStuckInMainThreadTest.class); + assertEquals(1, result.getRunCount()); + assertEquals(1, result.getFailureCount()); + Throwable exception = result.getFailures().get(0).getException(); + assertThat(exception.getMessage(), containsString("test timed out after 100 milliseconds")); + assertThat(exception.getMessage(), not(containsString("Appears to be stuck"))); } @Test