From 2e52c0b8d85ef5e4fdbdf89da441d5ff01de9209 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 26 Jan 2021 17:02:47 +0000 Subject: [PATCH] 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 --- .../google/android/exoplayer2/util/Clock.java | 8 + .../android/exoplayer2/util/SystemClock.java | 5 + .../exoplayer2/ExoPlayerImplInternal.java | 1 + .../android/exoplayer2/PlayerMessage.java | 1 + .../android/exoplayer2/ExoPlayerTest.java | 95 ++++------- .../exoplayer2/MetadataRetrieverTest.java | 7 + .../analytics/AnalyticsCollectorTest.java | 27 ++-- .../analytics/PlaybackStatsListenerTest.java | 33 ++-- .../transformer/TransformerTest.java | 7 +- .../robolectric/TestPlayerRunHelper.java | 1 + .../android/exoplayer2/testutil/Action.java | 1 + .../testutil/AutoAdvancingFakeClock.java | 31 +--- .../testutil/ExoPlayerTestRunner.java | 8 +- .../exoplayer2/testutil/FakeClock.java | 147 +++++++++++++++--- .../exoplayer2/testutil/FakeClockTest.java | 129 +++++++++++++-- 15 files changed, 331 insertions(+), 170 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Clock.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Clock.java index f6b98a1c66..8ecb2ab8ec 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/Clock.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Clock.java @@ -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. + * + *

Should be a no-op for all non-test cases. + */ + void onThreadBlocked(); } diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/SystemClock.java b/library/common/src/main/java/com/google/android/exoplayer2/util/SystemClock.java index e315d8bf25..c3b31aa5c9 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/SystemClock.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/SystemClock.java @@ -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. + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index a97a38e8ef..5a2c783a6f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -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; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java index 36f562f7cb..4191480700 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -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(); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 60e96308e3..d163e704a5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -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. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MetadataRetrieverTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MetadataRetrieverTest.java index 53f6c24f10..154ec0df1b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MetadataRetrieverTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MetadataRetrieverTest.java @@ -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 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 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 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 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 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 trackGroupsFuture = retrieveMetadata(context, mediaItem, clock); + ShadowLooper.idleMainLooper(); assertThrows( ExecutionException.class, () -> trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS)); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index ad807c4079..bc7d149007 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -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 */, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java index c736444a43..bd5dfb97a5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java @@ -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 eventTimeCaptor = ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java index 12f799fc53..d3ef423217 100644 --- a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java @@ -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); diff --git a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TestPlayerRunHelper.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TestPlayerRunHelper.java index fe67af3d93..b813a93d2a 100644 --- a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TestPlayerRunHelper.java +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TestPlayerRunHelper.java @@ -313,6 +313,7 @@ public class TestPlayerRunHelper { blockPlaybackThreadCondition.open(); }); try { + player.getClock().onThreadBlocked(); blockPlaybackThreadCondition.block(); } catch (InterruptedException e) { // Ignore. diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index fb0ee74bae..003c3eb3ba 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -701,6 +701,7 @@ public abstract class Action { blockPlaybackThreadCondition.open(); }); try { + player.getClock().onThreadBlocked(); blockPlaybackThreadCondition.block(); } catch (InterruptedException e) { // Ignore. diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/AutoAdvancingFakeClock.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/AutoAdvancingFakeClock.java index d9a789ea18..86b9bb39f3 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/AutoAdvancingFakeClock.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/AutoAdvancingFakeClock.java @@ -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. - * - *

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); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index b9ee9c9f8d..8232cba48b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -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(); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java index 4dea57f087..4dd32f6cc5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java @@ -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. * *

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()}. * *

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 handlerMessages; + @GuardedBy("this") + private final Set 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, 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 { } } + + diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java index c6b39d7f27..28e57d8e66 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java @@ -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 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 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 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]);