Make FakeClock fully deterministic.

This is achieved by only triggering one message at a time. After
triggering a message we send another to ourselves to know when the
following message can be triggered.

Other required changes:
 - The messages need to be sorted correctly (by time and creation order)
 - To prevent deadlocks when one thread is waiting for another,
   we need to add new method to Clock to indicate that the current
   thread is about to wait. This then allows us to trigger messages
   from other threads in FakeClock.
 - AnalyticsCollectorTest needed some adjustments:
   - onTimelineChanged now deterministically arrives after the initial
     timline is already known, so some of the period information changes
     from window only to full period info.
   - The playlistOperations test suffers from a bug that the first frame
     is rendered too early and that's why we now get additional events.

PiperOrigin-RevId: 353877832
This commit is contained in:
tonihei 2021-01-26 17:02:47 +00:00 committed by Ian Baker
parent a318e56d15
commit 2e52c0b8d8
15 changed files with 331 additions and 170 deletions

View File

@ -50,4 +50,12 @@ public interface Clock {
* @see Handler#Handler(Looper, Handler.Callback)
*/
HandlerWrapper createHandler(Looper looper, @Nullable Handler.Callback callback);
/**
* Notifies the clock that the current thread is about to be blocked and won't return until a
* condition on another thread becomes true.
*
* <p>Should be a no-op for all non-test cases.
*/
void onThreadBlocked();
}

View File

@ -47,4 +47,9 @@ public class SystemClock implements Clock {
public HandlerWrapper createHandler(Looper looper, @Nullable Callback callback) {
return new SystemHandlerWrapper(new Handler(looper, callback));
}
@Override
public void onThreadBlocked() {
// Do nothing.
}
}

View File

@ -624,6 +624,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
boolean wasInterrupted = false;
while (!condition.get() && remainingMs > 0) {
try {
clock.onThreadBlocked();
wait(remainingMs);
} catch (InterruptedException e) {
wasInterrupted = true;

View File

@ -341,6 +341,7 @@ public final class PlayerMessage {
long deadlineMs = clock.elapsedRealtime() + timeoutMs;
long remainingMs = timeoutMs;
while (!isProcessed && remainingMs > 0) {
clock.onThreadBlocked();
wait(remainingMs);
remainingMs = deadlineMs - clock.elapsedRealtime();
}

View File

@ -130,7 +130,6 @@ import org.mockito.ArgumentMatcher;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.robolectric.shadows.ShadowAudioManager;
import org.robolectric.shadows.ShadowLooper;
/** Unit test for {@link ExoPlayer}. */
@RunWith(AndroidJUnit4.class)
@ -1003,11 +1002,15 @@ public final class ExoPlayerTest {
.waitForPlaybackState(Player.STATE_BUFFERING)
// Block until createPeriod has been called on the fake media source.
.executeRunnable(
() -> {
try {
createPeriodCalledCountDownLatch.await();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
try {
player.getClock().onThreadBlocked();
createPeriodCalledCountDownLatch.await();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
})
// Set playback speed (while the fake media period is not yet prepared).
@ -1023,7 +1026,6 @@ public final class ExoPlayerTest {
.blockUntilEnded(TIMEOUT_MS);
}
@Ignore // Temporarily disabled because the AutoAdvancingFakeClock picks up the wrong thread.
@Test
public void seekBeforePreparationCompletes_seeksToCorrectPosition() throws Exception {
CountDownLatch createPeriodCalledCountDownLatch = new CountDownLatch(1);
@ -2043,7 +2045,6 @@ public final class ExoPlayerTest {
assertThat(target80.positionMs).isAtLeast(target50.positionMs);
}
@Ignore // Temporarily disabled because the AutoAdvancingFakeClock picks up the wrong thread.
@Test
public void sendMessagesFromStartPositionOnlyOnce() throws Exception {
AtomicInteger counter = new AtomicInteger();
@ -2820,6 +2821,7 @@ public final class ExoPlayerTest {
// seek in the timeline which still has two windows in EPI, but when the seek
// arrives in EPII the actual timeline has one window only. Hence it tries to
// find the subsequent period of the removed period and finds it.
player.getClock().onThreadBlocked();
sourceReleasedCountDownLatch.await();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
@ -2961,7 +2963,6 @@ public final class ExoPlayerTest {
assertThat(sequence).containsExactly(0, 1, 2).inOrder();
}
@Ignore // Temporarily disabled because the AutoAdvancingFakeClock picks up the wrong thread.
@Test
public void recursiveTimelineChangeInStopAreReportedInCorrectOrder() throws Exception {
Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 2);
@ -4243,7 +4244,7 @@ public final class ExoPlayerTest {
createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000));
assertThat(windowIndex[0]).isEqualTo(0);
assertThat(positionMs[0]).isGreaterThan(8000);
assertThat(positionMs[0]).isEqualTo(8000);
assertThat(bufferedPositions[0]).isEqualTo(10_000);
assertThat(totalBufferedDuration[0]).isEqualTo(10_000 - positionMs[0]);
@ -4470,7 +4471,7 @@ public final class ExoPlayerTest {
assertThat(windowIndex[2]).isEqualTo(0);
assertThat(isPlayingAd[2]).isFalse();
assertThat(positionMs[2]).isGreaterThan(8000);
assertThat(positionMs[2]).isEqualTo(8000);
assertThat(bufferedPositionMs[2]).isEqualTo(contentDurationMs);
assertThat(totalBufferedDurationMs[2]).isAtLeast(contentDurationMs - positionMs[2]);
}
@ -4592,68 +4593,32 @@ public final class ExoPlayerTest {
runUntilPlaybackState(player, Player.STATE_ENDED);
}
@Ignore // Temporarily disabled because the AutoAdvancingFakeClock picks up the wrong thread.
@Test
public void becomingNoisyIgnoredIfBecomingNoisyHandlingIsDisabled() throws Exception {
CountDownLatch becomingNoisyHandlingDisabled = new CountDownLatch(1);
CountDownLatch becomingNoisyDelivered = new CountDownLatch(1);
PlayerStateGrabber playerStateGrabber = new PlayerStateGrabber();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
player.setHandleAudioBecomingNoisy(false);
becomingNoisyHandlingDisabled.countDown();
SimpleExoPlayer player = new TestExoPlayerBuilder(context).build();
player.play();
// Wait for the broadcast to be delivered from the main thread.
try {
becomingNoisyDelivered.await();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
})
.delay(1) // Handle pending messages on the playback thread.
.executeRunnable(playerStateGrabber)
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context).setActionSchedule(actionSchedule).build().start();
becomingNoisyHandlingDisabled.await();
player.setHandleAudioBecomingNoisy(false);
deliverBroadcast(new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
becomingNoisyDelivered.countDown();
TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player);
boolean playWhenReadyAfterBroadcast = player.getPlayWhenReady();
player.release();
testRunner.blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);
assertThat(playerStateGrabber.playWhenReady).isTrue();
assertThat(playWhenReadyAfterBroadcast).isTrue();
}
@Test
public void pausesWhenBecomingNoisyIfBecomingNoisyHandlingIsEnabled() throws Exception {
CountDownLatch becomingNoisyHandlingEnabled = new CountDownLatch(1);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
player.setHandleAudioBecomingNoisy(true);
becomingNoisyHandlingEnabled.countDown();
}
})
.waitForPlayWhenReady(false) // Becoming noisy should set playWhenReady = false
.play()
.build();
SimpleExoPlayer player = new TestExoPlayerBuilder(context).build();
player.play();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context).setActionSchedule(actionSchedule).build().start();
becomingNoisyHandlingEnabled.await();
player.setHandleAudioBecomingNoisy(true);
deliverBroadcast(new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player);
boolean playWhenReadyAfterBroadcast = player.getPlayWhenReady();
player.release();
// If the player fails to handle becoming noisy, blockUntilActionScheduleFinished will time out
// and throw, causing the test to fail.
testRunner.blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);
assertThat(playWhenReadyAfterBroadcast).isFalse();
}
@Test
@ -7003,7 +6968,7 @@ public final class ExoPlayerTest {
}, // buffers after set items with seek
maskingPlaybackStates);
assertArrayEquals(new int[] {2, 0, 0, 1, 1, 0, 0, 0, 0}, currentWindowIndices);
assertThat(currentPositions[0]).isGreaterThan(0);
assertThat(currentPositions[0]).isEqualTo(0);
assertThat(currentPositions[1]).isEqualTo(0);
assertThat(currentPositions[2]).isEqualTo(0);
assertThat(bufferedPositions[0]).isGreaterThan(0);
@ -8891,7 +8856,7 @@ public final class ExoPlayerTest {
player.setMediaSource(new FakeMediaSource(new FakeTimeline(), formatWithStaticMetadata));
player.seekTo(2_000);
player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2.0f));
ShadowLooper.runMainLooperToNextTask();
TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player);
verify(listener).onTimelineChanged(any(), anyInt());
verify(listener).onMediaItemTransition(any(), anyInt());
@ -8914,7 +8879,7 @@ public final class ExoPlayerTest {
}
});
player.setRepeatMode(Player.REPEAT_MODE_ONE);
ShadowLooper.runMainLooperToNextTask();
TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player);
verify(listener).onRepeatModeChanged(anyInt());
verify(listener).onShuffleModeEnabledChanged(anyBoolean());
@ -8930,7 +8895,7 @@ public final class ExoPlayerTest {
player.play();
player.setMediaItem(MediaItem.fromUri("http://this-will-throw-an-exception.mp4"));
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_IDLE);
ShadowLooper.runMainLooperToNextTask();
TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player);
player.release();
// Verify that all callbacks have been called at least once.

View File

@ -40,6 +40,7 @@ import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.shadows.ShadowLooper;
/** Tests for {@link MetadataRetriever}. */
@RunWith(AndroidJUnit4.class)
@ -63,6 +64,7 @@ public class MetadataRetrieverTest {
ListenableFuture<TrackGroupArray> trackGroupsFuture =
retrieveMetadata(context, mediaItem, clock);
ShadowLooper.idleMainLooper();
TrackGroupArray trackGroups = trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
assertThat(trackGroups.length).isEqualTo(2);
@ -85,6 +87,7 @@ public class MetadataRetrieverTest {
retrieveMetadata(context, mediaItem1, clock);
ListenableFuture<TrackGroupArray> trackGroupsFuture2 =
retrieveMetadata(context, mediaItem2, clock);
ShadowLooper.idleMainLooper();
TrackGroupArray trackGroups1 = trackGroupsFuture1.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
TrackGroupArray trackGroups2 = trackGroupsFuture2.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
@ -118,6 +121,7 @@ public class MetadataRetrieverTest {
ListenableFuture<TrackGroupArray> trackGroupsFuture =
retrieveMetadata(context, mediaItem, clock);
ShadowLooper.idleMainLooper();
TrackGroupArray trackGroups = trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
assertThat(trackGroups.length).isEqualTo(1);
@ -134,6 +138,7 @@ public class MetadataRetrieverTest {
ListenableFuture<TrackGroupArray> trackGroupsFuture =
retrieveMetadata(context, mediaItem, clock);
ShadowLooper.idleMainLooper();
TrackGroupArray trackGroups = trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
assertThat(trackGroups.length).isEqualTo(1);
@ -164,6 +169,7 @@ public class MetadataRetrieverTest {
ListenableFuture<TrackGroupArray> trackGroupsFuture =
retrieveMetadata(context, mediaItem, clock);
ShadowLooper.idleMainLooper();
TrackGroupArray trackGroups = trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
assertThat(trackGroups.length).isEqualTo(2); // Video and audio
@ -185,6 +191,7 @@ public class MetadataRetrieverTest {
ListenableFuture<TrackGroupArray> trackGroupsFuture =
retrieveMetadata(context, mediaItem, clock);
ShadowLooper.idleMainLooper();
assertThrows(
ExecutionException.class, () -> trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS));

View File

@ -215,7 +215,7 @@ public final class AnalyticsCollectorTest {
period0 /* ENDED */)
.inOrder();
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */)
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */)
.inOrder();
assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED))
.containsExactly(period0 /* started */, period0 /* stopped */)
@ -656,9 +656,9 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(
WINDOW_0 /* PLAYLIST_CHANGE */,
WINDOW_0 /* SOURCE_UPDATE */,
period0Seq0 /* SOURCE_UPDATE */,
WINDOW_0 /* PLAYLIST_CHANGE */,
WINDOW_0 /* SOURCE_UPDATE */);
period0Seq1 /* SOURCE_UPDATE */);
assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED))
.containsExactly(period0Seq0, period0Seq0, period0Seq1, period0Seq1)
.inOrder();
@ -748,7 +748,7 @@ public final class AnalyticsCollectorTest {
period0Seq0 /* ENDED */)
.inOrder();
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* prepared */);
.containsExactly(WINDOW_0 /* prepared */, period0Seq0 /* prepared */);
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period0Seq0);
assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0Seq0);
assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0Seq0);
@ -929,7 +929,7 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(
WINDOW_0 /* PLAYLIST_CHANGED */,
WINDOW_0 /* SOURCE_UPDATE (first item) */,
period0Seq0 /* SOURCE_UPDATE (first item) */,
period0Seq0 /* PLAYLIST_CHANGED (add) */,
period0Seq0 /* SOURCE_UPDATE (second item) */,
period0Seq1 /* PLAYLIST_CHANGED (remove) */)
@ -949,7 +949,7 @@ public final class AnalyticsCollectorTest {
.containsExactly(period0Seq0, period1Seq1)
.inOrder();
assertThat(listener.getEvents(EVENT_DECODER_ENABLED))
.containsExactly(period0Seq0, period1Seq1, period0Seq1)
.containsExactly(period0Seq0, period0Seq1)
.inOrder();
assertThat(listener.getEvents(EVENT_DECODER_INIT))
.containsExactly(period0Seq0, period1Seq1)
@ -957,10 +957,9 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED))
.containsExactly(period0Seq0, period1Seq1)
.inOrder();
assertThat(listener.getEvents(EVENT_DECODER_DISABLED))
.containsExactly(period0Seq0, period0Seq0);
assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0Seq0);
assertThat(listener.getEvents(EVENT_VIDEO_ENABLED))
.containsExactly(period0Seq0, period1Seq1, period0Seq1)
.containsExactly(period0Seq0, period0Seq1)
.inOrder();
assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INITIALIZED))
.containsExactly(period0Seq0, period1Seq1)
@ -968,13 +967,13 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED))
.containsExactly(period0Seq0, period1Seq1)
.inOrder();
assertThat(listener.getEvents(EVENT_VIDEO_DISABLED)).containsExactly(period0Seq0, period0Seq0);
assertThat(listener.getEvents(EVENT_VIDEO_DISABLED)).containsExactly(period0Seq0);
assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0Seq1);
assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED))
.containsExactly(period0Seq0, period0Seq1)
.containsExactly(period0Seq0, period1Seq1, period0Seq1)
.inOrder();
assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME))
.containsExactly(period0Seq0, period0Seq1);
.containsExactly(period0Seq0, period1Seq1, period0Seq1);
assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET))
.containsExactly(period0Seq1);
listener.assertNoMoreEvents();
@ -1132,7 +1131,7 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(
WINDOW_0 /* PLAYLIST_CHANGED */,
WINDOW_0 /* SOURCE_UPDATE (initial) */,
prerollAd /* SOURCE_UPDATE (initial) */,
contentAfterPreroll /* SOURCE_UPDATE (played preroll) */,
contentAfterMidroll /* SOURCE_UPDATE (played midroll) */,
contentAfterPostroll /* SOURCE_UPDATE (played postroll) */)
@ -1327,7 +1326,7 @@ public final class AnalyticsCollectorTest {
contentAfterMidroll /* ENDED */)
.inOrder();
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */);
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, contentBeforeMidroll /* SOURCE_UPDATE */);
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY))
.containsExactly(
contentAfterMidroll /* seek */,

View File

@ -15,13 +15,13 @@
*/
package com.google.android.exoplayer2.analytics;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.robolectric.shadows.ShadowLooper.runMainLooperToNextTask;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
@ -42,6 +42,7 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.robolectric.shadows.ShadowLooper;
/** Unit test for {@link PlaybackStatsListener}. */
@RunWith(AndroidJUnit4.class)
@ -60,41 +61,41 @@ public final class PlaybackStatsListenerTest {
}
@Test
public void events_duringInitialIdleState_dontCreateNewPlaybackStats() {
public void events_duringInitialIdleState_dontCreateNewPlaybackStats() throws Exception {
PlaybackStatsListener playbackStatsListener =
new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null);
player.addAnalyticsListener(playbackStatsListener);
player.seekTo(/* positionMs= */ 1234);
runMainLooperToNextTask();
runUntilPendingCommandsAreFullyHandled(player);
player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f));
runMainLooperToNextTask();
runUntilPendingCommandsAreFullyHandled(player);
player.play();
runMainLooperToNextTask();
runUntilPendingCommandsAreFullyHandled(player);
assertThat(playbackStatsListener.getPlaybackStats()).isNull();
}
@Test
public void stateChangeEvent_toNonIdle_createsInitialPlaybackStats() {
public void stateChangeEvent_toNonIdle_createsInitialPlaybackStats() throws Exception {
PlaybackStatsListener playbackStatsListener =
new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null);
player.addAnalyticsListener(playbackStatsListener);
player.prepare();
runMainLooperToNextTask();
runUntilPendingCommandsAreFullyHandled(player);
assertThat(playbackStatsListener.getPlaybackStats()).isNotNull();
}
@Test
public void timelineChangeEvent_toNonEmpty_createsInitialPlaybackStats() {
public void timelineChangeEvent_toNonEmpty_createsInitialPlaybackStats() throws Exception {
PlaybackStatsListener playbackStatsListener =
new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null);
player.addAnalyticsListener(playbackStatsListener);
player.setMediaItem(MediaItem.fromUri("http://test.org"));
runMainLooperToNextTask();
runUntilPendingCommandsAreFullyHandled(player);
assertThat(playbackStatsListener.getPlaybackStats()).isNotNull();
}
@ -109,7 +110,7 @@ public final class PlaybackStatsListenerTest {
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
runMainLooperToNextTask();
runUntilPendingCommandsAreFullyHandled(player);
@Nullable PlaybackStats playbackStats = playbackStatsListener.getPlaybackStats();
assertThat(playbackStats).isNotNull();
@ -126,7 +127,7 @@ public final class PlaybackStatsListenerTest {
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
runMainLooperToNextTask();
runUntilPendingCommandsAreFullyHandled(player);
@Nullable PlaybackStats playbackStats = playbackStatsListener.getPlaybackStats();
assertThat(playbackStats).isNotNull();
@ -134,7 +135,7 @@ public final class PlaybackStatsListenerTest {
}
@Test
public void finishedSession_callsCallback() {
public void finishedSession_callsCallback() throws Exception {
PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class);
PlaybackStatsListener playbackStatsListener =
new PlaybackStatsListener(/* keepHistory= */ true, callback);
@ -143,10 +144,10 @@ public final class PlaybackStatsListenerTest {
// Create session with some events and finish it by removing it from the playlist.
player.setMediaSource(new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)));
player.prepare();
runMainLooperToNextTask();
runUntilPendingCommandsAreFullyHandled(player);
verify(callback, never()).onPlaybackStatsReady(any(), any());
player.clearMediaItems();
runMainLooperToNextTask();
runUntilPendingCommandsAreFullyHandled(player);
verify(callback).onPlaybackStatsReady(any(), any());
}
@ -166,9 +167,9 @@ public final class PlaybackStatsListenerTest {
// the first one isn't finished yet.
TestPlayerRunHelper.playUntilPosition(
player, /* windowIndex= */ 0, /* positionMs= */ player.getDuration());
runMainLooperToNextTask();
runUntilPendingCommandsAreFullyHandled(player);
player.release();
runMainLooperToNextTask();
ShadowLooper.idleMainLooper();
ArgumentCaptor<AnalyticsListener.EventTime> eventTimeCaptor =
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);

View File

@ -143,9 +143,6 @@ public final class TransformerTest {
TransformerTestRunner.runUntilCompleted(transformer);
Files.delete(Paths.get(outputPath));
// Transformer.startTransformation() will create a new SimpleExoPlayer instance. Reset the
// clock's handler so that the clock advances with the new SimpleExoPlayer instance.
clock.resetHandler();
// Transform second media item.
transformer.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer);
@ -236,9 +233,7 @@ public final class TransformerTest {
transformer.startTransformation(mediaItem, outputPath);
transformer.cancel();
Files.delete(Paths.get(outputPath));
// Transformer.startTransformation() will create a new SimpleExoPlayer instance. Reset the
// clock's handler so that the clock advances with the new SimpleExoPlayer instance.
clock.resetHandler();
// This would throw if the previous transformation had not been cancelled.
transformer.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer);

View File

@ -313,6 +313,7 @@ public class TestPlayerRunHelper {
blockPlaybackThreadCondition.open();
});
try {
player.getClock().onThreadBlocked();
blockPlaybackThreadCondition.block();
} catch (InterruptedException e) {
// Ignore.

View File

@ -701,6 +701,7 @@ public abstract class Action {
blockPlaybackThreadCondition.open();
});
try {
player.getClock().onThreadBlocked();
blockPlaybackThreadCondition.block();
} catch (InterruptedException e) {
// Ignore.

View File

@ -15,23 +15,12 @@
*/
package com.google.android.exoplayer2.testutil;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.HandlerWrapper;
/**
* {@link FakeClock} extension which automatically advances time whenever an empty message is
* enqueued at a future time.
*
* <p>The clock time is advanced to the time of enqueued empty messages. The first Handler sending
* messages at a future time will be allowed to advance time to ensure there is only one primary
* time source at a time. This should usually be the Handler of the internal playback loop. You can
* {@link #resetHandler() reset the handler} so that the next Handler that sends messages at a
* future time becomes the primary time source.
*/
public final class AutoAdvancingFakeClock extends FakeClock {
@Nullable private HandlerWrapper autoAdvancingHandler;
/** Creates the auto-advancing clock with an initial time of 0. */
public AutoAdvancingFakeClock() {
this(/* initialTimeMs= */ 0);
@ -43,24 +32,6 @@ public final class AutoAdvancingFakeClock extends FakeClock {
* @param initialTimeMs The initial time of the clock in milliseconds.
*/
public AutoAdvancingFakeClock(long initialTimeMs) {
super(initialTimeMs);
}
@Override
protected synchronized void addPendingHandlerMessage(HandlerMessage message) {
super.addPendingHandlerMessage(message);
HandlerWrapper handler = message.getTarget();
long currentTimeMs = elapsedRealtime();
long messageTimeMs = message.getTimeMs();
if (currentTimeMs < messageTimeMs
&& (autoAdvancingHandler == null || autoAdvancingHandler == handler)) {
autoAdvancingHandler = handler;
advanceTime(messageTimeMs - currentTimeMs);
}
}
/** Resets the internal handler, so that this clock can later be used with another handler. */
public void resetHandler() {
autoAdvancingHandler = null;
super(initialTimeMs, /* isAutoAdvancing= */ true);
}
}

View File

@ -351,6 +351,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
@Nullable private final Player.EventListener eventListener;
@Nullable private final AnalyticsListener analyticsListener;
private final Clock clock;
private final HandlerThread playerThread;
private final HandlerWrapper handler;
private final CountDownLatch endedCountDownLatch;
@ -388,6 +389,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
this.actionSchedule = actionSchedule;
this.eventListener = eventListener;
this.analyticsListener = analyticsListener;
this.clock = playerBuilder.getClock();
timelines = new ArrayList<>();
timelineChangeReasons = new ArrayList<>();
mediaItems = new ArrayList<>();
@ -399,8 +401,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
actionScheduleFinishedCountDownLatch = new CountDownLatch(actionSchedule != null ? 1 : 0);
playerThread = new HandlerThread("ExoPlayerTest thread");
playerThread.start();
handler =
playerBuilder.getClock().createHandler(playerThread.getLooper(), /* callback= */ null);
handler = clock.createHandler(playerThread.getLooper(), /* callback= */ null);
this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems;
}
@ -476,6 +477,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
* @throws Exception If any exception occurred during playback, release, or due to a timeout.
*/
public ExoPlayerTestRunner blockUntilEnded(long timeoutMs) throws Exception {
clock.onThreadBlocked();
if (!endedCountDownLatch.await(timeoutMs, MILLISECONDS)) {
exception = new TimeoutException("Test playback timed out waiting for playback to end.");
}
@ -498,6 +500,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
*/
public ExoPlayerTestRunner blockUntilActionScheduleFinished(long timeoutMs)
throws TimeoutException, InterruptedException {
clock.onThreadBlocked();
if (!actionScheduleFinishedCountDownLatch.await(timeoutMs, MILLISECONDS)) {
throw new TimeoutException("Test playback timed out waiting for action schedule to finish.");
}
@ -619,6 +622,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
playerThread.quit();
}
});
clock.onThreadBlocked();
playerThread.join();
}

View File

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.testutil;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.Looper;
@ -23,38 +25,69 @@ import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.common.collect.ComparisonChain;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Fake {@link Clock} implementation that allows to {@link #advanceTime(long) advance the time}
* manually to trigger pending timed messages.
*
* <p>All timed messages sent by a {@link #createHandler(Looper, Callback) Handler} created from
* this clock are governed by the clock's time.
* this clock are governed by the clock's time. Messages sent through these handlers are not
* triggered until previous messages on any thread have been handled to ensure deterministic
* execution. Note that this includes messages sent from the main Robolectric test thread, meaning
* that these messages are only triggered if the main test thread is idle, which can be explicitly
* requested by calling {@code ShadowLooper.idleMainLooper()}.
*
* <p>The clock also sets the time of the {@link SystemClock} to match the {@link #elapsedRealtime()
* clock's time}.
*/
public class FakeClock implements Clock {
private static long messageIdProvider = 0;
private final boolean isAutoAdvancing;
@GuardedBy("this")
private final List<HandlerMessage> handlerMessages;
@GuardedBy("this")
private final Set<Looper> busyLoopers;
@GuardedBy("this")
private final long bootTimeMs;
@GuardedBy("this")
private long timeSinceBootMs;
@GuardedBy("this")
private boolean waitingForMessage;
/**
* Creates a fake clock assuming the system was booted exactly at time {@code 0} (the Unix Epoch)
* and {@code initialTimeMs} milliseconds have passed since system boot.
* Creates a fake clock that doesn't auto-advance and assumes that the system was booted exactly
* at time {@code 0} (the Unix Epoch) and {@code initialTimeMs} milliseconds have passed since
* system boot.
*
* @param initialTimeMs The initial elapsed time since the boot time, in milliseconds.
*/
public FakeClock(long initialTimeMs) {
this(/* bootTimeMs= */ 0, initialTimeMs);
this(/* bootTimeMs= */ 0, initialTimeMs, /* isAutoAdvancing= */ false);
}
/**
* Creates a fake clock that assumes that the system was booted exactly at time {@code 0} (the
* Unix Epoch) and {@code initialTimeMs} milliseconds have passed since system boot.
*
* @param initialTimeMs The initial elapsed time since the boot time, in milliseconds.
* @param isAutoAdvancing Whether the clock should automatically advance the time to the time of
* next message that is due to be sent.
*/
public FakeClock(long initialTimeMs, boolean isAutoAdvancing) {
this(/* bootTimeMs= */ 0, initialTimeMs, isAutoAdvancing);
}
/**
@ -63,11 +96,15 @@ public class FakeClock implements Clock {
*
* @param bootTimeMs The time the system was booted since the Unix Epoch, in milliseconds.
* @param initialTimeMs The initial elapsed time since the boot time, in milliseconds.
* @param isAutoAdvancing Whether the clock should automatically advance the time to the time of
* next message that is due to be sent.
*/
public FakeClock(long bootTimeMs, long initialTimeMs) {
public FakeClock(long bootTimeMs, long initialTimeMs, boolean isAutoAdvancing) {
this.bootTimeMs = bootTimeMs;
this.timeSinceBootMs = initialTimeMs;
this.isAutoAdvancing = isAutoAdvancing;
this.handlerMessages = new ArrayList<>();
this.busyLoopers = new HashSet<>();
SystemClock.setCurrentTimeMillis(initialTimeMs);
}
@ -77,9 +114,8 @@ public class FakeClock implements Clock {
* @param timeDiffMs The amount of time to add to the timestamp in milliseconds.
*/
public synchronized void advanceTime(long timeDiffMs) {
timeSinceBootMs += timeDiffMs;
SystemClock.setCurrentTimeMillis(timeSinceBootMs);
maybeTriggerMessages();
advanceTimeInternal(timeDiffMs);
maybeTriggerMessage();
}
@Override
@ -102,10 +138,22 @@ public class FakeClock implements Clock {
return new ClockHandler(looper, callback);
}
@Override
public synchronized void onThreadBlocked() {
busyLoopers.add(checkNotNull(Looper.myLooper()));
waitingForMessage = false;
maybeTriggerMessage();
}
/** Adds a message to the list of pending messages. */
protected synchronized void addPendingHandlerMessage(HandlerMessage message) {
handlerMessages.add(message);
maybeTriggerMessages();
if (!waitingForMessage) {
// If this isn't executed from inside a message created by this class, make sure the current
// looper message is finished before handling the new message.
waitingForMessage = true;
new Handler(checkNotNull(Looper.myLooper())).post(this::onMessageHandled);
}
}
private synchronized void removePendingHandlerMessages(ClockHandler handler, int what) {
@ -139,27 +187,65 @@ public class FakeClock implements Clock {
return handler.handler.hasMessages(what);
}
private synchronized void maybeTriggerMessages() {
for (int i = handlerMessages.size() - 1; i >= 0; i--) {
HandlerMessage message = handlerMessages.get(i);
if (message.timeMs <= timeSinceBootMs) {
if (message.runnable != null) {
message.handler.handler.post(message.runnable);
} else {
message
.handler
.handler
.obtainMessage(message.what, message.arg1, message.arg2, message.obj)
.sendToTarget();
}
handlerMessages.remove(i);
private synchronized void maybeTriggerMessage() {
if (waitingForMessage) {
return;
}
if (handlerMessages.isEmpty()) {
return;
}
Collections.sort(handlerMessages);
int messageIndex = 0;
HandlerMessage message = handlerMessages.get(messageIndex);
int messageCount = handlerMessages.size();
while (busyLoopers.contains(message.handler.getLooper()) && messageIndex < messageCount) {
messageIndex++;
if (messageIndex == messageCount) {
return;
}
message = handlerMessages.get(messageIndex);
}
if (message.timeMs > timeSinceBootMs) {
if (isAutoAdvancing) {
advanceTimeInternal(message.timeMs - timeSinceBootMs);
} else {
return;
}
}
handlerMessages.remove(messageIndex);
waitingForMessage = true;
if (message.runnable != null) {
message.handler.handler.post(message.runnable);
} else {
message
.handler
.handler
.obtainMessage(message.what, message.arg1, message.arg2, message.obj)
.sendToTarget();
}
message.handler.internalHandler.post(this::onMessageHandled);
}
private synchronized void onMessageHandled() {
busyLoopers.remove(Looper.myLooper());
waitingForMessage = false;
maybeTriggerMessage();
}
private synchronized void advanceTimeInternal(long timeDiffMs) {
timeSinceBootMs += timeDiffMs;
SystemClock.setCurrentTimeMillis(timeSinceBootMs);
}
private static synchronized long getNextMessageId() {
return messageIdProvider++;
}
/** Message data saved to send messages or execute runnables at a later time on a Handler. */
protected final class HandlerMessage implements HandlerWrapper.Message {
protected final class HandlerMessage
implements Comparable<HandlerMessage>, HandlerWrapper.Message {
private final long messageId;
private final long timeMs;
private final ClockHandler handler;
@Nullable private final Runnable runnable;
@ -176,6 +262,7 @@ public class FakeClock implements Clock {
int arg2,
@Nullable Object obj,
@Nullable Runnable runnable) {
this.messageId = getNextMessageId();
this.timeMs = timeMs;
this.handler = handler;
this.runnable = runnable;
@ -199,15 +286,25 @@ public class FakeClock implements Clock {
public HandlerWrapper getTarget() {
return handler;
}
@Override
public int compareTo(HandlerMessage other) {
return ComparisonChain.start()
.compare(this.timeMs, other.timeMs)
.compare(this.messageId, other.messageId)
.result();
}
}
/** HandlerWrapper implementation using the enclosing Clock to schedule delayed messages. */
private final class ClockHandler implements HandlerWrapper {
public final Handler handler;
public final Handler internalHandler;
public ClockHandler(Looper looper, @Nullable Callback callback) {
handler = new Handler(looper, callback);
internalHandler = new Handler(looper);
}
@Override
@ -311,3 +408,5 @@ public class FakeClock implements Clock {
}
}

View File

@ -30,9 +30,9 @@ import com.google.common.base.Objects;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.List;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.shadows.ShadowLooper;
/** Unit test for {@link FakeClock}. */
@RunWith(AndroidJUnit4.class)
@ -46,13 +46,16 @@ public final class FakeClockTest {
@Test
public void currentTimeMillis_withBootTime() {
FakeClock fakeClock = new FakeClock(/* bootTimeMs= */ 150, /* initialTimeMs= */ 200);
FakeClock fakeClock =
new FakeClock(
/* bootTimeMs= */ 150, /* initialTimeMs= */ 200, /* isAutoAdvancing= */ false);
assertThat(fakeClock.currentTimeMillis()).isEqualTo(350);
}
@Test
public void currentTimeMillis_afterAdvanceTime_currentTimeHasAdvanced() {
FakeClock fakeClock = new FakeClock(/* bootTimeMs= */ 100, /* initialTimeMs= */ 50);
FakeClock fakeClock =
new FakeClock(/* bootTimeMs= */ 100, /* initialTimeMs= */ 50, /* isAutoAdvancing= */ false);
fakeClock.advanceTime(/* timeDiffMs */ 250);
assertThat(fakeClock.currentTimeMillis()).isEqualTo(400);
}
@ -82,6 +85,7 @@ public final class FakeClockTest {
handler
.obtainMessage(/* what= */ 4, /* arg1= */ 88, /* arg2= */ 33, /* obj=*/ testObject)
.sendToTarget();
ShadowLooper.idleMainLooper();
shadowOf(handler.getLooper()).idle();
assertThat(callback.messages)
@ -105,6 +109,7 @@ public final class FakeClockTest {
handler.sendEmptyMessageAtTime(/* what= */ 2, /* uptimeMs= */ fakeClock.uptimeMillis() + 60);
handler.sendEmptyMessageDelayed(/* what= */ 3, /* delayMs= */ 50);
handler.sendEmptyMessage(/* what= */ 4);
ShadowLooper.idleMainLooper();
shadowOf(handler.getLooper()).idle();
assertThat(callback.messages)
@ -128,8 +133,6 @@ public final class FakeClockTest {
.isEqualTo(new MessageData(/* what= */ 2, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null));
}
// Temporarily disabled until messages are ordered correctly.
@Ignore
@Test
public void createHandler_sendMessageAtFrontOfQueue_sendsMessageFirst() {
HandlerThread handlerThread = new HandlerThread("FakeClockTest");
@ -141,6 +144,7 @@ public final class FakeClockTest {
handler.obtainMessage(/* what= */ 1).sendToTarget();
handler.sendMessageAtFrontOfQueue(handler.obtainMessage(/* what= */ 2));
handler.obtainMessage(/* what= */ 3).sendToTarget();
ShadowLooper.idleMainLooper();
shadowOf(handler.getLooper()).idle();
assertThat(callback.messages)
@ -169,12 +173,14 @@ public final class FakeClockTest {
handler.postDelayed(testRunnables[0], 0);
handler.postDelayed(testRunnables[1], 100);
handler.postDelayed(testRunnables[2], 200);
ShadowLooper.idleMainLooper();
shadowOf(handler.getLooper()).idle();
assertTestRunnableStates(new boolean[] {true, false, false, false, false}, testRunnables);
fakeClock.advanceTime(150);
handler.postDelayed(testRunnables[3], 50);
handler.postDelayed(testRunnables[4], 100);
ShadowLooper.idleMainLooper();
shadowOf(handler.getLooper()).idle();
assertTestRunnableStates(new boolean[] {true, true, false, false, false}, testRunnables);
@ -197,9 +203,6 @@ public final class FakeClockTest {
TestCallback otherCallback = new TestCallback();
HandlerWrapper otherHandler = fakeClock.createHandler(handlerThread.getLooper(), otherCallback);
// Block any further execution on the HandlerThread until we had a chance to remove messages.
ConditionVariable startCondition = new ConditionVariable();
handler.post(startCondition::block);
TestRunnable testRunnable1 = new TestRunnable();
TestRunnable testRunnable2 = new TestRunnable();
Object messageToken = new Object();
@ -213,8 +216,8 @@ public final class FakeClockTest {
handler.removeMessages(/* what= */ 2);
handler.removeCallbacksAndMessages(messageToken);
startCondition.open();
fakeClock.advanceTime(50);
ShadowLooper.idleMainLooper();
shadowOf(handlerThread.getLooper()).idle();
assertThat(callback.messages)
@ -239,9 +242,6 @@ public final class FakeClockTest {
TestCallback otherCallback = new TestCallback();
HandlerWrapper otherHandler = fakeClock.createHandler(handlerThread.getLooper(), otherCallback);
// Block any further execution on the HandlerThread until we had a chance to remove messages.
ConditionVariable startCondition = new ConditionVariable();
handler.post(startCondition::block);
TestRunnable testRunnable1 = new TestRunnable();
TestRunnable testRunnable2 = new TestRunnable();
Object messageToken = new Object();
@ -254,8 +254,8 @@ public final class FakeClockTest {
handler.removeCallbacksAndMessages(/* token= */ null);
startCondition.open();
fakeClock.advanceTime(50);
ShadowLooper.idleMainLooper();
shadowOf(handlerThread.getLooper()).idle();
assertThat(callback.messages).isEmpty();
@ -268,6 +268,109 @@ public final class FakeClockTest {
new MessageData(/* what= */ 1, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null));
}
@Test
public void createHandler_withIsAutoAdvancing_advancesTimeToNextMessages() {
HandlerThread handlerThread = new HandlerThread("FakeClockTest");
handlerThread.start();
FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 0, /* isAutoAdvancing= */ true);
HandlerWrapper handler =
fakeClock.createHandler(handlerThread.getLooper(), /* callback= */ null);
// Post a series of immediate and delayed messages.
ArrayList<Long> clockTimes = new ArrayList<>();
handler.post(
() -> {
handler.postDelayed(
() -> clockTimes.add(fakeClock.elapsedRealtime()), /* delayMs= */ 100);
handler.postDelayed(() -> clockTimes.add(fakeClock.elapsedRealtime()), /* delayMs= */ 50);
handler.post(() -> clockTimes.add(fakeClock.elapsedRealtime()));
handler.postDelayed(
() -> {
clockTimes.add(fakeClock.elapsedRealtime());
handler.postDelayed(
() -> clockTimes.add(fakeClock.elapsedRealtime()), /* delayMs= */ 50);
},
/* delayMs= */ 20);
});
ShadowLooper.idleMainLooper();
shadowOf(handler.getLooper()).idle();
assertThat(clockTimes).containsExactly(0L, 20L, 50L, 70L, 100L).inOrder();
}
@Test
public void createHandler_multiThreadCommunication_deliversMessagesDeterministicallyInOrder() {
HandlerThread handlerThread1 = new HandlerThread("FakeClockTest");
handlerThread1.start();
HandlerThread handlerThread2 = new HandlerThread("FakeClockTest");
handlerThread2.start();
FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 0);
HandlerWrapper handler1 =
fakeClock.createHandler(handlerThread1.getLooper(), /* callback= */ null);
HandlerWrapper handler2 =
fakeClock.createHandler(handlerThread2.getLooper(), /* callback= */ null);
ConditionVariable messagesFinished = new ConditionVariable();
ArrayList<Integer> executionOrder = new ArrayList<>();
handler1.post(
() -> {
executionOrder.add(1);
handler2.post(() -> executionOrder.add(2));
handler1.post(() -> executionOrder.add(3));
handler2.post(
() -> {
executionOrder.add(4);
handler2.post(() -> executionOrder.add(7));
handler1.post(
() -> {
executionOrder.add(8);
messagesFinished.open();
});
});
handler2.post(() -> executionOrder.add(5));
handler1.post(() -> executionOrder.add(6));
});
ShadowLooper.idleMainLooper();
messagesFinished.block();
assertThat(executionOrder).containsExactly(1, 2, 3, 4, 5, 6, 7, 8).inOrder();
}
@Test
public void createHandler_blockingThreadWithOnBusyWaiting_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);
blockingCondition.open();
},
/* delayMs= */ 50);
handler1.post(() -> executionOrder.add(4));
fakeClock.onThreadBlocked();
blockingCondition.block();
executionOrder.add(3);
});
ShadowLooper.idleMainLooper();
shadowOf(handler1.getLooper()).idle();
shadowOf(handler2.getLooper()).idle();
assertThat(executionOrder).containsExactly(1, 2, 3, 4).inOrder();
}
private static void assertTestRunnableStates(boolean[] states, TestRunnable[] testRunnables) {
for (int i = 0; i < testRunnables.length; i++) {
assertThat(testRunnables[i].hasRun).isEqualTo(states[i]);