/*
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.imagepipeline.producers;
import javax.annotation.concurrent.GuardedBy;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import android.os.SystemClock;
import com.facebook.common.internal.Throwables;
import com.facebook.common.memory.ByteArrayPool;
import com.facebook.common.memory.PooledByteBuffer;
import com.facebook.common.memory.PooledByteBufferFactory;
import com.facebook.common.memory.PooledByteBufferOutputStream;
import com.facebook.common.references.CloseableReference;
import com.facebook.imagepipeline.common.Priority;
import com.facebook.imagepipeline.request.ImageRequest;
import org.junit.*;
import org.junit.runner.*;
import org.mockito.*;
import org.mockito.Mock;
import org.powermock.api.mockito.*;
import org.powermock.core.classloader.annotations.*;
import org.powermock.modules.junit4.rule.*;
import org.robolectric.*;
import org.robolectric.annotation.*;
import static org.junit.Assert.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
@RunWith(RobolectricTestRunner.class)
@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" })
@Config(manifest= Config.NONE)
@PrepareForTest({SystemClock.class})
public class NetworkFetchProducerTest {
@Mock public ByteArrayPool mByteArrayPool;
@Mock public PooledByteBuffer mPooledByteBuffer;
@Mock public PooledByteBufferOutputStream mPooledByteBufferOutputStream;
@Mock public PooledByteBufferFactory mPooledByteBufferFactory;
@Mock public ImageRequest mImageRequest;
@Mock public ProducerListener mProducerListener;
@Mock public Consumer mConsumer;
@Mock public NetworkFetcher mNetworkFetcher;
@Mock public Map<String, String> mExtrasMap;
@Rule
public PowerMockRule rule = new PowerMockRule();
private byte[] mCommonByteArray;
private final String mRequestId = "mRequestId";
private NetworkFetchProducer mNetworkFetchProducer;
private SettableProducerContext mProducerContext;
private FetchState mFetchState;
private ExecutorService mTestExecutor;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
PowerMockito.mockStatic(SystemClock.class);
mNetworkFetchProducer = new NetworkFetchProducer(
mPooledByteBufferFactory,
mByteArrayPool,
mNetworkFetcher);
mProducerContext = new SettableProducerContext(
mImageRequest,
mRequestId,
mProducerListener,
mock(Object.class),
ImageRequest.RequestLevel.FULL_FETCH,
false /* isPrefetch */,
true /* isIntermediateResultExpected */,
Priority.MEDIUM);
mFetchState = new FetchState(mConsumer, mProducerContext);
mCommonByteArray = new byte[10];
when(mByteArrayPool.get(anyInt())).thenReturn(mCommonByteArray);
when(mPooledByteBufferFactory.newOutputStream(anyInt()))
.thenReturn(mPooledByteBufferOutputStream);
when(mPooledByteBufferFactory.newOutputStream())
.thenReturn(mPooledByteBufferOutputStream);
when(mPooledByteBufferOutputStream.toByteBuffer()).thenReturn(mPooledByteBuffer);
when(mProducerListener.requiresExtraMap(anyString())).thenReturn(true);
when(mNetworkFetcher.getExtraMap(any(FetchState.class), anyInt())).thenReturn(mExtrasMap);
when(mNetworkFetcher.createFetchState(eq(mConsumer), eq(mProducerContext)))
.thenReturn(mFetchState);
mTestExecutor = Executors.newCachedThreadPool();
}
@After
public void tearDown() {
mTestExecutor.shutdownNow();
}
@Test
public void testExceptionInFetchImage() {
NetworkFetcher.Callback callback = performFetch();
callback.onFailure(new RuntimeException());
verify(mProducerListener).onProducerFinishWithFailure(
eq(mRequestId),
eq(NetworkFetchProducer.PRODUCER_NAME),
any(RuntimeException.class),
isNull(Map.class));
verify(mProducerListener)
.onUltimateProducerReached(mRequestId, NetworkFetchProducer.PRODUCER_NAME, false);
}
@Test(timeout = 5000)
public void testNoIntermediateResults() throws Exception {
long currentTime = 86400l;
when(SystemClock.uptimeMillis()).thenReturn(currentTime);
NetworkFetcher.Callback callback = performFetch();
when(mNetworkFetcher.shouldPropagate(any(FetchState.class))).thenReturn(false);
final BlockingInputStream inputStream = new BlockingInputStream();
final Future requestHandlerFuture = performResponse(inputStream, -1, callback);
// Consumer should not be notified before any data is read
inputStream.waitUntilReadingThreadBlocked();
verify(mPooledByteBufferFactory).newOutputStream();
verify(mConsumer, never()).onNewResult(any(CloseableReference.class), anyInt());
verifyPooledByteBufferUsed(0);
// Allow NetworkFetchProducer to read 1024 bytes and check that consumer is not notified
inputStream.increaseBytesToRead(1024);
inputStream.waitUntilReadingThreadBlocked();
inputStream.increaseBytesToRead(1024);
inputStream.waitUntilReadingThreadBlocked();
verify(mConsumer, never()).onNewResult(any(CloseableReference.class), anyInt());
verifyPooledByteBufferUsed(0);
inputStream.signalEof();
requestHandlerFuture.get();
// Check no intermediate results were propagated
verify(mProducerListener, times(0)).onProducerEvent(
mRequestId,
NetworkFetchProducer.PRODUCER_NAME,
NetworkFetchProducer.INTERMEDIATE_RESULT_PRODUCER_EVENT);
// Test final result
verify(mConsumer, times(1)).onNewResult(any(CloseableReference.class), eq(Consumer.IS_LAST));
verifyPooledByteBufferUsed(1);
// When everything is over, pooled byte buffer output stream should be closed
verify(mPooledByteBufferOutputStream).close();
}
@Test(timeout = 5000)
public void testDownloadHandler() throws Exception {
long currentTime = 86400l;
when(SystemClock.uptimeMillis()).thenReturn(currentTime);
NetworkFetcher.Callback callback = performFetch();
when(mNetworkFetcher.shouldPropagate(any(FetchState.class))).thenReturn(true);
final BlockingInputStream inputStream = new BlockingInputStream();
final Future requestHandlerFuture = performResponse(inputStream, -1, callback);
// Consumer should not be notified before any data is read
inputStream.waitUntilReadingThreadBlocked();
verify(mPooledByteBufferFactory).newOutputStream();
verify(mConsumer, never()).onNewResult(any(CloseableReference.class), anyInt());
verifyPooledByteBufferUsed(0);
// Allow NetworkFetchProducer to read 1024 bytes and check that consumer is notified once
inputStream.increaseBytesToRead(1024);
inputStream.waitUntilReadingThreadBlocked();
verify(mProducerListener, times(1)).onProducerEvent(
mRequestId,
NetworkFetchProducer.PRODUCER_NAME,
NetworkFetchProducer.INTERMEDIATE_RESULT_PRODUCER_EVENT);
verify(mConsumer, times(1)).onNewResult(any(CloseableReference.class), eq(Consumer.NO_FLAGS));
verifyPooledByteBufferUsed(1);
// Read another 1024 bytes, but do not bump timer - consumer should not be notified
inputStream.increaseBytesToRead(1024);
inputStream.waitUntilReadingThreadBlocked();
verify(mProducerListener, times(1)).onProducerEvent(
mRequestId,
NetworkFetchProducer.PRODUCER_NAME,
NetworkFetchProducer.INTERMEDIATE_RESULT_PRODUCER_EVENT);
verify(mConsumer, times(1)).onNewResult(any(CloseableReference.class), eq(Consumer.NO_FLAGS));
verifyPooledByteBufferUsed(1);
// Read another 1024 bytes - this time bump timer. Consumer should be notified
currentTime += NetworkFetchProducer.TIME_BETWEEN_PARTIAL_RESULTS_MS;
when(SystemClock.uptimeMillis()).thenReturn(currentTime);
inputStream.increaseBytesToRead(1024);
inputStream.waitUntilReadingThreadBlocked();
verify(mProducerListener, times(2)).onProducerEvent(
mRequestId,
NetworkFetchProducer.PRODUCER_NAME,
NetworkFetchProducer.INTERMEDIATE_RESULT_PRODUCER_EVENT);
verify(mConsumer, times(2)).onNewResult(any(CloseableReference.class), eq(Consumer.NO_FLAGS));
verifyPooledByteBufferUsed(2);
// Test final result
verify(mConsumer, times(0)).onNewResult(any(CloseableReference.class), eq(Consumer.IS_LAST));
inputStream.signalEof();
requestHandlerFuture.get();
verify(mProducerListener, times(2)).onProducerEvent(
mRequestId,
NetworkFetchProducer.PRODUCER_NAME,
NetworkFetchProducer.INTERMEDIATE_RESULT_PRODUCER_EVENT);
verify(mProducerListener).onProducerFinishWithSuccess(
eq(mRequestId), eq(NetworkFetchProducer.PRODUCER_NAME), eq(mExtrasMap));
verify(mProducerListener)
.onUltimateProducerReached(mRequestId, NetworkFetchProducer.PRODUCER_NAME, true);
verify(mConsumer, times(1)).onNewResult(any(CloseableReference.class), eq(Consumer.IS_LAST));
verifyPooledByteBufferUsed(3);
// When everything is over, pooled byte buffer output stream should be closed
verify(mPooledByteBufferOutputStream).close();
}
@Test
public void testExceptionInResponseHandler() throws IOException {
NetworkFetcher.Callback callback = performFetch();
InputStream inputStream = mock(InputStream.class);
when(inputStream.read(any(byte[].class))).thenThrow(new IOException());
when(mNetworkFetcher.shouldPropagate(any(FetchState.class))).thenReturn(false);
try {
callback.onResponse(inputStream, 100);
fail();
} catch (Exception e) {
verify(mPooledByteBufferFactory).newOutputStream(100);
verify(mPooledByteBufferOutputStream).close();
verify(mProducerListener, never())
.onProducerEvent(eq(mRequestId), any(String.class), any(String.class));
}
}
private void verifyPooledByteBufferUsed(int times) {
verify(mPooledByteBufferOutputStream, times(times)).toByteBuffer();
verify(mPooledByteBuffer, times(times)).close();
}
private NetworkFetcher.Callback performFetch() {
mNetworkFetchProducer.produceResults(mConsumer, mProducerContext);
ArgumentCaptor<NetworkFetcher.Callback> callbackCaptor =
ArgumentCaptor.forClass(NetworkFetcher.Callback.class);
verify(mNetworkFetcher).fetch(any(FetchState.class), callbackCaptor.capture());
return callbackCaptor.getValue();
}
private Future performResponse(
final InputStream inputStream,
final int length,
final NetworkFetcher.Callback callback) {
return mTestExecutor.submit(
new Callable() {
@Override
public Object call() throws Exception {
callback.onResponse(inputStream, length);
return null;
}
});
}
private class BlockingInputStream extends InputStream {
@GuardedBy("BlockingInputStream.this")
private int mBytesLeft = 0;
@GuardedBy("BlockingInputStream.this")
private boolean mFinished = false;
@GuardedBy("BlockingInputStream.this")
private boolean mReaderBlocked = false;
@Override
public int read() throws IOException {
fail();
return 0;
}
@Override
public synchronized int read(byte[] buffer, int offset, int length) throws IOException {
while (true) {
if (mBytesLeft > 0) {
final int bytesToRead = Math.min(mBytesLeft, length);
mBytesLeft -= bytesToRead;
return bytesToRead;
} else if (mFinished) {
return -1;
} else {
mReaderBlocked = true;
try {
notify();
wait();
} catch (InterruptedException ie) {
throw Throwables.propagate(ie);
} finally {
mReaderBlocked = false;
}
}
}
}
public synchronized void increaseBytesToRead(int n) {
mBytesLeft += n;
notify();
}
public synchronized void waitUntilReadingThreadBlocked() {
while (mBytesLeft > 0 || !mReaderBlocked) {
try {
wait();
} catch (InterruptedException ie) {
throw Throwables.propagate(ie);
}
}
}
public synchronized void signalEof() {
mFinished = true;
notify();
}
}
}