From 13a3aa7e770a6170eea9abacd4525b900bc5a9dc Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 25 Apr 2024 07:40:45 -0700 Subject: [PATCH] Add TestPlayerRunHelper run(player).untilBackgroundThreadCondition(..) This method is useful for cases where the target condition can become true outside of a message on the main thread. To ensure we don't execute the rest of the test method in parallel with other code, we have to introduce artifical messages on the main thread that check the target condition. PiperOrigin-RevId: 628072444 --- .../utils/robolectric/RobolectricUtil.java | 24 +++++++++++ .../robolectric/TestPlayerRunHelper.java | 42 +++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/RobolectricUtil.java b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/RobolectricUtil.java index bff6778b3f..c814bf3ce0 100644 --- a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/RobolectricUtil.java +++ b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/RobolectricUtil.java @@ -63,6 +63,12 @@ public final class RobolectricUtil { * *

Must be called on the main test thread. * + *

Note for {@link androidx.media3.test.utils.FakeClock} users: If the condition changes + * outside of a main {@link Looper} message, for example because it's checking a volatile variable + * or shared synchronized state that is updated on a background thread, or because checking the + * condition itself may cause it to become true, then the remainder of the test method may be + * executed in parallel with other background thread messages. + * * @param condition The condition. * @throws TimeoutException If the {@link #DEFAULT_TIMEOUT_MS} is exceeded. */ @@ -76,6 +82,12 @@ public final class RobolectricUtil { * *

Must be called on the main test thread. * + *

Note for {@link androidx.media3.test.utils.FakeClock} users: If the condition changes + * outside of a main {@link Looper} message, for example because it's checking a volatile variable + * or shared synchronized state that is updated on a background thread, or because checking the + * condition itself may cause it to become true, then the remainder of the test method may be + * executed in parallel with other background thread messages. + * * @param condition The condition. * @param timeoutMs The timeout in milliseconds. * @param clock The {@link Clock} to measure the timeout. @@ -91,6 +103,12 @@ public final class RobolectricUtil { * *

Must be called on the thread corresponding to the {@code looper}. * + *

Note for {@link androidx.media3.test.utils.FakeClock} users: If the condition changes + * outside of a message on this {@code Looper}, for example because it's checking a volatile + * variable or shared synchronized state that is updated on a background thread, or because + * checking the condition itself may cause it to become true, then the remainder of the test + * method may be executed in parallel with other background thread messages. + * * @param looper The {@link Looper}. * @param condition The condition. * @throws TimeoutException If the {@link #DEFAULT_TIMEOUT_MS} is exceeded. @@ -105,6 +123,12 @@ public final class RobolectricUtil { * *

Must be called on the thread corresponding to the {@code looper}. * + *

Note for {@link androidx.media3.test.utils.FakeClock} users: If the condition changes + * outside of a message on this {@code Looper}, for example because it's checking a volatile + * variable or shared synchronized state that is updated on a background thread, or because + * checking the condition itself may cause it to become true, then the remainder of the test + * method may be executed in parallel with other background thread messages. + * * @param looper The {@link Looper}. * @param condition The condition. * @param timeoutMs The timeout in milliseconds. diff --git a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java index d9106acdbe..b28f4a92ee 100644 --- a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java +++ b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java @@ -26,6 +26,7 @@ import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; import androidx.media3.common.Timeline; import androidx.media3.common.util.ConditionVariable; +import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.NullableType; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; @@ -411,6 +412,47 @@ public final class TestPlayerRunHelper { runMainLooperUntil(receivedMessageCallback::get); } + /** + * Runs tasks of the main {@link Looper} until the specified condition becomes true independent + * of a message on the main {@link Looper}. + * + *

This method is useful for cases where the condition may change outside of a main {@link + * Looper} message, for example because it's checking a volatile variable or shared synchronized + * state that is updated on a background thread, or because checking the condition itself may + * cause it to become true. + * + *

This method ensures the condition is checked within artificially created main {@link + * Looper} messages. When using a {@link androidx.media3.test.utils.FakeClock}, this guarantees + * the remainder of the test method is not executed in parallel with other background thread + * messages. + * + * @param backgroundThreadCondition The condition to wait for. + * @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is + * exceeded. + */ + public void untilBackgroundThreadCondition(Supplier backgroundThreadCondition) + throws Exception { + if (backgroundThreadCondition.get()) { + return; + } + AtomicBoolean conditionTrue = new AtomicBoolean(); + HandlerWrapper handler = + player.getClock().createHandler(Util.getCurrentOrMainLooper(), /* callback= */ null); + Runnable checkCondition = + new Runnable() { + @Override + public void run() { + if (backgroundThreadCondition.get()) { + conditionTrue.set(true); + } else { + handler.postDelayed(this, /* delayMs= */ 1); + } + } + }; + handler.post(checkCondition); + runUntil(conditionTrue::get); + } + @Override public ExoPlayerRunResult ignoringNonFatalErrors() { checkState(!hasBeenUsed);