aboutsummaryrefslogtreecommitdiff
path: root/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java
diff options
context:
space:
mode:
Diffstat (limited to 'robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java')
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java137
1 files changed, 135 insertions, 2 deletions
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java
index 71e85c9b6..ebb9e02d8 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java
@@ -13,6 +13,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
+import static org.robolectric.util.reflector.Reflector.reflector;
import android.os.Build.VERSION_CODES;
import android.os.Handler;
@@ -21,6 +22,7 @@ import android.os.Looper;
import android.os.MessageQueue.IdleHandler;
import android.os.SystemClock;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.util.concurrent.SettableFuture;
import java.time.Duration;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
@@ -28,16 +30,20 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.res.android.Ref;
import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.reflector.Direct;
+import org.robolectric.util.reflector.ForType;
@RunWith(AndroidJUnit4.class)
@LooperMode(LooperMode.Mode.PAUSED)
@@ -643,6 +649,42 @@ public class ShadowPausedLooperTest {
assertThat(wasRun.get()).isTrue();
}
+ /**
+ * Tests a race condition that could occur if a paused background Looper was quit but the thread
+ * was still alive. The resetter would attempt to unpause it, but the message would never run
+ * because the looper was quit. This caused a deadlock.
+ */
+ @Test
+ public void looper_customThread_unPauseAfterQuit() throws Exception {
+ for (int i = 0; i < 100; i++) {
+ final SettableFuture<Looper> future = SettableFuture.create();
+ final CountDownLatch countDownLatch = new CountDownLatch(1);
+
+ Thread t =
+ new Thread(
+ () -> {
+ try {
+ Looper.prepare();
+ } finally {
+ future.set(Looper.myLooper());
+ }
+ Looper.loop();
+ try {
+ countDownLatch.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ });
+ t.start();
+ Looper looper = future.get();
+ shadowOf(looper).pause();
+ new Handler(looper).post(() -> looper.quitSafely());
+ shadowOf(looper).idle();
+ ((ShadowPausedLooper) shadowOf(looper)).resetLooperToInitialState();
+ countDownLatch.countDown();
+ }
+ }
+
@Test
@Config(minSdk = VERSION_CODES.P)
public void runOneTask_ignoreSyncBarrier_with_async() {
@@ -660,6 +702,73 @@ public class ShadowPausedLooperTest {
Looper.getMainLooper().getQueue().removeSyncBarrier(barrier);
}
+ /**
+ * Verify that calling a control operation like idle while a sync barrier is being held doesn't
+ * deadlock the looper
+ */
+ @Test
+ public void idle_paused_onSyncBarrier() {
+
+ Handler handler = new Handler(handlerThread.getLooper());
+ Handler asyncHandler = ShadowPausedLooper.createAsyncHandler(handlerThread.getLooper());
+ ShadowPausedLooper shadowLooper = Shadow.extract(handlerThread.getLooper());
+ shadowLooper.pause();
+
+ AtomicInteger token = new AtomicInteger(-1);
+ AtomicBoolean wasRun = new AtomicBoolean(false);
+ handler.post(
+ () -> {
+ token.set(postSyncBarrierCompat(handlerThread.getLooper()));
+ handler.post(
+ () -> {
+ wasRun.set(true);
+ });
+ });
+ shadowLooper.idle();
+ assertThat(token.get()).isNotEqualTo(-1);
+ assertThat(wasRun.get()).isEqualTo(false);
+ // should be effectively a no-op and not deadlock
+ shadowLooper.idle();
+ // remove sync barriers messages need to get posted as async
+ asyncHandler.post(
+ () -> {
+ removeSyncBarrierCompat(handlerThread.getLooper(), token.get());
+ });
+ shadowLooper.idle();
+ assertThat(wasRun.get()).isEqualTo(true);
+ }
+
+ /** Similar to previous test but with a running aka unpaused looper. */
+ @Test
+ public void idle_running_onSyncBarrier() {
+ Handler handler = new Handler(handlerThread.getLooper());
+ Handler asyncHandler = ShadowPausedLooper.createAsyncHandler(handlerThread.getLooper());
+ ShadowPausedLooper shadowLooper = Shadow.extract(handlerThread.getLooper());
+
+ AtomicInteger token = new AtomicInteger(-1);
+ AtomicBoolean wasRun = new AtomicBoolean(false);
+ handler.post(
+ () -> {
+ token.set(postSyncBarrierCompat(handlerThread.getLooper()));
+ handler.post(
+ () -> {
+ wasRun.set(true);
+ });
+ });
+ shadowLooper.idle();
+ assertThat(token.get()).isNotEqualTo(-1);
+ assertThat(wasRun.get()).isEqualTo(false);
+ // should be effectively a no-op and not deadlock
+ shadowLooper.idle();
+ // remove sync barriers messages need to get posted as async
+ asyncHandler.post(
+ () -> {
+ removeSyncBarrierCompat(handlerThread.getLooper(), token.get());
+ });
+ shadowLooper.idle();
+ assertThat(wasRun.get()).isEqualTo(true);
+ }
+
private static class BlockingRunnable implements Runnable {
CountDownLatch latch = new CountDownLatch(1);
@@ -675,7 +784,31 @@ public class ShadowPausedLooperTest {
private void postToMainLooper() {
// just post a runnable and rely on setUp to check
Handler handler = new Handler(getMainLooper());
- Runnable mockRunnable = mock(Runnable.class);
- handler.post(mockRunnable);
+ handler.post(() -> {});
+ }
+
+ private static int postSyncBarrierCompat(Looper looper) {
+ if (RuntimeEnvironment.getApiLevel() >= 23) {
+ return looper.getQueue().postSyncBarrier();
+ } else {
+ return reflector(LooperReflector.class, looper).postSyncBarrier();
+ }
+ }
+
+ private static void removeSyncBarrierCompat(Looper looper, int token) {
+ if (RuntimeEnvironment.getApiLevel() >= 23) {
+ looper.getQueue().removeSyncBarrier(token);
+ } else {
+ reflector(LooperReflector.class, looper).removeSyncBarrier(token);
+ }
+ }
+
+ @ForType(Looper.class)
+ private interface LooperReflector {
+ @Direct
+ int postSyncBarrier();
+
+ @Direct
+ void removeSyncBarrier(int token);
}
}