Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Block
UiController#loopMainThreadUntilIdle
on registered idling res…
…ources being idle. When using the paused looper collect the registered idling resources and wait for them to become idle before returning from `loopMainThreadUntilIdle`. Because Robolectric runs on the same thread as the main looper we need to continually loop the main looper until all idling resources transition to idle state. To do this we'll first drain the looper of currently scheduled tasks and then collect all of the idling resources that are not reporting idle. While this list is not empty (we need to loop as one idling resource becoming idle may cause another idling resource to become not idle, we need to observe them all idle at once) wait on the message queue to receive new messages up to the error timeout. Issue: #4807 PiperOrigin-RevId: 429489649
- Loading branch information
1 parent
31b77be
commit f229c35
Showing
9 changed files
with
552 additions
and
18 deletions.
There are no files selected for viewing
181 changes: 181 additions & 0 deletions
181
...x_test/src/test/java/org/robolectric/integrationtests/axt/EspressoIdlingResourceTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
package org.robolectric.integrationtests.axt; | ||
|
||
import static androidx.test.espresso.Espresso.onIdle; | ||
import static com.google.common.truth.Truth.assertThat; | ||
|
||
import android.os.Handler; | ||
import android.os.HandlerThread; | ||
import android.os.Looper; | ||
import androidx.test.espresso.IdlingRegistry; | ||
import androidx.test.espresso.IdlingResource; | ||
import androidx.test.ext.junit.runners.AndroidJUnit4; | ||
import java.util.concurrent.CountDownLatch; | ||
import java.util.concurrent.ExecutorService; | ||
import java.util.concurrent.Executors; | ||
import java.util.concurrent.atomic.AtomicBoolean; | ||
import org.junit.After; | ||
import org.junit.Before; | ||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
|
||
/** Test Espresso IdlingResource support. */ | ||
@RunWith(AndroidJUnit4.class) | ||
public final class EspressoIdlingResourceTest { | ||
private final IdlingRegistry idlingRegistry = IdlingRegistry.getInstance(); | ||
|
||
private ExecutorService executor; | ||
|
||
@Before | ||
public void setup() { | ||
executor = Executors.newSingleThreadExecutor(); | ||
} | ||
|
||
@After | ||
public void teardown() { | ||
for (IdlingResource resource : idlingRegistry.getResources()) { | ||
idlingRegistry.unregister(resource); | ||
} | ||
for (Looper looper : idlingRegistry.getLoopers()) { | ||
idlingRegistry.unregisterLooperAsIdlingResource(looper); | ||
} | ||
executor.shutdown(); | ||
} | ||
|
||
@Test | ||
public void onIdle_idlingResourceIsIdle_doesntBlock() { | ||
AtomicBoolean didCheckIdle = new AtomicBoolean(); | ||
idlingRegistry.register( | ||
new NamedIdleResource("Test", /* isIdle= */ true) { | ||
@Override | ||
public boolean isIdleNow() { | ||
didCheckIdle.set(true); | ||
return super.isIdleNow(); | ||
} | ||
}); | ||
|
||
onIdle(); | ||
|
||
assertThat(didCheckIdle.get()).isTrue(); | ||
} | ||
|
||
@SuppressWarnings("FutureReturnValueIgnored") | ||
@Test | ||
public void onIdle_postToMainThread() { | ||
idlingRegistry.register( | ||
new NamedIdleResource("Test", /* isIdle= */ false) { | ||
boolean submitted; | ||
|
||
@Override | ||
public boolean isIdleNow() { | ||
if (!submitted) { | ||
submitted = true; | ||
executor.submit(this::postToMainLooper); | ||
} | ||
return super.isIdleNow(); | ||
} | ||
|
||
void postToMainLooper() { | ||
new Handler(Looper.getMainLooper()).post(() -> setIdle(true)); | ||
} | ||
}); | ||
|
||
onIdle(); | ||
} | ||
|
||
@SuppressWarnings("FutureReturnValueIgnored") | ||
@Test | ||
public void onIdle_cooperativeResources() { | ||
NamedIdleResource a = new NamedIdleResource("A", /* isIdle= */ true); | ||
NamedIdleResource b = new NamedIdleResource("B", /* isIdle= */ false); | ||
NamedIdleResource c = new NamedIdleResource("C", /* isIdle= */ false); | ||
idlingRegistry.register(a, b); | ||
executor.submit( | ||
() -> { | ||
a.setIdle(false); | ||
b.setIdle(true); | ||
c.setIdle(false); | ||
executor.submit( | ||
() -> { | ||
a.setIdle(true); | ||
b.setIdle(false); | ||
c.setIdle(false); | ||
executor.submit( | ||
() -> { | ||
a.setIdle(true); | ||
b.setIdle(true); | ||
c.setIdle(true); | ||
}); | ||
}); | ||
}); | ||
|
||
onIdle(); | ||
|
||
assertThat(a.isIdleNow()).isTrue(); | ||
assertThat(b.isIdleNow()).isTrue(); | ||
assertThat(c.isIdleNow()).isTrue(); | ||
} | ||
|
||
@SuppressWarnings("FutureReturnValueIgnored") | ||
@Test | ||
public void onIdle_looperIsIdle() throws Exception { | ||
HandlerThread handlerThread = new HandlerThread("Test"); | ||
try { | ||
handlerThread.start(); | ||
Handler handler = new Handler(handlerThread.getLooper()); | ||
CountDownLatch handlerStarted = new CountDownLatch(1); | ||
CountDownLatch releaseHandler = new CountDownLatch(1); | ||
handler.post( | ||
() -> { | ||
handlerStarted.countDown(); | ||
try { | ||
releaseHandler.await(); | ||
} catch (InterruptedException e) { | ||
// ignore | ||
} | ||
}); | ||
handlerStarted.await(); | ||
idlingRegistry.registerLooperAsIdlingResource(handlerThread.getLooper()); | ||
|
||
executor.submit(releaseHandler::countDown); | ||
onIdle(); | ||
|
||
// onIdle should have blocked on the looper waiting on the release latch | ||
assertThat(releaseHandler.getCount()).isEqualTo(0); | ||
} finally { | ||
handlerThread.quit(); | ||
} | ||
} | ||
|
||
private static class NamedIdleResource implements IdlingResource { | ||
final String name; | ||
final AtomicBoolean isIdle; | ||
ResourceCallback callback; | ||
|
||
NamedIdleResource(String name, boolean isIdle) { | ||
this.name = name; | ||
this.isIdle = new AtomicBoolean(isIdle); | ||
} | ||
|
||
@Override | ||
public String getName() { | ||
return name; | ||
} | ||
|
||
@Override | ||
public boolean isIdleNow() { | ||
return isIdle.get(); | ||
} | ||
|
||
void setIdle(boolean isIdle) { | ||
this.isIdle.set(isIdle); | ||
if (isIdle && callback != null) { | ||
callback.onTransitionToIdle(); | ||
} | ||
} | ||
|
||
@Override | ||
public void registerIdleTransitionCallback(ResourceCallback callback) { | ||
this.callback = callback; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
24 changes: 24 additions & 0 deletions
24
...ectric/src/main/java/org/robolectric/android/internal/IdlingResourceTimeoutException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package org.robolectric.android.internal; | ||
|
||
import static com.google.common.base.Preconditions.checkNotNull; | ||
|
||
import com.google.common.annotations.Beta; | ||
import java.util.List; | ||
import java.util.Locale; | ||
|
||
/** | ||
* Timeout exception thrown when idling resources are not idle for longer than the configured | ||
* timeout. | ||
* | ||
* <p>See {@link androidx.test.espresso.IdlingResourceTimeoutException}. | ||
* | ||
* <p>Note: This API may be removed in the future in favor of using espresso's exception directly. | ||
*/ | ||
@Beta | ||
public final class IdlingResourceTimeoutException extends RuntimeException { | ||
public IdlingResourceTimeoutException(List<String> resourceNames) { | ||
super( | ||
String.format( | ||
Locale.ROOT, "Wait for %s to become idle timed out", checkNotNull(resourceNames))); | ||
} | ||
} |
Oops, something went wrong.