Commit 37d9e2f4 by olly Committed by Oliver Woodman

DownloadManagerTest: Improve thread interactions

- Remove assertReleased and replace it with a proper condition variable
  that's opened when Downloader.download or Download.remove finish. As
  far as I can tell assertReleased was basically implementing "sleep for
  10 seconds after the Downloader starts". Note fixing this properly
  also makes the tests run much faster!
- Use ConditionVariable instead of CountDownLatch(1).
- Use AtomicInteger instead of volatile int because it's clearer and
  allows removal of explanatory comments.

PiperOrigin-RevId: 308819204
parent 5c216977
...@@ -28,14 +28,14 @@ import com.google.android.exoplayer2.testutil.DummyMainThread; ...@@ -28,14 +28,14 @@ import com.google.android.exoplayer2.testutil.DummyMainThread;
import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable; import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable;
import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; import com.google.android.exoplayer2.testutil.TestDownloadManagerListener;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.ConditionVariable;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.TimeUnit;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
...@@ -54,10 +54,6 @@ public class DownloadManagerTest { ...@@ -54,10 +54,6 @@ public class DownloadManagerTest {
private static final int ASSERT_TRUE_TIMEOUT = 10000; private static final int ASSERT_TRUE_TIMEOUT = 10000;
/** Used to check if condition stays false for this time interval. */ /** Used to check if condition stays false for this time interval. */
private static final int ASSERT_FALSE_TIME = 1000; private static final int ASSERT_FALSE_TIME = 1000;
/** Maximum retry delay in DownloadManager. */
private static final int MAX_RETRY_DELAY = 5000;
/** Maximum number of times a downloader can be restarted before doing a released check. */
private static final int MAX_STARTS_BEFORE_RELEASED = 1;
/** A stop reason. */ /** A stop reason. */
private static final int APP_STOP_REASON = 1; private static final int APP_STOP_REASON = 1;
/** The minimum number of times a task must be retried before failing. */ /** The minimum number of times a task must be retried before failing. */
...@@ -110,7 +106,7 @@ public class DownloadManagerTest { ...@@ -110,7 +106,7 @@ public class DownloadManagerTest {
public void postDownloadRequest_downloads() throws Throwable { public void postDownloadRequest_downloads() throws Throwable {
DownloadRunner runner = new DownloadRunner(uri1).postDownloadRequest(); DownloadRunner runner = new DownloadRunner(uri1).postDownloadRequest();
runner.assertDownloading(); runner.assertDownloading();
runner.getDownloader(0).unblock().assertReleased().assertStartCount(1); runner.getDownloader(0).unblock().assertCompleted().assertStartCount(1);
runner.assertCompleted(); runner.assertCompleted();
runner.assertCreatedDownloaderCount(1); runner.assertCreatedDownloaderCount(1);
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
...@@ -121,7 +117,7 @@ public class DownloadManagerTest { ...@@ -121,7 +117,7 @@ public class DownloadManagerTest {
public void postRemoveRequest_removes() throws Throwable { public void postRemoveRequest_removes() throws Throwable {
DownloadRunner runner = new DownloadRunner(uri1).postDownloadRequest().postRemoveRequest(); DownloadRunner runner = new DownloadRunner(uri1).postDownloadRequest().postRemoveRequest();
runner.assertRemoving(); runner.assertRemoving();
runner.getDownloader(1).unblock().assertReleased().assertStartCount(1); runner.getDownloader(1).unblock().assertCompleted().assertStartCount(1);
runner.assertRemoved(); runner.assertRemoved();
runner.assertCreatedDownloaderCount(2); runner.assertCreatedDownloaderCount(2);
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
...@@ -135,10 +131,10 @@ public class DownloadManagerTest { ...@@ -135,10 +131,10 @@ public class DownloadManagerTest {
FakeDownloader downloader = runner.getDownloader(0); FakeDownloader downloader = runner.getDownloader(0);
for (int i = 0; i <= MIN_RETRY_COUNT; i++) { for (int i = 0; i <= MIN_RETRY_COUNT; i++) {
downloader.assertStarted(MAX_RETRY_DELAY).fail(); downloader.assertStarted().fail();
} }
downloader.assertReleased().assertStartCount(MIN_RETRY_COUNT + 1); downloader.assertCompleted().assertStartCount(MIN_RETRY_COUNT + 1);
runner.assertFailed(); runner.assertFailed();
downloadManagerListener.blockUntilTasksComplete(); downloadManagerListener.blockUntilTasksComplete();
assertThat(downloadManager.getCurrentDownloads()).isEmpty(); assertThat(downloadManager.getCurrentDownloads()).isEmpty();
...@@ -151,11 +147,11 @@ public class DownloadManagerTest { ...@@ -151,11 +147,11 @@ public class DownloadManagerTest {
FakeDownloader downloader = runner.getDownloader(0); FakeDownloader downloader = runner.getDownloader(0);
for (int i = 0; i < MIN_RETRY_COUNT; i++) { for (int i = 0; i < MIN_RETRY_COUNT; i++) {
downloader.assertStarted(MAX_RETRY_DELAY).fail(); downloader.assertStarted().fail();
} }
downloader.assertStarted(MAX_RETRY_DELAY).unblock(); downloader.assertStarted().unblock();
downloader.assertReleased().assertStartCount(MIN_RETRY_COUNT + 1); downloader.assertCompleted().assertStartCount(MIN_RETRY_COUNT + 1);
runner.assertCompleted(); runner.assertCompleted();
downloadManagerListener.blockUntilTasksComplete(); downloadManagerListener.blockUntilTasksComplete();
assertThat(downloadManager.getCurrentDownloads()).isEmpty(); assertThat(downloadManager.getCurrentDownloads()).isEmpty();
...@@ -170,11 +166,11 @@ public class DownloadManagerTest { ...@@ -170,11 +166,11 @@ public class DownloadManagerTest {
int tooManyRetries = MIN_RETRY_COUNT + 10; int tooManyRetries = MIN_RETRY_COUNT + 10;
for (int i = 0; i < tooManyRetries; i++) { for (int i = 0; i < tooManyRetries; i++) {
downloader.incrementBytesDownloaded(); downloader.incrementBytesDownloaded();
downloader.assertStarted(MAX_RETRY_DELAY).fail(); downloader.assertStarted().fail();
} }
downloader.assertStarted(MAX_RETRY_DELAY).unblock(); downloader.assertStarted().unblock();
downloader.assertReleased().assertStartCount(tooManyRetries + 1); downloader.assertCompleted().assertStartCount(tooManyRetries + 1);
runner.assertCompleted(); runner.assertCompleted();
downloadManagerListener.blockUntilTasksComplete(); downloadManagerListener.blockUntilTasksComplete();
} }
...@@ -189,7 +185,7 @@ public class DownloadManagerTest { ...@@ -189,7 +185,7 @@ public class DownloadManagerTest {
runner.postRemoveRequest(); runner.postRemoveRequest();
downloader1.assertCanceled().assertStartCount(1); downloader1.assertCanceled().assertStartCount(1);
runner.getDownloader(1).unblock().assertNotCanceled(); runner.getDownloader(1).unblock().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
...@@ -202,8 +198,8 @@ public class DownloadManagerTest { ...@@ -202,8 +198,8 @@ public class DownloadManagerTest {
downloader1.assertStarted(); downloader1.assertStarted();
runner.postDownloadRequest(); runner.postDownloadRequest();
downloader1.unblock().assertNotCanceled(); downloader1.unblock().assertCompleted();
runner.getDownloader(2).unblock().assertNotCanceled(); runner.getDownloader(2).unblock().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
...@@ -216,7 +212,7 @@ public class DownloadManagerTest { ...@@ -216,7 +212,7 @@ public class DownloadManagerTest {
downloader1.assertStarted(); downloader1.assertStarted();
runner.postRemoveRequest(); runner.postRemoveRequest();
downloader1.unblock().assertNotCanceled(); downloader1.unblock().assertCompleted();
runner.assertRemoved(); runner.assertRemoved();
runner.assertCreatedDownloaderCount(2); runner.assertCreatedDownloaderCount(2);
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
...@@ -378,7 +374,7 @@ public class DownloadManagerTest { ...@@ -378,7 +374,7 @@ public class DownloadManagerTest {
runner1.assertQueued(); runner1.assertQueued();
// remove requests aren't stopped. // remove requests aren't stopped.
runner2.getDownloader(1).unblock().assertReleased(); runner2.getDownloader(1).unblock().assertCompleted();
runner2.assertQueued(); runner2.assertQueued();
// Although remove2 is finished, download2 doesn't start. // Although remove2 is finished, download2 doesn't start.
runner2.getDownloader(2).assertDoesNotStart(); runner2.getDownloader(2).assertDoesNotStart();
...@@ -447,7 +443,7 @@ public class DownloadManagerTest { ...@@ -447,7 +443,7 @@ public class DownloadManagerTest {
runner1.assertStopped(); runner1.assertStopped();
// Other downloads aren't affected. // Other downloads aren't affected.
runner2.getDownloader(1).unblock().assertReleased(); runner2.getDownloader(1).unblock().assertCompleted();
// New download requests can be added and they start. // New download requests can be added and they start.
runner3.postDownloadRequest().getDownloader(0).assertStarted().unblock(); runner3.postDownloadRequest().getDownloader(0).assertStarted().unblock();
...@@ -540,7 +536,7 @@ public class DownloadManagerTest { ...@@ -540,7 +536,7 @@ public class DownloadManagerTest {
downloadManagerListener = downloadManagerListener =
new TestDownloadManagerListener(downloadManager, dummyMainThread); new TestDownloadManagerListener(downloadManager, dummyMainThread);
}); });
downloadManagerListener.waitUntilInitialized(); downloadManagerListener.blockUntilInitialized();
} catch (Throwable throwable) { } catch (Throwable throwable) {
throw new Exception(throwable); throw new Exception(throwable);
} }
...@@ -696,120 +692,125 @@ public class DownloadManagerTest { ...@@ -696,120 +692,125 @@ public class DownloadManagerTest {
private static final class FakeDownloader implements Downloader { private static final class FakeDownloader implements Downloader {
private final com.google.android.exoplayer2.util.ConditionVariable blocker;
private DownloadRequest request; private DownloadRequest request;
private CountDownLatch started;
private final ConditionVariable started;
private final ConditionVariable finished;
private final ConditionVariable blocker;
private final AtomicInteger startCount;
private final AtomicInteger bytesDownloaded;
private volatile boolean interrupted; private volatile boolean interrupted;
private volatile boolean cancelled; private volatile boolean canceled;
private volatile boolean enableDownloadIOException; private volatile boolean enableDownloadIOException;
private volatile int startCount;
private volatile int bytesDownloaded;
private FakeDownloader() { private FakeDownloader() {
this.started = new CountDownLatch(1); started = TestUtil.createRobolectricConditionVariable();
this.blocker = new com.google.android.exoplayer2.util.ConditionVariable(); finished = TestUtil.createRobolectricConditionVariable();
blocker = TestUtil.createRobolectricConditionVariable();
startCount = new AtomicInteger();
bytesDownloaded = new AtomicInteger();
} }
@SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"})
@Override @Override
public void download(ProgressListener listener) throws InterruptedException, IOException { public void cancel() {
// It's ok to update this directly as no other thread will update it. canceled = true;
startCount++;
started.countDown();
block();
if (bytesDownloaded > 0) {
listener.onProgress(C.LENGTH_UNSET, bytesDownloaded, C.PERCENTAGE_UNSET);
}
if (enableDownloadIOException) {
enableDownloadIOException = false;
throw new IOException();
}
} }
@Override @Override
public void cancel() { public void download(ProgressListener listener) throws InterruptedException, IOException {
cancelled = true; startCount.incrementAndGet();
started.open();
try {
block();
int bytesDownloaded = this.bytesDownloaded.get();
if (listener != null && bytesDownloaded > 0) {
listener.onProgress(C.LENGTH_UNSET, bytesDownloaded, C.PERCENTAGE_UNSET);
}
if (enableDownloadIOException) {
enableDownloadIOException = false;
throw new IOException();
}
} finally {
finished.open();
}
} }
@SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"})
@Override @Override
public void remove() throws InterruptedException { public void remove() throws InterruptedException {
// It's ok to update this directly as no other thread will update it. startCount.incrementAndGet();
startCount++; started.open();
started.countDown();
block();
}
private void block() throws InterruptedException {
try { try {
blocker.block(); block();
} catch (InterruptedException e) {
interrupted = true;
throw e;
} finally { } finally {
blocker.close(); finished.open();
} }
} }
private FakeDownloader assertStarted() throws InterruptedException { /** Unblocks {@link #download} or {@link #remove}, allowing the task to finish successfully. */
return assertStarted(ASSERT_TRUE_TIMEOUT); public FakeDownloader unblock() {
blocker.open();
return this;
} }
private FakeDownloader assertStarted(int timeout) throws InterruptedException { /** Fails {@link #download} or {@link #remove}, allowing the task to finish with an error. */
assertThat(started.await(timeout, TimeUnit.MILLISECONDS)).isTrue(); public FakeDownloader fail() {
started = new CountDownLatch(1); enableDownloadIOException = true;
blocker.open();
return this; return this;
} }
private FakeDownloader assertStartCount(int count) { /** Increments the number of bytes that the fake downloader has downloaded. */
assertThat(startCount).isEqualTo(count); public void incrementBytesDownloaded() {
return this; bytesDownloaded.incrementAndGet();
} }
private FakeDownloader assertReleased() throws InterruptedException { public FakeDownloader assertStarted() throws InterruptedException {
int count = 0; assertThat(started.block(ASSERT_TRUE_TIMEOUT)).isTrue();
while (started.await(ASSERT_TRUE_TIMEOUT, TimeUnit.MILLISECONDS)) { started.close();
if (count++ >= MAX_STARTS_BEFORE_RELEASED) {
fail();
}
started = new CountDownLatch(1);
}
return this; return this;
} }
private FakeDownloader assertCanceled() throws InterruptedException { public FakeDownloader assertStartCount(int count) {
assertReleased(); assertThat(startCount.get()).isEqualTo(count);
assertThat(interrupted).isTrue();
assertThat(cancelled).isTrue();
return this; return this;
} }
private FakeDownloader assertNotCanceled() throws InterruptedException { public FakeDownloader assertCompleted() throws InterruptedException {
assertReleased(); blockUntilFinished();
assertThat(interrupted).isFalse(); assertThat(interrupted).isFalse();
assertThat(cancelled).isFalse(); assertThat(canceled).isFalse();
return this; return this;
} }
private FakeDownloader unblock() { public FakeDownloader assertCanceled() throws InterruptedException {
blocker.open(); blockUntilFinished();
assertThat(interrupted).isTrue();
assertThat(canceled).isTrue();
return this; return this;
} }
private FakeDownloader fail() { public void assertDoesNotStart() throws InterruptedException {
enableDownloadIOException = true; Thread.sleep(ASSERT_FALSE_TIME);
return unblock(); assertThat(started.isOpen()).isFalse();
} }
private void assertDoesNotStart() throws InterruptedException { // Internal methods.
Thread.sleep(ASSERT_FALSE_TIME);
assertThat(started.getCount()).isEqualTo(1); private void block() throws InterruptedException {
try {
blocker.block();
} catch (InterruptedException e) {
interrupted = true;
throw e;
} finally {
blocker.close();
}
} }
@SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"}) private void blockUntilFinished() throws InterruptedException {
private void incrementBytesDownloaded() { assertThat(finished.block(ASSERT_TRUE_TIMEOUT)).isTrue();
bytesDownloaded++; finished.close();
} }
} }
} }
...@@ -264,8 +264,7 @@ public class DownloadManagerDashTest { ...@@ -264,8 +264,7 @@ public class DownloadManagerDashTest {
downloadManager.setRequirements(new Requirements(0)); downloadManager.setRequirements(new Requirements(0));
downloadManagerListener = downloadManagerListener =
new TestDownloadManagerListener( new TestDownloadManagerListener(downloadManager, dummyMainThread);
downloadManager, dummyMainThread, /* timeoutMs= */ 3000);
downloadManager.resumeDownloads(); downloadManager.resumeDownloads();
}); });
} }
......
...@@ -22,41 +22,32 @@ import androidx.annotation.Nullable; ...@@ -22,41 +22,32 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.offline.Download; import com.google.android.exoplayer2.offline.Download;
import com.google.android.exoplayer2.offline.Download.State; import com.google.android.exoplayer2.offline.Download.State;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.ConditionVariable;
import java.util.HashMap; import java.util.HashMap;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** A {@link DownloadManager.Listener} for testing. */ /** A {@link DownloadManager.Listener} for testing. */
public final class TestDownloadManagerListener implements DownloadManager.Listener { public final class TestDownloadManagerListener implements DownloadManager.Listener {
private static final int TIMEOUT_MS = 1000; private static final int TIMEOUT_MS = 10_000;
private static final int INITIALIZATION_TIMEOUT_MS = 10_000;
private static final int STATE_REMOVED = -1; private static final int STATE_REMOVED = -1;
private final DownloadManager downloadManager; private final DownloadManager downloadManager;
private final DummyMainThread dummyMainThread; private final DummyMainThread dummyMainThread;
private final HashMap<String, ArrayBlockingQueue<Integer>> downloadStates; private final HashMap<String, ArrayBlockingQueue<Integer>> downloadStates;
private final CountDownLatch initializedCondition; private final ConditionVariable initializedCondition;
private final int timeoutMs; private final ConditionVariable idleCondition;
private @MonotonicNonNull CountDownLatch downloadFinishedCondition;
@Download.FailureReason private int failureReason; @Download.FailureReason private int failureReason;
public TestDownloadManagerListener( public TestDownloadManagerListener(
DownloadManager downloadManager, DummyMainThread dummyMainThread) { DownloadManager downloadManager, DummyMainThread dummyMainThread) {
this(downloadManager, dummyMainThread, TIMEOUT_MS);
}
public TestDownloadManagerListener(
DownloadManager downloadManager, DummyMainThread dummyMainThread, int timeoutMs) {
this.downloadManager = downloadManager; this.downloadManager = downloadManager;
this.dummyMainThread = dummyMainThread; this.dummyMainThread = dummyMainThread;
this.timeoutMs = timeoutMs;
downloadStates = new HashMap<>(); downloadStates = new HashMap<>();
initializedCondition = new CountDownLatch(1); initializedCondition = TestUtil.createRobolectricConditionVariable();
idleCondition = TestUtil.createRobolectricConditionVariable();
downloadManager.addListener(this); downloadManager.addListener(this);
} }
...@@ -67,13 +58,12 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen ...@@ -67,13 +58,12 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
@Override @Override
public void onInitialized(DownloadManager downloadManager) { public void onInitialized(DownloadManager downloadManager) {
initializedCondition.countDown(); initializedCondition.open();
} }
public void waitUntilInitialized() throws InterruptedException { public void blockUntilInitialized() throws InterruptedException {
if (!downloadManager.isInitialized()) { if (!downloadManager.isInitialized()) {
assertThat(initializedCondition.await(INITIALIZATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) assertThat(initializedCondition.block(TIMEOUT_MS)).isTrue();
.isTrue();
} }
} }
...@@ -92,9 +82,7 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen ...@@ -92,9 +82,7 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
@Override @Override
public synchronized void onIdle(DownloadManager downloadManager) { public synchronized void onIdle(DownloadManager downloadManager) {
if (downloadFinishedCondition != null) { idleCondition.open();
downloadFinishedCondition.countDown();
}
} }
/** /**
...@@ -110,16 +98,14 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen ...@@ -110,16 +98,14 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
/** Blocks until all remove and download tasks are complete. Task errors are ignored. */ /** Blocks until all remove and download tasks are complete. Task errors are ignored. */
public void blockUntilTasksComplete() throws InterruptedException { public void blockUntilTasksComplete() throws InterruptedException {
synchronized (this) { idleCondition.close();
downloadFinishedCondition = new CountDownLatch(1);
}
dummyMainThread.runOnMainThread( dummyMainThread.runOnMainThread(
() -> { () -> {
if (downloadManager.isIdle()) { if (downloadManager.isIdle()) {
Util.castNonNull(downloadFinishedCondition).countDown(); idleCondition.open();
} }
}); });
assertThat(downloadFinishedCondition.await(timeoutMs, TimeUnit.MILLISECONDS)).isTrue(); assertThat(idleCondition.block(TIMEOUT_MS)).isTrue();
} }
private ArrayBlockingQueue<Integer> getStateQueue(String taskId) { private ArrayBlockingQueue<Integer> getStateQueue(String taskId) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment