Skip to content

Commit c4ed5cd

Browse files
Refactor timeout handling in AndroidJUnit4ClassRunner.
This change replaces the use of JUnit's FailOnTimeout with a custom implementation that ensures @before, @test, and @after methods run on the same thread. The previous FailOnTimeout would execute only the @test method in a separate thread, potentially causing issues with thread-local state. The new implementation wraps the entire test block (including Before/After) in a separate thread when a timeout is active, preserving thread locality within the test lifecycle. PiperOrigin-RevId: 903547390
1 parent 1144878 commit c4ed5cd

3 files changed

Lines changed: 132 additions & 17 deletions

File tree

runner/android_junit_runner/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
**Bug Fixes**
88

9+
* Ensure @Before and @Test run on the same thread in AndroidJUnit4ClassRunner.
10+
911
**New Features**
1012

1113
* Make perfetto trace sections for tests more identifiable by prefixing with "test:" and using fully qualified class name. (b/204992764)

runner/android_junit_runner/java/androidx/test/internal/runner/junit4/AndroidJUnit4ClassRunner.java

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,17 @@
2323
import androidx.test.internal.runner.junit4.statement.UiThreadStatement;
2424
import androidx.test.internal.util.AndroidRunnerParams;
2525
import java.util.List;
26+
import java.util.concurrent.CountDownLatch;
27+
import java.util.concurrent.TimeUnit;
28+
import java.util.concurrent.atomic.AtomicReference;
2629
import org.junit.After;
2730
import org.junit.Before;
2831
import org.junit.Test;
29-
import org.junit.internal.runners.statements.FailOnTimeout;
3032
import org.junit.runners.BlockJUnit4ClassRunner;
3133
import org.junit.runners.model.FrameworkMethod;
3234
import org.junit.runners.model.InitializationError;
3335
import org.junit.runners.model.Statement;
36+
import org.junit.runners.model.TestTimedOutException;
3437

3538
/** A specialized {@link BlockJUnit4ClassRunner} that can handle timeouts */
3639
public class AndroidJUnit4ClassRunner extends BlockJUnit4ClassRunner {
@@ -55,13 +58,35 @@ public AndroidJUnit4ClassRunner(Class<?> klass) throws InitializationError {
5558
this(klass, RunnerArgs.parseTestTimeout(getArguments()));
5659
}
5760

61+
private static final ThreadLocal<CountDownLatch> currentTestStartedLatch = new ThreadLocal<>();
62+
private static final ThreadLocal<CountDownLatch> currentTestFinishedLatch = new ThreadLocal<>();
63+
5864
/** Returns a {@link Statement} that invokes {@code method} on {@code test} */
5965
@Override
6066
protected Statement methodInvoker(FrameworkMethod method, Object test) {
67+
final Statement invoker;
6168
if (UiThreadStatement.shouldRunOnUiThread(method)) {
62-
return new UiThreadStatement(super.methodInvoker(method, test), true);
69+
invoker = new UiThreadStatement(super.methodInvoker(method, test), true);
70+
} else {
71+
invoker = super.methodInvoker(method, test);
6372
}
64-
return super.methodInvoker(method, test);
73+
return new Statement() {
74+
@Override
75+
public void evaluate() throws Throwable {
76+
CountDownLatch startLatch = currentTestStartedLatch.get();
77+
if (startLatch != null) {
78+
startLatch.countDown();
79+
}
80+
try {
81+
invoker.evaluate();
82+
} finally {
83+
CountDownLatch finishLatch = currentTestFinishedLatch.get();
84+
if (finishLatch != null) {
85+
finishLatch.countDown();
86+
}
87+
}
88+
}
89+
};
6590
}
6691

6792
@Override
@@ -76,28 +101,76 @@ protected Statement withAfters(FrameworkMethod method, Object target, Statement
76101
return afters.isEmpty() ? statement : new RunAfters(method, statement, afters, target);
77102
}
78103

79-
/**
80-
* Default to {@link org.junit.Test#timeout()} level timeout if set. Otherwise, set the timeout
81-
* that was passed to the instrumentation via argument.
82-
*/
83104
@Override
84-
protected Statement withPotentialTimeout(FrameworkMethod method, Object test, Statement next) {
85-
// test level timeout i.e @Test(timeout = 123)
86-
long timeout = getTimeout(method.getAnnotation(Test.class));
105+
protected Statement methodBlock(FrameworkMethod method) {
106+
final Statement statement = super.methodBlock(method);
87107

88-
// use runner arg timeout if test level timeout is not present
108+
long timeout = getTimeout(method.getAnnotation(Test.class));
89109
if (timeout <= 0 && perTestTimeout > 0) {
90110
timeout = perTestTimeout;
91111
}
112+
final long finalTimeout = timeout;
92113

93-
if (timeout <= 0) {
94-
// no timeout was set
95-
return next;
114+
if (finalTimeout <= 0) {
115+
return statement;
96116
}
97117

98-
// Cannot switch to use builder as that is not supported in JUnit 4.10 which is what is
99-
// available in AOSP.
100-
return new FailOnTimeout(next, timeout);
118+
return new Statement() {
119+
@Override
120+
@SuppressWarnings("Interruption") // We want to interrupt the thread to stop the test.
121+
public void evaluate() throws Throwable {
122+
final AtomicReference<Throwable> failure = new AtomicReference<>();
123+
final CountDownLatch testStartedLatch = new CountDownLatch(1);
124+
final CountDownLatch testFinishedLatch = new CountDownLatch(1);
125+
final CountDownLatch doneLatch = new CountDownLatch(1);
126+
127+
Thread thread =
128+
new Thread(
129+
new Runnable() {
130+
@Override
131+
public void run() {
132+
currentTestStartedLatch.set(testStartedLatch);
133+
currentTestFinishedLatch.set(testFinishedLatch);
134+
try {
135+
statement.evaluate();
136+
} catch (Throwable t) {
137+
failure.set(t);
138+
} finally {
139+
testStartedLatch.countDown();
140+
testFinishedLatch.countDown();
141+
doneLatch.countDown();
142+
currentTestStartedLatch.remove();
143+
currentTestFinishedLatch.remove();
144+
}
145+
}
146+
},
147+
"Time-limited test");
148+
thread.setDaemon(true);
149+
thread.start();
150+
151+
testStartedLatch.await();
152+
boolean finishedInTime = testFinishedLatch.await(finalTimeout, TimeUnit.MILLISECONDS);
153+
154+
if (!finishedInTime) {
155+
thread.interrupt();
156+
throw new TestTimedOutException(finalTimeout, TimeUnit.MILLISECONDS);
157+
}
158+
159+
doneLatch.await();
160+
if (failure.get() != null) {
161+
throw failure.get();
162+
}
163+
}
164+
};
165+
}
166+
167+
/**
168+
* Default to {@link org.junit.Test#timeout()} level timeout if set. Otherwise, set the timeout
169+
* that was passed to the instrumentation via argument.
170+
*/
171+
@Override
172+
protected Statement withPotentialTimeout(FrameworkMethod method, Object test, Statement next) {
173+
return next;
101174
}
102175

103176
private long getTimeout(Test annotation) {

runner/android_junit_runner/javatests/androidx/test/internal/runner/junit4/AndroidAnnotatedBuilderTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@
2626
import java.lang.reflect.InvocationTargetException;
2727
import java.util.Arrays;
2828
import java.util.Collection;
29+
import org.junit.After;
2930
import org.junit.Assert;
3031
import org.junit.Before;
3132
import org.junit.Test;
33+
import org.junit.runner.JUnitCore;
34+
import org.junit.runner.Result;
3235
import org.junit.runner.RunWith;
3336
import org.junit.runner.Runner;
3437
import org.junit.runners.JUnit4;
@@ -133,4 +136,41 @@ public Runner buildAndroidRunner(Class<? extends Runner> runnerClass, Class<?> t
133136
// attempt to create a runner for a class with no @RunWith annotation
134137
ab.runnerForClass(NoRunWithClass.class);
135138
}
139+
140+
@SuppressWarnings("NonFinalStaticField") // Static fields are needed to check thread assignment.
141+
public static class TimeoutTestClass {
142+
static Thread beforeThread;
143+
static Thread testThread;
144+
static Thread afterThread;
145+
146+
@Before
147+
public void before() {
148+
beforeThread = Thread.currentThread();
149+
}
150+
151+
@Test(timeout = 5000)
152+
public void testWithTimeout() {
153+
testThread = Thread.currentThread();
154+
}
155+
156+
@After
157+
public void after() {
158+
afterThread = Thread.currentThread();
159+
}
160+
}
161+
162+
@Test
163+
public void testThreadsSameWithTimeout() throws org.junit.runners.model.InitializationError {
164+
TimeoutTestClass.beforeThread = null;
165+
TimeoutTestClass.testThread = null;
166+
TimeoutTestClass.afterThread = null;
167+
168+
AndroidJUnit4ClassRunner runner = new AndroidJUnit4ClassRunner(TimeoutTestClass.class, 0);
169+
Result result = new JUnitCore().run(runner);
170+
171+
assertEquals(0, result.getFailureCount());
172+
Assert.assertNotNull(TimeoutTestClass.beforeThread);
173+
assertEquals(TimeoutTestClass.beforeThread, TimeoutTestClass.testThread);
174+
assertEquals(TimeoutTestClass.testThread, TimeoutTestClass.afterThread);
175+
}
136176
}

0 commit comments

Comments
 (0)