ConditionVariable: Improve documentation and allow clock injection

- Improve documentation explaining the benefits of ExoPlayer's ConditionVariable
  over the one that the platform provides
- Allow Clock injection
- Create TestUtil method for obtaining a ConditionVariable whose block(long)
  method times out correctly when used in a Robolectric test
- Add basic unit tests for ConditionVariable

PiperOrigin-RevId: 308812698
This commit is contained in:
olly 2020-04-28 14:35:49 +01:00 committed by Oliver Woodman
parent 3ac4c1a6e5
commit 9213ffafa8
5 changed files with 220 additions and 6 deletions

View file

@ -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}:
*
* <ul>
* <li>Consistent use of ({@link Clock#elapsedRealtime()} for timing {@link #block(long)} timeout
* intervals. {@link android.os.ConditionVariable} used {@link System#currentTimeMillis()}
* prior to Android 10, which is not a correct clock to use for interval timing because it's
* not guaranteed to be monotonic.
* <li>Support for injecting a custom {@link Clock}.
* <li>The ability to query the variable's current state, by calling {@link #isOpen()}.
* <li>{@link #open()} and {@link #close()} return whether they changed the variable's state.
* </ul>
*/
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;
}

View file

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

View file

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

View file

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

View file

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