mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
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:
parent
3ac4c1a6e5
commit
9213ffafa8
5 changed files with 220 additions and 6 deletions
|
|
@ -16,13 +16,39 @@
|
||||||
package com.google.android.exoplayer2.util;
|
package com.google.android.exoplayer2.util;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An interruptible condition variable whose {@link #open()} and {@link #close()} methods return
|
* An interruptible condition variable. This class provides a number of benefits over {@link
|
||||||
* whether they resulted in a change of state.
|
* 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 {
|
public final class ConditionVariable {
|
||||||
|
|
||||||
|
private final Clock clock;
|
||||||
private boolean isOpen;
|
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.
|
* 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.
|
* @throws InterruptedException If the thread is interrupted.
|
||||||
*/
|
*/
|
||||||
public synchronized boolean block(long timeout) throws InterruptedException {
|
public synchronized boolean block(long timeout) throws InterruptedException {
|
||||||
long now = android.os.SystemClock.elapsedRealtime();
|
long now = clock.elapsedRealtime();
|
||||||
long end = now + timeout;
|
long end = now + timeout;
|
||||||
while (!isOpen && now < end) {
|
while (!isOpen && now < end) {
|
||||||
wait(end - now);
|
wait(end - now);
|
||||||
now = android.os.SystemClock.elapsedRealtime();
|
now = clock.elapsedRealtime();
|
||||||
}
|
}
|
||||||
return isOpen;
|
return isOpen;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,12 @@ import android.os.Looper;
|
||||||
import androidx.annotation.Nullable;
|
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
|
@Override
|
||||||
public long currentTimeMillis() {
|
public long currentTimeMillis() {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,9 @@ import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
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 com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
@ -441,4 +444,22 @@ public class TestUtil {
|
||||||
}
|
}
|
||||||
return new DefaultExtractorInput(dataSource, position, length);
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue