Skip to content

Commit de77f66

Browse files
authored
Only create ThreadGroups if FailOnTimeout.lookForStuckThread is true. (#1691)
This reduces the differences (where possible) when tests are run with a timeout. Creating a ThreadGroup can cause noticeable differences, for example in code that uses java.beans.ThreadGroupContext.
1 parent 877750a commit de77f66

File tree

3 files changed

+85
-11
lines changed

3 files changed

+85
-11
lines changed

doc/ReleaseNotes4.13.2.md

+17
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,20 @@ people reported problems that were caused by explicitly destroying the `ThreadGr
1111

1212
In this change, the code was updated to call `ThreadGroup.setDaemon(true)` instead of destroying the
1313
ThreadGroup.
14+
15+
### [Pull request $1691:](https://github.com/junit-team/junit/pull/1691) Only create ThreadGroups if FailOnTimeout.lookForStuckThread is true.
16+
17+
In JUnit 4.12 ([pull request #742](https://github.com/junit-team/junit4/pull/742)) the `Timeout`
18+
Rule was updated to optionally display the stacktrace of the thread that appears to be stuck
19+
(enabled on an opt-in basis by passing `true` to `Timeout.Builder.lookForStuckThread(boolean)`).
20+
When that change was made, time-limited tests were changed to start the new thread in a new
21+
`ThreadGroup`, even if the test did not call `lookForStuckThread()`. This subtle change in
22+
behavior resulted in visible behavior changes to some tests (for example, tests of code that uses
23+
`java.beans.ThreadGroupContext`).
24+
25+
In this change, the code is updated to only create a new `ThreadGroup` if the caller calls
26+
`Timeout.Builder.lookForStuckThread(true)`. Tests with timeouts that do not make this call will
27+
behave as they did in JUnit 4.11 (and more similar to tests that do not have a timeout). This
28+
unfortunately could result in visible changes of tests written or updated since the 4.12
29+
release. If this change adversely affects your tests, you can create the `Timeout` rule via the
30+
builder and call `Timeout.Builder.lookForStuckThread(true)`.

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

+26-8
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,7 @@ public FailOnTimeout build(Statement statement) {
120120
public void evaluate() throws Throwable {
121121
CallableStatement callable = new CallableStatement();
122122
FutureTask<Throwable> task = new FutureTask<Throwable>(callable);
123-
ThreadGroup threadGroup = new ThreadGroup("FailOnTimeoutGroup");
124-
if (!threadGroup.isDaemon()) {
125-
try {
126-
threadGroup.setDaemon(true);
127-
} catch (SecurityException e) {
128-
// Swallow the exception to keep the same behavior as in JUnit 4.12.
129-
}
130-
}
123+
ThreadGroup threadGroup = threadGroupForNewThread();
131124
Thread thread = new Thread(threadGroup, task, "Time-limited test");
132125
thread.setDaemon(true);
133126
thread.start();
@@ -138,6 +131,31 @@ public void evaluate() throws Throwable {
138131
}
139132
}
140133

134+
private ThreadGroup threadGroupForNewThread() {
135+
if (!lookForStuckThread) {
136+
// Use the default ThreadGroup (usually the one from the current
137+
// thread).
138+
return null;
139+
}
140+
141+
// Create the thread in a new ThreadGroup, so if the time-limited thread
142+
// becomes stuck, getStuckThread() can find the thread likely to be the
143+
// culprit.
144+
ThreadGroup threadGroup = new ThreadGroup("FailOnTimeoutGroup");
145+
if (!threadGroup.isDaemon()) {
146+
// Mark the new ThreadGroup as a daemon thread group, so it will be
147+
// destroyed after the time-limited thread completes. By ensuring the
148+
// ThreadGroup is destroyed, any data associated with the ThreadGroup
149+
// (ex: via java.beans.ThreadGroupContext) is destroyed.
150+
try {
151+
threadGroup.setDaemon(true);
152+
} catch (SecurityException e) {
153+
// Swallow the exception to keep the same behavior as in JUnit 4.12.
154+
}
155+
}
156+
return threadGroup;
157+
}
158+
141159
/**
142160
* Wait for the test task, returning the exception thrown by the test if the
143161
* test failed, an exception indicating a timeout if the test timed out, or

src/test/java/org/junit/internal/runners/statements/FailOnTimeoutTest.java

+42-3
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,53 @@
88
import static java.util.concurrent.TimeUnit.MILLISECONDS;
99
import static org.junit.Assert.assertEquals;
1010
import static org.junit.Assert.assertFalse;
11+
import static org.junit.Assert.assertNotSame;
1112
import static org.junit.Assert.assertSame;
1213
import static org.junit.Assert.assertThrows;
1314
import static org.junit.Assert.assertTrue;
1415
import static org.junit.Assert.fail;
15-
import static org.junit.internal.runners.statements.FailOnTimeout.builder;
16+
import static org.junit.Assume.assumeFalse;
17+
import static org.junit.Assume.assumeTrue;
1618

19+
import java.util.Arrays;
1720
import java.util.concurrent.TimeUnit;
1821
import java.util.concurrent.atomic.AtomicReference;
1922

2023
import org.junit.Test;
2124
import org.junit.function.ThrowingRunnable;
2225
import org.junit.internal.runners.statements.Fail;
26+
import org.junit.runner.RunWith;
27+
import org.junit.runners.Parameterized;
2328
import org.junit.runners.model.Statement;
2429
import org.junit.runners.model.TestTimedOutException;
2530

2631

2732
/**
2833
* @author Asaf Ary, Stefan Birkner
2934
*/
35+
@RunWith(Parameterized.class)
3036
public class FailOnTimeoutTest {
3137
private static final long TIMEOUT = 100;
3238
private static final long DURATION_THAT_EXCEEDS_TIMEOUT = 60 * 60 * 1000; //1 hour
3339

3440
private final TestStatement statement = new TestStatement();
3541

36-
private final FailOnTimeout failOnTimeout = builder().withTimeout(TIMEOUT, MILLISECONDS).build(statement);
42+
private final boolean lookingForStuckThread;
43+
private final FailOnTimeout failOnTimeout;
44+
45+
@Parameterized.Parameters(name = "lookingForStuckThread = {0}")
46+
public static Iterable<Boolean> getParameters() {
47+
return Arrays.asList(Boolean.TRUE, Boolean.FALSE);
48+
}
49+
50+
public FailOnTimeoutTest(Boolean lookingForStuckThread) {
51+
this.lookingForStuckThread = lookingForStuckThread;
52+
this.failOnTimeout = builder().withTimeout(TIMEOUT, MILLISECONDS).build(statement);
53+
}
54+
55+
private FailOnTimeout.Builder builder() {
56+
return FailOnTimeout.builder().withLookingForStuckThread(lookingForStuckThread);
57+
}
3758

3859
@Test
3960
public void throwsTestTimedOutException() {
@@ -212,14 +233,17 @@ private void notTheRealCauseOfTheTimeout() {
212233
}
213234

214235
@Test
215-
public void threadGroupNotLeaked() throws Throwable {
236+
public void lookingForStuckThread_threadGroupNotLeaked() throws Throwable {
237+
assumeTrue(lookingForStuckThread);
216238
final AtomicReference<ThreadGroup> innerThreadGroup = new AtomicReference<ThreadGroup>();
217239
final AtomicReference<Thread> innerThread = new AtomicReference<Thread>();
240+
final ThreadGroup outerThreadGroup = currentThread().getThreadGroup();
218241
ThrowingRunnable runnable = evaluateWithDelegate(new Statement() {
219242
@Override
220243
public void evaluate() {
221244
innerThread.set(currentThread());
222245
ThreadGroup group = currentThread().getThreadGroup();
246+
assertNotSame("inner thread should use a different thread group", outerThreadGroup, group);
223247
innerThreadGroup.set(group);
224248
assertTrue("the 'FailOnTimeoutGroup' thread group should be a daemon thread group", group.isDaemon());
225249
}
@@ -231,4 +255,19 @@ public void evaluate() {
231255
innerThread.get().join();
232256
assertTrue("the 'FailOnTimeoutGroup' thread group should be destroyed after running the test", innerThreadGroup.get().isDestroyed());
233257
}
258+
259+
@Test
260+
public void notLookingForStuckThread_usesSameThreadGroup() throws Throwable {
261+
assumeFalse(lookingForStuckThread);
262+
final ThreadGroup outerThreadGroup = currentThread().getThreadGroup();
263+
ThrowingRunnable runnable = evaluateWithDelegate(new Statement() {
264+
@Override
265+
public void evaluate() {
266+
ThreadGroup group = currentThread().getThreadGroup();
267+
assertSame("inner thread should use the same thread group", outerThreadGroup, group);
268+
}
269+
});
270+
271+
runnable.run();
272+
}
234273
}

0 commit comments

Comments
 (0)