Skip to content

Commit 53a1488

Browse files
brettchabotcopybara-androidxtest
authored andcommitted
Make Espresso release TestLooperManager from main looper.
Previously Espresso's UIControllerImpl would acquire a TestLooperManager and effectively never release it. Which would cause any tests that attempted to acquire the TestLooperManager after any Espresso code was run to fail. This commit changes UIControllerImpl to only acquire the main Looper TestLooperManager when interrogatoring, and release it immediately afterwards. PiperOrigin-RevId: 746524541
1 parent fa44e7f commit 53a1488

File tree

5 files changed

+68
-33
lines changed

5 files changed

+68
-33
lines changed

espresso/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The following artifacts were released:
1717
**Bug Fixes**
1818

1919
* Downgrade to kotlin 1.9
20+
* Only hold main Looper's TestLooperManager during interrogation
2021

2122
**New Features**
2223

espresso/core/java/androidx/test/espresso/base/Interrogator.java

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -92,27 +92,16 @@ interface InterrogationHandler<R> extends QueueInterrogationHandler<R> {
9292
public String getMessage();
9393
}
9494

95-
private final TestLooperManagerCompat testLooperManager;
96-
97-
static Interrogator acquire(Looper looper) {
98-
return new Interrogator(TestLooperManagerCompat.acquire(looper));
99-
}
100-
101-
private Interrogator(TestLooperManagerCompat testLooperManager) {
102-
this.testLooperManager = testLooperManager;
103-
}
104-
105-
void release() {
106-
testLooperManager.release();
107-
}
95+
Interrogator() {}
10896

10997
/**
11098
* Loops the main thread and informs the interrogation handler at interesting points in the exec
11199
* state.
112100
*
113101
* @param handler an interrogation handler that controls whether to continue looping or not.
114102
*/
115-
<T> T loopAndInterrogate(InterrogationHandler<T> handler) {
103+
<T> T loopAndInterrogate(
104+
TestLooperManagerCompat testLooperManager, InterrogationHandler<T> handler) {
116105
checkSanity();
117106
interrogating.set(Boolean.TRUE);
118107
boolean stillInterested = true;
@@ -124,7 +113,7 @@ <T> T loopAndInterrogate(InterrogationHandler<T> handler) {
124113
final long threadIdentity = Binder.clearCallingIdentity();
125114
while (stillInterested) {
126115
// run until the observer is no longer interested.
127-
stillInterested = interrogateQueueState(handler);
116+
stillInterested = interrogateQueueState(testLooperManager, handler);
128117
if (stillInterested) {
129118
Message m = testLooperManager.next();
130119

@@ -179,16 +168,18 @@ <T> T loopAndInterrogate(InterrogationHandler<T> handler) {
179168
* queueEmpty(), taskDueSoon(), taskDueLong() or barrierUp(). once and only once.
180169
* @return the result of handler.get()
181170
*/
182-
<T> T peekAtQueueState(QueueInterrogationHandler<T> handler) {
171+
<T> T peekAtQueueState(
172+
TestLooperManagerCompat testLooperManager, QueueInterrogationHandler<T> handler) {
183173
checkNotNull(handler);
184174
checkState(
185-
!interrogateQueueState(handler),
175+
!interrogateQueueState(testLooperManager, handler),
186176
"It is expected that %s would stop interrogation after a single peak at the queue.",
187177
handler);
188178
return handler.get();
189179
}
190180

191-
private boolean interrogateQueueState(QueueInterrogationHandler<?> handler) {
181+
private boolean interrogateQueueState(
182+
TestLooperManagerCompat testLooperManager, QueueInterrogationHandler<?> handler) {
192183
synchronized (testLooperManager.getQueue()) {
193184
if (testLooperManager.isBlockedOnSyncBarrier()) {
194185
if (Log.isLoggable(TAG, Log.DEBUG)) {

espresso/core/java/androidx/test/espresso/base/LooperIdlingResourceInterrogationHandler.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public boolean barrierUp() {
7171
private volatile Looper looper = null;
7272
private volatile boolean idle = true;
7373

74-
private volatile Interrogator interrogator = null;
74+
private volatile TestLooperManagerCompat testLooperManager = null;
7575

7676
// written on main - read on looper
7777
private volatile IdlingResource.ResourceCallback cb = null;
@@ -99,9 +99,9 @@ static LooperIdlingResourceInterrogationHandler forLooper(Looper l) {
9999
@Override
100100
public void run() {
101101
ir.looper = Looper.myLooper();
102-
ir.interrogator = Interrogator.acquire(ir.looper);
102+
ir.testLooperManager = TestLooperManagerCompat.acquire(ir.looper);
103103
ir.started = true;
104-
ir.interrogator.loopAndInterrogate(ir);
104+
new Interrogator().loopAndInterrogate(ir.testLooperManager, ir);
105105
}
106106
});
107107

@@ -118,7 +118,10 @@ public String getMessage() {
118118

119119
@Override
120120
public void quitting() {
121-
interrogator.release();
121+
if (testLooperManager != null) {
122+
testLooperManager.release();
123+
testLooperManager = null;
124+
}
122125
transitionToIdle();
123126
}
124127

@@ -166,7 +169,8 @@ public boolean isIdleNow() {
166169
// make sure nothing has arrived in the queue while the looper thread is waiting to pull a
167170
// new task out of it. There can be some delay between a new message entering the queue and
168171
// the looper thread pulling it out and processing it.
169-
return Boolean.FALSE.equals(interrogator.peekAtQueueState(queueHasNewTasks));
172+
return Boolean.FALSE.equals(
173+
new Interrogator().peekAtQueueState(testLooperManager, queueHasNewTasks));
170174
}
171175
return false;
172176
}
@@ -189,6 +193,9 @@ private void transitionToIdle() {
189193
}
190194

191195
public void release() {
192-
interrogator.release();
196+
if (testLooperManager != null) {
197+
testLooperManager.release();
198+
testLooperManager = null;
199+
}
193200
}
194201
}

espresso/core/java/androidx/test/espresso/base/UiControllerImpl.java

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -502,13 +502,15 @@ private IdleNotifier<IdleNotificationCallback> loopUntil(
502502
EnumSet<IdleCondition> conditions, IdleNotifier<IdleNotificationCallback> dynamicIdle) {
503503
IdlingPolicy masterIdlePolicy = IdlingPolicies.getMasterIdlingPolicy();
504504
IdlingPolicy dynamicIdlePolicy = IdlingPolicies.getDynamicIdlingResourceErrorPolicy();
505+
TestLooperManagerCompat testLooperManager = TestLooperManagerCompat.acquire(mainLooper);
505506
try {
506507
long start = SystemClock.uptimeMillis();
507508
long end =
508509
start + masterIdlePolicy.getIdleTimeoutUnit().toMillis(masterIdlePolicy.getIdleTimeout());
509510
interrogation = new MainThreadInterrogation(conditions, conditionSet, end);
510511

511-
InterrogationStatus result = getInterrogator().loopAndInterrogate(interrogation);
512+
InterrogationStatus result =
513+
new Interrogator().loopAndInterrogate(testLooperManager, interrogation);
512514
if (InterrogationStatus.COMPLETED == result) {
513515
// did not time out, all conditions happy.
514516
return dynamicIdle;
@@ -582,17 +584,10 @@ private IdleNotifier<IdleNotificationCallback> loopUntil(
582584
condition.reset(conditionSet);
583585
}
584586
interrogation = null;
587+
testLooperManager.release();
585588
}
586589
return dynamicIdle;
587590
}
588-
589-
private Interrogator getInterrogator() {
590-
if (interrogator == null) {
591-
interrogator = Interrogator.acquire(mainLooper);
592-
}
593-
return interrogator;
594-
}
595-
596591
@Override
597592
public void interruptEspressoTasks() {
598593
controllerHandler.post(

espresso/core/javatests/androidx/test/espresso/base/EspressoIdleTest.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package androidx.test.espresso.base;
22

33
import static android.os.Looper.getMainLooper;
4+
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
45
import static com.google.common.truth.Truth.assertThat;
6+
import static org.junit.Assume.assumeTrue;
57

8+
import android.os.Build.VERSION;
69
import android.os.Handler;
710
import android.os.HandlerThread;
811
import android.os.Looper;
12+
import android.os.TestLooperManager;
913
import androidx.test.espresso.Espresso;
1014
import androidx.test.espresso.IdlingRegistry;
1115
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -64,4 +68,41 @@ public void onIdle_afterPost_backgroundLooper() {
6468
ht.quit();
6569
}
6670
}
71+
72+
/** Verify TestLooperManager can be used after Espresso idle is run. */
73+
@Test
74+
public void onIdle_with_TestLooperManager() {
75+
assumeTrue(VERSION.SDK_INT >= 36);
76+
77+
Espresso.onIdle();
78+
79+
TestLooperManager manager = getInstrumentation().acquireLooperManager(getMainLooper());
80+
assertThat(manager).isNotNull();
81+
manager.release();
82+
83+
Espresso.onIdle();
84+
}
85+
86+
@Test
87+
public void onIdle_backgroundLooper_with_TestLooperManager() throws InterruptedException {
88+
assumeTrue(VERSION.SDK_INT >= 36);
89+
90+
HandlerThread ht = new HandlerThread("onIdle_backgroundLooper_with_TestLooperManager");
91+
ht.start();
92+
Looper looper = ht.getLooper();
93+
94+
IdlingRegistry.getInstance().registerLooperAsIdlingResource(looper);
95+
AtomicBoolean wasRun = new AtomicBoolean(false);
96+
new Handler(looper).post(() -> wasRun.set(true));
97+
Espresso.onIdle();
98+
assertThat(wasRun.get()).isTrue();
99+
IdlingRegistry.getInstance().unregisterLooperAsIdlingResource(looper);
100+
101+
Espresso.onIdle();
102+
103+
TestLooperManager manager = getInstrumentation().acquireLooperManager(looper);
104+
assertThat(manager).isNotNull();
105+
manager.release();
106+
ht.quit();
107+
}
67108
}

0 commit comments

Comments
 (0)