Keep playback thread blocked until next action with playUntilPosition
We currently pause playback to prevent further progress while the app thread runs assertion and triggers additional actions. This is not ideal because we do not actually want to pause playback in almost all cases where this method used. This can be improved by keeping the playback thread blocked and only unblock it the next time the app thread waits for the player (either via RobolectricUtil methods or by blocking the thread itself). To add this automatic handling, this change introduces a new util class for the tests that can keep the list of waiting threads statically (because the access to this logic is spread across multiple independent classes). PiperOrigin-RevId: 589784204
This commit is contained in:
parent
00d5b6ec99
commit
00c7a9bcbb
@ -120,6 +120,9 @@
|
||||
media fails on the cast device
|
||||
([#708](https://github.com/androidx/media/issues/708)).
|
||||
* Test Utilities:
|
||||
* Don't pause playback in `TestPlayerRunHelper.playUntilPosition`. The
|
||||
test keeps the playback in a playing state, but suspends progress until
|
||||
the test is able to add assertions and further actions.
|
||||
* Remove deprecated symbols:
|
||||
* Demo app:
|
||||
* Add a shortform demo module to demo the usage of `PreloadMediaSource`
|
||||
|
@ -547,10 +547,8 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
WINDOW_0 /* BUFFERING */,
|
||||
period0 /* READY */,
|
||||
period0 /* setPlayWhenReady=true */,
|
||||
period0 /* setPlayWhenReady=false */,
|
||||
period0 /* BUFFERING */,
|
||||
period0 /* READY */,
|
||||
period0 /* setPlayWhenReady=true */,
|
||||
period1Seq2 /* ENDED */)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
||||
@ -829,10 +827,8 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
WINDOW_0 /* BUFFERING */,
|
||||
window0Period1Seq0 /* READY */,
|
||||
window0Period1Seq0 /* setPlayWhenReady=true */,
|
||||
window0Period1Seq0 /* setPlayWhenReady=false */,
|
||||
period1Seq0 /* BUFFERING */,
|
||||
period1Seq0 /* READY */,
|
||||
period1Seq0 /* setPlayWhenReady=true */,
|
||||
period1Seq0 /* ENDED */)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
||||
@ -1107,10 +1103,6 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
WINDOW_0 /* BUFFERING */,
|
||||
prerollAd /* READY */,
|
||||
prerollAd /* setPlayWhenReady=true */,
|
||||
contentAfterPreroll /* setPlayWhenReady=false */,
|
||||
contentAfterPreroll /* setPlayWhenReady=true */,
|
||||
contentAfterMidroll /* setPlayWhenReady=false */,
|
||||
contentAfterMidroll /* setPlayWhenReady=true */,
|
||||
contentAfterPostroll /* ENDED */)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
||||
@ -1186,8 +1178,7 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
contentAfterPostroll)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES))
|
||||
.containsExactly(contentAfterPreroll, contentAfterMidroll, contentAfterPostroll)
|
||||
.inOrder();
|
||||
.containsExactly(contentAfterPostroll);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED))
|
||||
.containsExactly(prerollAd) // First frame rendered
|
||||
.inOrder();
|
||||
@ -1201,8 +1192,7 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
contentAfterPostroll)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET))
|
||||
.containsExactly(contentAfterPreroll, contentAfterMidroll, contentAfterPostroll)
|
||||
.inOrder();
|
||||
.containsExactly(contentAfterPostroll);
|
||||
listener.assertNoMoreEvents();
|
||||
}
|
||||
|
||||
|
@ -182,7 +182,8 @@ public class FakeClock implements Clock {
|
||||
// This isn't a looper message created by this class, so no need to handle the blocking.
|
||||
return;
|
||||
}
|
||||
busyLoopers.add(checkNotNull(Looper.myLooper()));
|
||||
busyLoopers.add(currentLooper);
|
||||
ThreadTestUtil.unblockThreadsWaitingForProgressOnCurrentLooper();
|
||||
waitingForMessage = false;
|
||||
maybeTriggerMessage();
|
||||
}
|
||||
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.test.utils;
|
||||
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.ConditionVariable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
|
||||
/** Static utility to coordinate threads in testing environments. */
|
||||
@UnstableApi
|
||||
public final class ThreadTestUtil {
|
||||
|
||||
@GuardedBy("blockedThreadConditions")
|
||||
private static final ArrayListMultimap<Looper, ConditionVariable> blockedThreadConditions =
|
||||
ArrayListMultimap.create();
|
||||
|
||||
/**
|
||||
* Registers that the current thread will be blocked with the provided {@link ConditionVariable}
|
||||
* until the specified {@link Looper} reports to have made progress via {@link
|
||||
* #unblockThreadsWaitingForProgressOnCurrentLooper()}.
|
||||
*
|
||||
* @param conditionVariable The {@link ConditionVariable} that will block the current thread.
|
||||
* @param looper The {@link Looper} that must report progress to unblock the current thread. Must
|
||||
* not be the {@link Looper} of the current thread.
|
||||
*/
|
||||
public static void registerThreadIsBlockedUntilProgressOnLooper(
|
||||
ConditionVariable conditionVariable, Looper looper) {
|
||||
Assertions.checkArgument(looper != Looper.myLooper());
|
||||
synchronized (blockedThreadConditions) {
|
||||
blockedThreadConditions.put(looper, conditionVariable);
|
||||
}
|
||||
}
|
||||
|
||||
/** Unblocks any threads that are waiting for progress on the current {@link Looper} thread. */
|
||||
public static void unblockThreadsWaitingForProgressOnCurrentLooper() {
|
||||
Looper myLooper = Assertions.checkNotNull(Looper.myLooper());
|
||||
synchronized (blockedThreadConditions) {
|
||||
for (ConditionVariable condition : blockedThreadConditions.removeAll(myLooper)) {
|
||||
condition.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ThreadTestUtil() {}
|
||||
}
|
@ -23,13 +23,13 @@ import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.os.ConditionVariable;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.widget.Button;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.ConditionVariable;
|
||||
import androidx.media3.common.util.HandlerWrapper;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.base.Objects;
|
||||
@ -322,7 +322,8 @@ public final class FakeClockTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createHandler_multiThreadCommunication_deliversMessagesDeterministicallyInOrder() {
|
||||
public void createHandler_multiThreadCommunication_deliversMessagesDeterministicallyInOrder()
|
||||
throws Exception {
|
||||
HandlerThread handlerThread1 = new HandlerThread("FakeClockTest");
|
||||
handlerThread1.start();
|
||||
HandlerThread handlerThread2 = new HandlerThread("FakeClockTest");
|
||||
@ -362,7 +363,7 @@ public final class FakeClockTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createHandler_blockingThreadWithOnBusyWaiting_canBeUnblockedByOtherThread() {
|
||||
public void createHandler_blockingThreadWithOnThreadBlocked_canBeUnblockedByOtherThread() {
|
||||
HandlerThread handlerThread1 = new HandlerThread("FakeClockTest");
|
||||
handlerThread1.start();
|
||||
HandlerThread handlerThread2 = new HandlerThread("FakeClockTest");
|
||||
@ -386,7 +387,11 @@ public final class FakeClockTest {
|
||||
/* delayMs= */ 50);
|
||||
handler1.post(() -> executionOrder.add(4));
|
||||
fakeClock.onThreadBlocked();
|
||||
blockingCondition.block();
|
||||
try {
|
||||
blockingCondition.block();
|
||||
} catch (InterruptedException e) {
|
||||
// Ignore.
|
||||
}
|
||||
executionOrder.add(3);
|
||||
});
|
||||
ShadowLooper.idleMainLooper();
|
||||
@ -399,7 +404,102 @@ public final class FakeClockTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createHandler_messageOnDeadThread_doesNotBlockExecution() {
|
||||
public void
|
||||
createHandler_blockingThreadUntilProgressOnLooperWithOnThreadBlocked_canBeUnblockedByOtherThread() {
|
||||
HandlerThread handlerThread1 = new HandlerThread("FakeClockTest");
|
||||
handlerThread1.start();
|
||||
HandlerThread handlerThread2 = new HandlerThread("FakeClockTest");
|
||||
handlerThread2.start();
|
||||
FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 0, /* isAutoAdvancing= */ true);
|
||||
HandlerWrapper handler1 =
|
||||
fakeClock.createHandler(handlerThread1.getLooper(), /* callback= */ null);
|
||||
HandlerWrapper handler2 =
|
||||
fakeClock.createHandler(handlerThread2.getLooper(), /* callback= */ null);
|
||||
|
||||
ArrayList<Integer> executionOrder = new ArrayList<>();
|
||||
handler1.post(
|
||||
() -> {
|
||||
executionOrder.add(1);
|
||||
ConditionVariable blockingCondition = new ConditionVariable();
|
||||
handler2.postDelayed(
|
||||
() -> {
|
||||
executionOrder.add(2);
|
||||
ThreadTestUtil.unblockThreadsWaitingForProgressOnCurrentLooper();
|
||||
},
|
||||
/* delayMs= */ 100);
|
||||
handler1.post(() -> executionOrder.add(4));
|
||||
ThreadTestUtil.registerThreadIsBlockedUntilProgressOnLooper(
|
||||
blockingCondition, handlerThread2.getLooper());
|
||||
fakeClock.onThreadBlocked();
|
||||
try {
|
||||
blockingCondition.block();
|
||||
} catch (InterruptedException e) {
|
||||
// Ignore.
|
||||
}
|
||||
executionOrder.add(3);
|
||||
});
|
||||
ShadowLooper.idleMainLooper();
|
||||
shadowOf(handler1.getLooper()).idle();
|
||||
shadowOf(handler2.getLooper()).idle();
|
||||
handlerThread1.quitSafely();
|
||||
handlerThread2.quitSafely();
|
||||
|
||||
assertThat(executionOrder).containsExactly(1, 2, 3, 4).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createHandler_blockingDeadlock_unblocksItself() {
|
||||
HandlerThread handlerThread1 = new HandlerThread("FakeClockTest");
|
||||
handlerThread1.start();
|
||||
HandlerThread handlerThread2 = new HandlerThread("FakeClockTest");
|
||||
handlerThread2.start();
|
||||
FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 0, /* isAutoAdvancing= */ true);
|
||||
HandlerWrapper handler1 =
|
||||
fakeClock.createHandler(handlerThread1.getLooper(), /* callback= */ null);
|
||||
HandlerWrapper handler2 =
|
||||
fakeClock.createHandler(handlerThread2.getLooper(), /* callback= */ null);
|
||||
|
||||
ArrayList<Integer> executionOrder = new ArrayList<>();
|
||||
|
||||
handler1.post(
|
||||
() -> {
|
||||
executionOrder.add(1);
|
||||
ConditionVariable deadlockCondition1 = new ConditionVariable();
|
||||
ConditionVariable deadlockCondition2 = new ConditionVariable();
|
||||
handler2.postDelayed(
|
||||
() -> {
|
||||
executionOrder.add(2);
|
||||
fakeClock.onThreadBlocked();
|
||||
try {
|
||||
deadlockCondition2.block();
|
||||
} catch (InterruptedException e) {
|
||||
// Ignore.
|
||||
}
|
||||
},
|
||||
/* delayMs= */ 100);
|
||||
handler1.post(() -> executionOrder.add(4));
|
||||
ThreadTestUtil.registerThreadIsBlockedUntilProgressOnLooper(
|
||||
deadlockCondition1, handlerThread2.getLooper());
|
||||
fakeClock.onThreadBlocked();
|
||||
try {
|
||||
deadlockCondition1.block();
|
||||
} catch (InterruptedException e) {
|
||||
// Ignore.
|
||||
}
|
||||
executionOrder.add(3);
|
||||
deadlockCondition2.open();
|
||||
});
|
||||
ShadowLooper.idleMainLooper();
|
||||
shadowOf(handler1.getLooper()).idle();
|
||||
shadowOf(handler2.getLooper()).idle();
|
||||
handlerThread1.quitSafely();
|
||||
handlerThread2.quitSafely();
|
||||
|
||||
assertThat(executionOrder).containsExactly(1, 2, 3, 4).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createHandler_messageOnDeadThread_doesNotBlockExecution() throws Exception {
|
||||
HandlerThread handlerThread1 = new HandlerThread("FakeClockTest");
|
||||
handlerThread1.start();
|
||||
HandlerThread handlerThread2 = new HandlerThread("FakeClockTest");
|
||||
|
@ -22,6 +22,7 @@ import androidx.media3.common.util.Clock;
|
||||
import androidx.media3.common.util.ConditionVariable;
|
||||
import androidx.media3.common.util.SystemClock;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.test.utils.ThreadTestUtil;
|
||||
import com.google.common.base.Supplier;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import org.robolectric.shadows.ShadowLooper;
|
||||
@ -116,6 +117,7 @@ public final class RobolectricUtil {
|
||||
if (Looper.myLooper() != looper) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
ThreadTestUtil.unblockThreadsWaitingForProgressOnCurrentLooper();
|
||||
ShadowLooper shadowLooper = shadowOf(looper);
|
||||
long timeoutTimeMs = clock.currentTimeMillis() + timeoutMs;
|
||||
while (!condition.get()) {
|
||||
|
@ -29,6 +29,7 @@ import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.exoplayer.ExoPlaybackException;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.test.utils.ThreadTestUtil;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@ -289,7 +290,12 @@ public class TestPlayerRunHelper {
|
||||
|
||||
/**
|
||||
* Calls {@link Player#play()}, runs tasks of the main {@link Looper} until the {@code player}
|
||||
* reaches the specified position or a playback error occurs, and then pauses the {@code player}.
|
||||
* reaches the specified position or a playback error occurs.
|
||||
*
|
||||
* <p>The playback thread is automatically blocked from making further progress after reaching
|
||||
* this position and will only be unblocked by other {@code run/playUntil...} methods, custom
|
||||
* {@link RobolectricUtil#runMainLooperUntil} conditions or an explicit {@link
|
||||
* ThreadTestUtil#unblockThreadsWaitingForProgressOnCurrentLooper()} on the main thread.
|
||||
*
|
||||
* <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
||||
*
|
||||
@ -308,17 +314,14 @@ public class TestPlayerRunHelper {
|
||||
player
|
||||
.createMessage(
|
||||
(messageType, payload) -> {
|
||||
// Block playback thread until pause command has been sent from test thread.
|
||||
// Block playback thread until the main app thread is able to trigger further actions.
|
||||
ConditionVariable blockPlaybackThreadCondition = new ConditionVariable();
|
||||
ThreadTestUtil.registerThreadIsBlockedUntilProgressOnLooper(
|
||||
blockPlaybackThreadCondition, applicationLooper);
|
||||
player
|
||||
.getClock()
|
||||
.createHandler(applicationLooper, /* callback= */ null)
|
||||
.post(
|
||||
() -> {
|
||||
player.pause();
|
||||
messageHandled.set(true);
|
||||
blockPlaybackThreadCondition.open();
|
||||
});
|
||||
.post(() -> messageHandled.set(true));
|
||||
try {
|
||||
player.getClock().onThreadBlocked();
|
||||
blockPlaybackThreadCondition.block();
|
||||
|
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2023 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest package="androidx.media3.test.utils.robolectric.test">
|
||||
<uses-sdk/>
|
||||
</manifest>
|
@ -23,8 +23,10 @@ import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.os.Looper;
|
||||
import androidx.media3.common.util.Clock;
|
||||
import androidx.media3.common.util.ConditionVariable;
|
||||
import androidx.media3.test.utils.ThreadTestUtil;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.base.Supplier;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
@ -90,4 +92,35 @@ public class RobolectricUtilTest {
|
||||
|
||||
verify(mockCondition, times(5)).get();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
runMainLooperUntil_whenConditionIsBlockedOnOtherThreadWaitingForProgress_unblocksItself()
|
||||
throws Exception {
|
||||
Clock mockClock = mock(Clock.class);
|
||||
ConditionVariable testCondition = new ConditionVariable();
|
||||
ConditionVariable testThreadReady = new ConditionVariable();
|
||||
Thread thread =
|
||||
new Thread("RobolectricUtilsTest") {
|
||||
@Override
|
||||
public void run() {
|
||||
ConditionVariable blockedCondition = new ConditionVariable();
|
||||
ThreadTestUtil.registerThreadIsBlockedUntilProgressOnLooper(
|
||||
blockedCondition, Looper.getMainLooper());
|
||||
testThreadReady.open();
|
||||
try {
|
||||
blockedCondition.block();
|
||||
} catch (InterruptedException e) {
|
||||
// Ignore.
|
||||
}
|
||||
testCondition.open();
|
||||
}
|
||||
};
|
||||
thread.start();
|
||||
testThreadReady.block();
|
||||
|
||||
// Verify the thread gets unblocked.
|
||||
RobolectricUtil.runMainLooperUntil(testCondition::isOpen, /* timeoutMs= */ 42, mockClock);
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user