1616package androidx .test .internal .runner .junit4 ;
1717
1818import static androidx .test .platform .app .InstrumentationRegistry .getArguments ;
19+ import static java .util .concurrent .TimeUnit .MILLISECONDS ;
1920
2021import androidx .test .internal .runner .RunnerArgs ;
2122import androidx .test .internal .runner .junit4 .statement .RunAfters ;
2223import androidx .test .internal .runner .junit4 .statement .RunBefores ;
2324import androidx .test .internal .runner .junit4 .statement .UiThreadStatement ;
2425import androidx .test .internal .util .AndroidRunnerParams ;
2526import java .util .List ;
27+ import java .util .concurrent .CountDownLatch ;
28+ import java .util .concurrent .atomic .AtomicReference ;
2629import org .junit .After ;
2730import org .junit .Before ;
2831import org .junit .Test ;
29- import org .junit .internal .runners .statements .FailOnTimeout ;
3032import org .junit .runners .BlockJUnit4ClassRunner ;
3133import org .junit .runners .model .FrameworkMethod ;
3234import org .junit .runners .model .InitializationError ;
3335import org .junit .runners .model .Statement ;
36+ import org .junit .runners .model .TestTimedOutException ;
3437
3538/** A specialized {@link BlockJUnit4ClassRunner} that can handle timeouts */
3639public 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 , MILLISECONDS );
153+
154+ if (!finishedInTime ) {
155+ thread .interrupt ();
156+ throw new TestTimedOutException (finalTimeout , 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 ) {
0 commit comments