diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java index c035c62a7e..1b5cd47401 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java @@ -16,13 +16,39 @@ package com.google.android.exoplayer2.util; /** - * An interruptible condition variable whose {@link #open()} and {@link #close()} methods return - * whether they resulted in a change of state. + * An interruptible condition variable. This class provides a number of benefits over {@link + * android.os.ConditionVariable}: + * + * */ public final class ConditionVariable { + private final Clock clock; private boolean isOpen; + /** Creates an instance using {@link Clock#DEFAULT}. */ + public ConditionVariable() { + this(Clock.DEFAULT); + } + + /** + * Creates an instance. + * + * @param clock The {@link Clock} whose {@link Clock#elapsedRealtime()} method is used to + * determine when {@link #block(long)} should time out. + */ + public ConditionVariable(Clock clock) { + this.clock = clock; + } + /** * Opens the condition and releases all threads that are blocked. * @@ -67,11 +93,11 @@ public final class ConditionVariable { * @throws InterruptedException If the thread is interrupted. */ public synchronized boolean block(long timeout) throws InterruptedException { - long now = android.os.SystemClock.elapsedRealtime(); + long now = clock.elapsedRealtime(); long end = now + timeout; while (!isOpen && now < end) { wait(end - now); - now = android.os.SystemClock.elapsedRealtime(); + now = clock.elapsedRealtime(); } return isOpen; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java index a094e810bf..89e1c60d7a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java @@ -21,9 +21,12 @@ import android.os.Looper; import androidx.annotation.Nullable; /** - * The standard implementation of {@link Clock}. + * The standard implementation of {@link Clock}, an instance of which is available via {@link + * SystemClock#DEFAULT}. */ -/* package */ final class SystemClock implements Clock { +public class SystemClock implements Clock { + + protected SystemClock() {} @Override public long currentTimeMillis() { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ConditionVariableTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ConditionVariableTest.java new file mode 100644 index 0000000000..1e47aa680d --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ConditionVariableTest.java @@ -0,0 +1,118 @@ +/* + * 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.util; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link ConditionVariableTest}. */ +@RunWith(AndroidJUnit4.class) +public class ConditionVariableTest { + + @Test + public void initialState_isClosed() { + ConditionVariable conditionVariable = buildTestConditionVariable(); + assertThat(conditionVariable.isOpen()).isFalse(); + } + + @Test + public void blockWithTimeout_timesOut() throws InterruptedException { + ConditionVariable conditionVariable = buildTestConditionVariable(); + assertThat(conditionVariable.block(1)).isFalse(); + assertThat(conditionVariable.isOpen()).isFalse(); + } + + @Test + public void blockWithTimeout_blocksForAtLeastTimeout() throws InterruptedException { + ConditionVariable conditionVariable = buildTestConditionVariable(); + long startTimeMs = System.currentTimeMillis(); + assertThat(conditionVariable.block(/* timeout= */ 500)).isFalse(); + long endTimeMs = System.currentTimeMillis(); + assertThat(endTimeMs - startTimeMs).isAtLeast(500L); + } + + @Test + public void blockWithoutTimeout_blocks() throws InterruptedException { + ConditionVariable conditionVariable = buildTestConditionVariable(); + + AtomicBoolean blockReturned = new AtomicBoolean(); + AtomicBoolean blockWasInterrupted = new AtomicBoolean(); + Thread blockingThread = + new Thread( + () -> { + try { + conditionVariable.block(); + blockReturned.set(true); + } catch (InterruptedException e) { + blockWasInterrupted.set(true); + } + }); + + blockingThread.start(); + Thread.sleep(500); + assertThat(blockReturned.get()).isFalse(); + + blockingThread.interrupt(); + blockingThread.join(); + assertThat(blockWasInterrupted.get()).isTrue(); + assertThat(conditionVariable.isOpen()).isFalse(); + } + + @Test + public void open_unblocksBlock() throws InterruptedException { + ConditionVariable conditionVariable = buildTestConditionVariable(); + + AtomicBoolean blockReturned = new AtomicBoolean(); + AtomicBoolean blockWasInterrupted = new AtomicBoolean(); + Thread blockingThread = + new Thread( + () -> { + try { + conditionVariable.block(); + blockReturned.set(true); + } catch (InterruptedException e) { + blockWasInterrupted.set(true); + } + }); + + blockingThread.start(); + Thread.sleep(500); + assertThat(blockReturned.get()).isFalse(); + + conditionVariable.open(); + blockingThread.join(); + assertThat(blockReturned.get()).isTrue(); + assertThat(conditionVariable.isOpen()).isTrue(); + } + + private static ConditionVariable buildTestConditionVariable() { + return new ConditionVariable( + new SystemClock() { + @Override + public long elapsedRealtime() { + // elapsedRealtime() does not advance during Robolectric test execution, so use + // currentTimeMillis() instead. This is technically unsafe because this clock is not + // guaranteed to be monotonic, but in practice it will work provided the clock of the + // host machine does not change during test execution. + return Clock.DEFAULT.currentTimeMillis(); + } + }); + } +} 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 c47b438100..b0beb1ba13 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 @@ -36,6 +36,9 @@ import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.upstream.DataSource; 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.SystemClock; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.io.InputStream; @@ -441,4 +444,22 @@ public class TestUtil { } return new DefaultExtractorInput(dataSource, position, length); } + + /** + * Creates a {@link ConditionVariable} whose {@link ConditionVariable#block(long)} method times + * out according to wallclock time when used in Robolectric tests. + */ + public static ConditionVariable createRobolectricConditionVariable() { + return new ConditionVariable( + new SystemClock() { + @Override + public long elapsedRealtime() { + // elapsedRealtime() does not advance during Robolectric test execution, so use + // currentTimeMillis() instead. This is technically unsafe because this clock is not + // guaranteed to be monotonic, but in practice it will work provided the clock of the + // host machine does not change during test execution. + return Clock.DEFAULT.currentTimeMillis(); + } + }); + } } 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 new file mode 100644 index 0000000000..a80d474f9b --- /dev/null +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/TestUtilTest.java @@ -0,0 +1,46 @@ +/* + * 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 com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.util.ConditionVariable; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link TestUtil}. */ +@RunWith(AndroidJUnit4.class) +public class TestUtilTest { + + @Test + public void createRobolectricConditionVariable_blockWithTimeout_timesOut() + throws InterruptedException { + ConditionVariable conditionVariable = TestUtil.createRobolectricConditionVariable(); + assertThat(conditionVariable.block(/* timeout= */ 1)).isFalse(); + assertThat(conditionVariable.isOpen()).isFalse(); + } + + @Test + public void createRobolectricConditionVariable_blockWithTimeout_blocksForAtLeastTimeout() + throws InterruptedException { + ConditionVariable conditionVariable = TestUtil.createRobolectricConditionVariable(); + long startTimeMs = System.currentTimeMillis(); + assertThat(conditionVariable.block(/* timeout= */ 500)).isFalse(); + long endTimeMs = System.currentTimeMillis(); + assertThat(endTimeMs - startTimeMs).isAtLeast(500); + } +}