Skip to content

Commit 1254795

Browse files
authored
Mark ThreadGroups created by FailOnTimeout as daemon groups (#1687)
Mark ThreadGroup created by FailOnTimeout as a daemon group. Previously, FailOnTimeout destroyed the ThreadGroup, which could cause race conditions if the ThreadGroup was referenced by other threads. Fixes #1652
1 parent 8b39600 commit 1254795

File tree

2 files changed

+41
-43
lines changed

2 files changed

+41
-43
lines changed

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

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -121,28 +121,21 @@ public void evaluate() throws Throwable {
121121
CallableStatement callable = new CallableStatement();
122122
FutureTask<Throwable> task = new FutureTask<Throwable>(callable);
123123
ThreadGroup threadGroup = new ThreadGroup("FailOnTimeoutGroup");
124-
Thread thread = new Thread(threadGroup, task, "Time-limited test");
125-
try {
126-
thread.setDaemon(true);
127-
thread.start();
128-
callable.awaitStarted();
129-
Throwable throwable = getResult(task, thread);
130-
if (throwable != null) {
131-
throw throwable;
132-
}
133-
} finally {
124+
if (!threadGroup.isDaemon()) {
134125
try {
135-
thread.join(1);
136-
} catch (InterruptedException e) {
137-
Thread.currentThread().interrupt();
138-
}
139-
try {
140-
threadGroup.destroy();
141-
} catch (IllegalThreadStateException e) {
142-
// If a thread from the group is still alive, the ThreadGroup cannot be destroyed.
143-
// Swallow the exception to keep the same behavior prior to this change.
126+
threadGroup.setDaemon(true);
127+
} catch (SecurityException e) {
128+
// Swallow the exception to keep the same behavior as in JUnit 4.12.
144129
}
145130
}
131+
Thread thread = new Thread(threadGroup, task, "Time-limited test");
132+
thread.setDaemon(true);
133+
thread.start();
134+
callable.awaitStarted();
135+
Throwable throwable = getResult(task, thread);
136+
if (throwable != null) {
137+
throw throwable;
138+
}
146139
}
147140

148141
/**

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

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,16 @@
1414
import static org.junit.Assert.fail;
1515
import static org.junit.internal.runners.statements.FailOnTimeout.builder;
1616

17-
import java.util.Arrays;
18-
import java.util.Collection;
19-
import java.util.HashSet;
20-
import java.util.Set;
2117
import java.util.concurrent.TimeUnit;
18+
import java.util.concurrent.atomic.AtomicReference;
2219

2320
import org.junit.Test;
2421
import org.junit.function.ThrowingRunnable;
22+
import org.junit.internal.runners.statements.Fail;
2523
import org.junit.runners.model.Statement;
2624
import org.junit.runners.model.TestTimedOutException;
2725

26+
2827
/**
2928
* @author Asaf Ary, Stefan Birkner
3029
*/
@@ -91,20 +90,24 @@ public void throwsExceptionWithTimeoutValueAndTimeUnitSet() {
9190
assertEquals(TimeUnit.MILLISECONDS, e.getTimeUnit());
9291
}
9392

94-
private ThrowingRunnable evaluateWithException(final Exception exception) {
93+
private ThrowingRunnable evaluateWithDelegate(final Statement delegate) {
9594
return new ThrowingRunnable() {
9695
public void run() throws Throwable {
97-
statement.nextException = exception;
96+
statement.nextStatement = delegate;
9897
statement.waitDuration = 0;
9998
failOnTimeout.evaluate();
10099
}
101100
};
102101
}
103102

103+
private ThrowingRunnable evaluateWithException(Exception exception) {
104+
return evaluateWithDelegate(new Fail(exception));
105+
}
106+
104107
private ThrowingRunnable evaluateWithWaitDuration(final long waitDuration) {
105108
return new ThrowingRunnable() {
106109
public void run() throws Throwable {
107-
statement.nextException = null;
110+
statement.nextStatement = null;
108111
statement.waitDuration = waitDuration;
109112
failOnTimeout.evaluate();
110113
}
@@ -114,13 +117,13 @@ public void run() throws Throwable {
114117
private static final class TestStatement extends Statement {
115118
long waitDuration;
116119

117-
Exception nextException;
120+
Statement nextStatement;
118121

119122
@Override
120123
public void evaluate() throws Throwable {
121124
sleep(waitDuration);
122-
if (nextException != null) {
123-
throw nextException;
125+
if (nextStatement != null) {
126+
nextStatement.evaluate();
124127
}
125128
}
126129
}
@@ -210,20 +213,22 @@ private void notTheRealCauseOfTheTimeout() {
210213

211214
@Test
212215
public void threadGroupNotLeaked() throws Throwable {
213-
Collection<ThreadGroup> groupsBeforeSet = subGroupsOfCurrentThread();
214-
215-
evaluateWithWaitDuration(0);
216-
217-
for (ThreadGroup group: subGroupsOfCurrentThread()) {
218-
if (!groupsBeforeSet.contains(group) && "FailOnTimeoutGroup".equals(group.getName())) {
219-
fail("A 'FailOnTimeoutGroup' thread group remains referenced after the test execution.");
216+
final AtomicReference<ThreadGroup> innerThreadGroup = new AtomicReference<ThreadGroup>();
217+
final AtomicReference<Thread> innerThread = new AtomicReference<Thread>();
218+
ThrowingRunnable runnable = evaluateWithDelegate(new Statement() {
219+
@Override
220+
public void evaluate() {
221+
innerThread.set(currentThread());
222+
ThreadGroup group = currentThread().getThreadGroup();
223+
innerThreadGroup.set(group);
224+
assertTrue("the 'FailOnTimeoutGroup' thread group should be a daemon thread group", group.isDaemon());
220225
}
221-
}
222-
}
223-
224-
private Collection<ThreadGroup> subGroupsOfCurrentThread() {
225-
ThreadGroup[] subGroups = new ThreadGroup[256];
226-
int numGroups = currentThread().getThreadGroup().enumerate(subGroups);
227-
return Arrays.asList(subGroups).subList(0, numGroups);
226+
});
227+
228+
runnable.run();
229+
230+
assertTrue("the Statement was never run", innerThread.get() != null);
231+
innerThread.get().join();
232+
assertTrue("the 'FailOnTimeoutGroup' thread group should be destroyed after running the test", innerThreadGroup.get().isDestroyed());
228233
}
229234
}

0 commit comments

Comments
 (0)