diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriodTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriodTest.java index 0478f77367..420fcb6fcc 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriodTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriodTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import static com.google.android.exoplayer2.testutil.TestUtil.runMainLooperUntil; import static com.google.common.truth.Truth.assertThat; import android.net.Uri; @@ -24,7 +25,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; -import com.google.android.exoplayer2.testutil.TestExoPlayer; import com.google.android.exoplayer2.upstream.AssetDataSource; import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; @@ -72,7 +72,7 @@ public final class ProgressiveMediaPeriodTest { } }, /* positionUs= */ 0); - TestExoPlayer.runUntil(prepareCallbackCalled::get); + runMainLooperUntil(prepareCallbackCalled::get); mediaPeriod.release(); assertThat(sourceInfoRefreshCalledBeforeOnPrepared.get()).isTrue(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java index a8672b703f..139088aeb6 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.testutil; +import static com.google.android.exoplayer2.testutil.TestUtil.runMainLooperUntil; import static com.google.common.truth.Truth.assertThat; import android.content.Context; @@ -36,11 +37,8 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; -import com.google.android.exoplayer2.util.Supplier; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoListener; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -52,30 +50,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; */ public class TestExoPlayer { - /** - * The default timeout applied when calling one of the {@code runUntil} methods. This timeout - * should be sufficient for any condition using a Robolectric test. - */ - public static final long DEFAULT_TIMEOUT_MS = 10_000; - - /** Reflectively call Robolectric ShadowLooper#runOneTask. */ - private static final Object shadowLooper; - - private static final Method runOneTaskMethod; - - static { - try { - Class clazz = Class.forName("org.robolectric.Shadows"); - Method shadowOfMethod = - Assertions.checkNotNull(clazz.getDeclaredMethod("shadowOf", Looper.class)); - shadowLooper = - Assertions.checkNotNull(shadowOfMethod.invoke(new Object(), Looper.getMainLooper())); - runOneTaskMethod = shadowLooper.getClass().getDeclaredMethod("runOneTask"); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - /** A builder of {@link SimpleExoPlayer} instances for testing. */ public static class Builder { @@ -317,7 +291,8 @@ public class TestExoPlayer { * * @param player The {@link Player}. * @param expectedState The expected {@link Player.State}. - * @throws TimeoutException If the {@link #DEFAULT_TIMEOUT_MS default timeout} is exceeded. + * @throws TimeoutException If the {@link TestUtil#DEFAULT_TIMEOUT_MS default timeout} is + * exceeded. */ public static void runUntilPlaybackState(Player player, @Player.State int expectedState) throws TimeoutException { @@ -336,7 +311,7 @@ public class TestExoPlayer { } }; player.addListener(listener); - runUntil(receivedExpectedState::get); + runMainLooperUntil(receivedExpectedState::get); player.removeListener(listener); } @@ -346,7 +321,8 @@ public class TestExoPlayer { * * @param player The {@link Player}. * @param expectedPlayWhenReady The expected value for {@link Player#getPlayWhenReady()}. - * @throws TimeoutException If the {@link #DEFAULT_TIMEOUT_MS default timeout} is exceeded. + * @throws TimeoutException If the {@link TestUtil#DEFAULT_TIMEOUT_MS default timeout} is + * exceeded. */ public static void runUntilPlayWhenReady(Player player, boolean expectedPlayWhenReady) throws TimeoutException { @@ -366,7 +342,7 @@ public class TestExoPlayer { } }; player.addListener(listener); - runUntil(receivedExpectedPlayWhenReady::get); + runMainLooperUntil(receivedExpectedPlayWhenReady::get); } /** @@ -375,7 +351,8 @@ public class TestExoPlayer { * * @param player The {@link Player}. * @param expectedTimeline The expected {@link Timeline}. - * @throws TimeoutException If the {@link #DEFAULT_TIMEOUT_MS default timeout} is exceeded. + * @throws TimeoutException If the {@link TestUtil#DEFAULT_TIMEOUT_MS default timeout} is + * exceeded. */ public static void runUntilTimelineChanged(Player player, Timeline expectedTimeline) throws TimeoutException { @@ -395,7 +372,7 @@ public class TestExoPlayer { } }; player.addListener(listener); - runUntil(receivedExpectedTimeline::get); + runMainLooperUntil(receivedExpectedTimeline::get); } /** @@ -403,7 +380,8 @@ public class TestExoPlayer { * * @param player The {@link Player}. * @return The new {@link Timeline}. - * @throws TimeoutException If the {@link #DEFAULT_TIMEOUT_MS default timeout} is exceeded. + * @throws TimeoutException If the {@link TestUtil#DEFAULT_TIMEOUT_MS default timeout} is + * exceeded. */ public static Timeline runUntilTimelineChanged(Player player) throws TimeoutException { verifyMainTestThread(player); @@ -417,7 +395,7 @@ public class TestExoPlayer { } }; player.addListener(listener); - runUntil(() -> receivedTimeline.get() != null); + runMainLooperUntil(() -> receivedTimeline.get() != null); return receivedTimeline.get(); } @@ -428,7 +406,8 @@ public class TestExoPlayer { * * @param player The {@link Player}. * @param expectedReason The expected {@link Player.DiscontinuityReason}. - * @throws TimeoutException If the {@link #DEFAULT_TIMEOUT_MS default timeout} is exceeded. + * @throws TimeoutException If the {@link TestUtil#DEFAULT_TIMEOUT_MS default timeout} is + * exceeded. */ public static void runUntilPositionDiscontinuity( Player player, @Player.DiscontinuityReason int expectedReason) throws TimeoutException { @@ -444,7 +423,7 @@ public class TestExoPlayer { } }; player.addListener(listener); - runUntil(receivedCallback::get); + runMainLooperUntil(receivedCallback::get); } /** @@ -452,7 +431,8 @@ public class TestExoPlayer { * * @param player The {@link Player}. * @return The raised {@link ExoPlaybackException}. - * @throws TimeoutException If the {@link #DEFAULT_TIMEOUT_MS default timeout} is exceeded. + * @throws TimeoutException If the {@link TestUtil#DEFAULT_TIMEOUT_MS default timeout} is + * exceeded. */ public static ExoPlaybackException runUntilError(Player player) throws TimeoutException { verifyMainTestThread(player); @@ -466,7 +446,7 @@ public class TestExoPlayer { } }; player.addListener(listener); - runUntil(() -> receivedError.get() != null); + runMainLooperUntil(() -> receivedError.get() != null); return receivedError.get(); } @@ -475,7 +455,8 @@ public class TestExoPlayer { * callback has been called. * * @param player The {@link Player}. - * @throws TimeoutException If the {@link #DEFAULT_TIMEOUT_MS default timeout} is exceeded. + * @throws TimeoutException If the {@link TestUtil#DEFAULT_TIMEOUT_MS default timeout} is + * exceeded. */ public static void runUntilRenderedFirstFrame(SimpleExoPlayer player) throws TimeoutException { verifyMainTestThread(player); @@ -489,7 +470,7 @@ public class TestExoPlayer { } }; player.addVideoListener(listener); - runUntil(receivedCallback::get); + runMainLooperUntil(receivedCallback::get); } /** @@ -497,7 +478,8 @@ public class TestExoPlayer { * commands on the internal playback thread. * * @param player The {@link Player}. - * @throws TimeoutException If the {@link #DEFAULT_TIMEOUT_MS default timeout} is exceeded. + * @throws TimeoutException If the {@link TestUtil#DEFAULT_TIMEOUT_MS default timeout} is + * exceeded. */ public static void runUntilPendingCommandsAreFullyHandled(ExoPlayer player) throws TimeoutException { @@ -510,41 +492,7 @@ public class TestExoPlayer { .createMessage((type, data) -> receivedMessageCallback.set(true)) .setHandler(Util.createHandler()) .send(); - runUntil(receivedMessageCallback::get); - } - - /** - * Runs tasks of the main {@link Looper} until the {@code condition} returns {@code true}. - * - * @param condition The condition. - * @throws TimeoutException If the {@link #DEFAULT_TIMEOUT_MS} is exceeded. - */ - public static void runUntil(Supplier condition) throws TimeoutException { - runUntil(condition, DEFAULT_TIMEOUT_MS, Clock.DEFAULT); - } - - /** - * Runs tasks of the main {@link Looper} until the {@code condition} returns {@code true}. - * - * @param condition The condition. - * @param timeoutMs The timeout in milliseconds. - * @param clock The {@link Clock} to measure the timeout. - * @throws TimeoutException If the {@code timeoutMs timeout} is exceeded. - */ - public static void runUntil(Supplier condition, long timeoutMs, Clock clock) - throws TimeoutException { - verifyMainTestThread(); - try { - long timeoutTimeMs = clock.currentTimeMillis() + timeoutMs; - while (!condition.get()) { - if (clock.currentTimeMillis() >= timeoutTimeMs) { - throw new TimeoutException(); - } - runOneTaskMethod.invoke(shadowLooper); - } - } catch (IllegalAccessException | InvocationTargetException e) { - throw new IllegalStateException(e); - } + runMainLooperUntil(receivedMessageCallback::get); } private static void verifyMainTestThread(Player player) { @@ -553,10 +501,4 @@ public class TestExoPlayer { throw new IllegalStateException(); } } - - private static void verifyMainTestThread() { - if (Looper.myLooper() != Looper.getMainLooper()) { - throw new IllegalStateException(); - } - } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index 0aac047e44..1a53d300d7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -26,6 +26,7 @@ import android.graphics.BitmapFactory; import android.graphics.Color; import android.media.MediaCodec; import android.net.Uri; +import android.os.Looper; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.database.DefaultDatabaseProvider; @@ -40,21 +41,38 @@ import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.ConditionVariable; +import com.google.android.exoplayer2.util.Supplier; import com.google.android.exoplayer2.util.SystemClock; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Random; +import java.util.concurrent.TimeoutException; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Utility methods for tests. */ public class TestUtil { + /** + * The default timeout applied when calling {@link #runMainLooperUntil(Supplier)}. This timeout + * should be sufficient for any condition using a Robolectric test. + */ + public static final long DEFAULT_TIMEOUT_MS = 10_000; + + /** Reflectively loaded Robolectric ShadowLooper#runOneTask. */ + private static @MonotonicNonNull Object shadowLooper; + + private static @MonotonicNonNull Method runOneTaskMethod; + private TestUtil() {} /** @@ -484,4 +502,64 @@ public class TestUtil { } }); } + + /** + * Runs tasks of the main Robolectric {@link Looper} until the {@code condition} returns {@code + * true}. + * + *

Must be called on the main test thread. + * + * @param condition The condition. + * @throws TimeoutException If the {@link #DEFAULT_TIMEOUT_MS} is exceeded. + */ + public static void runMainLooperUntil(Supplier condition) throws TimeoutException { + runMainLooperUntil(condition, DEFAULT_TIMEOUT_MS, Clock.DEFAULT); + } + + /** + * Runs tasks of the main Robolectric {@link Looper} until the {@code condition} returns {@code + * true}. + * + * @param condition The condition. + * @param timeoutMs The timeout in milliseconds. + * @param clock The {@link Clock} to measure the timeout. + * @throws TimeoutException If the {@code timeoutMs timeout} is exceeded. + */ + public static void runMainLooperUntil(Supplier condition, long timeoutMs, Clock clock) + throws TimeoutException { + if (Looper.myLooper() != Looper.getMainLooper()) { + throw new IllegalStateException(); + } + maybeInitShadowLooperAndRunOneTaskMethod(); + try { + long timeoutTimeMs = clock.currentTimeMillis() + timeoutMs; + while (!condition.get()) { + if (clock.currentTimeMillis() >= timeoutTimeMs) { + throw new TimeoutException(); + } + runOneTaskMethod.invoke(shadowLooper); + } + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } catch (InvocationTargetException e) { + throw new IllegalStateException(e.getCause()); + } + } + + @EnsuresNonNull({"shadowLooper", "runOneTaskMethod"}) + private static void maybeInitShadowLooperAndRunOneTaskMethod() { + if (shadowLooper != null && runOneTaskMethod != null) { + return; + } + try { + Class clazz = Class.forName("org.robolectric.Shadows"); + Method shadowOfMethod = + Assertions.checkNotNull(clazz.getDeclaredMethod("shadowOf", Looper.class)); + shadowLooper = + Assertions.checkNotNull(shadowOfMethod.invoke(new Object(), Looper.getMainLooper())); + runOneTaskMethod = shadowLooper.getClass().getDeclaredMethod("runOneTask"); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } } diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/TestExoPlayerTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/TestExoPlayerTest.java deleted file mode 100644 index 3e18222562..0000000000 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/TestExoPlayerTest.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.testutil; - -import static org.junit.Assert.assertThrows; -import static org.mockito.Mockito.atMost; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.util.Clock; -import com.google.android.exoplayer2.util.Supplier; -import java.util.concurrent.TimeoutException; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.annotation.LooperMode; - -/** Unit test for {@link TestExoPlayer}. */ -@RunWith(AndroidJUnit4.class) -@LooperMode(LooperMode.Mode.PAUSED) -public final class TestExoPlayerTest { - - @Test - public void runUntil_withConditionAlreadyTrue_returnsImmediately() throws Exception { - Clock mockClock = mock(Clock.class); - - TestExoPlayer.runUntil(() -> true, /* timeoutMs= */ 0, mockClock); - - verify(mockClock, atMost(1)).currentTimeMillis(); - } - - @Test - public void runUntil_withConditionThatNeverBecomesTrue_timesOut() { - Clock mockClock = mock(Clock.class); - when(mockClock.currentTimeMillis()).thenReturn(0L, 41L, 42L); - - assertThrows( - TimeoutException.class, - () -> TestExoPlayer.runUntil(() -> false, /* timeoutMs= */ 42, mockClock)); - - verify(mockClock, times(3)).currentTimeMillis(); - } - - @SuppressWarnings("unchecked") - @Test - public void runUntil_whenConditionBecomesTrueAfterDelay_returnsWhenConditionBecomesTrue() - throws Exception { - Supplier mockCondition = mock(Supplier.class); - when(mockCondition.get()) - .thenReturn(false) - .thenReturn(false) - .thenReturn(false) - .thenReturn(false) - .thenReturn(true); - - TestExoPlayer.runUntil(mockCondition, /* timeoutMs= */ 5674, mock(Clock.class)); - - verify(mockCondition, times(5)).get(); - } -} diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/TestUtilTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/TestUtilTest.java index 0a999c4161..01a119b694 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/TestUtilTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/TestUtilTest.java @@ -16,9 +16,18 @@ package com.google.android.exoplayer2.testutil; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.atMost; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.ConditionVariable; +import com.google.android.exoplayer2.util.Supplier; +import java.util.concurrent.TimeoutException; import org.junit.Test; import org.junit.runner.RunWith; @@ -43,4 +52,43 @@ public class TestUtilTest { long endTimeMs = System.currentTimeMillis(); assertThat(endTimeMs - startTimeMs).isAtLeast(500); } + + @Test + public void runMainLooperUntil_withConditionAlreadyTrue_returnsImmediately() throws Exception { + Clock mockClock = mock(Clock.class); + + TestUtil.runMainLooperUntil(() -> true, /* timeoutMs= */ 0, mockClock); + + verify(mockClock, atMost(1)).currentTimeMillis(); + } + + @Test + public void runMainLooperUntil_withConditionThatNeverBecomesTrue_timesOut() { + Clock mockClock = mock(Clock.class); + when(mockClock.currentTimeMillis()).thenReturn(0L, 41L, 42L); + + assertThrows( + TimeoutException.class, + () -> TestUtil.runMainLooperUntil(() -> false, /* timeoutMs= */ 42, mockClock)); + + verify(mockClock, times(3)).currentTimeMillis(); + } + + @SuppressWarnings("unchecked") + @Test + public void + runMainLooperUntil_whenConditionBecomesTrueAfterDelay_returnsWhenConditionBecomesTrue() + throws Exception { + Supplier mockCondition = mock(Supplier.class); + when(mockCondition.get()) + .thenReturn(false) + .thenReturn(false) + .thenReturn(false) + .thenReturn(false) + .thenReturn(true); + + TestUtil.runMainLooperUntil(mockCondition, /* timeoutMs= */ 5674, mock(Clock.class)); + + verify(mockCondition, times(5)).get(); + } }