Move runUntil method to TestUtil as it's used by multiple tests.

We started using this method from other tests unrelated to
TestExoPlayer, so the method is better placed inside a generic Util
class.

PiperOrigin-RevId: 316675067
This commit is contained in:
tonihei 2020-06-16 15:14:45 +01:00 committed by Andrew Lewis
parent b7233c28e9
commit cc97bcb469
5 changed files with 153 additions and 160 deletions

View File

@ -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();

View File

@ -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<Boolean> 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<Boolean> 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();
}
}
}

View File

@ -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}.
*
* <p>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<Boolean> 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<Boolean> 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);
}
}
}

View File

@ -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<Boolean> 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();
}
}

View File

@ -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<Boolean> 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();
}
}