/*
* 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 java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import bolts.Task;
import com.facebook.cache.common.CacheKey;
import com.facebook.cache.common.MultiCacheKey;
import com.facebook.cache.common.SimpleCacheKey;
import com.facebook.common.internal.ImmutableMap;
import com.facebook.common.memory.PooledByteBuffer;
import com.facebook.common.references.CloseableReference;
import com.facebook.imagepipeline.cache.BufferedDiskCache;
import com.facebook.imagepipeline.cache.CacheKeyFactory;
import com.facebook.imagepipeline.common.Priority;
import com.facebook.imagepipeline.image.EncodedImage;
import com.facebook.imagepipeline.request.ImageRequest;
import org.junit.*;
import org.junit.runner.*;
import org.mockito.*;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.*;
import org.robolectric.annotation.*;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;
/**
* Checks basic properties of disk cache producer operation, that is:
* - it delegates to the {@link BufferedDiskCache#get(CacheKey key, AtomicBoolean isCancelled)}
* - it returns a 'copy' of the cached value
* - if {@link BufferedDiskCache#get(CacheKey key, AtomicBoolean isCancelled)} is unsuccessful,
* then it passes the request to the next producer in the sequence.
* - if the next producer returns the value, then it is put into the disk cache.
*/
@RunWith(RobolectricTestRunner.class)
@Config(manifest= Config.NONE)
public class DiskCacheReadProducerTest {
private static final String PRODUCER_NAME = DiskCacheReadProducer.PRODUCER_NAME;
private static final Map EXPECTED_MAP_ON_CACHE_HIT =
ImmutableMap.of(DiskCacheReadProducer.EXTRA_CACHED_VALUE_FOUND, "true");
private static final Map EXPECTED_MAP_ON_CACHE_MISS =
ImmutableMap.of(DiskCacheReadProducer.EXTRA_CACHED_VALUE_FOUND, "false");
@Mock public CacheKeyFactory mCacheKeyFactory;
@Mock public Producer mInputProducer;
@Mock public Consumer mConsumer;
@Mock public ImageRequest mImageRequest;
@Mock public Object mCallerContext;
@Mock public ProducerListener mProducerListener;
@Mock public Exception mException;
private final BufferedDiskCache mDefaultBufferedDiskCache = mock(BufferedDiskCache.class);
private final BufferedDiskCache mSmallImageBufferedDiskCache =
mock(BufferedDiskCache.class);
private SettableProducerContext mProducerContext;
private SettableProducerContext mLowestLevelProducerContext;
private final String mRequestId = "mRequestId";
private MultiCacheKey mCacheKey;
private PooledByteBuffer mIntermediatePooledByteBuffer;
private PooledByteBuffer mFinalPooledByteBuffer;
private CloseableReference<PooledByteBuffer> mIntermediateImageReference;
private CloseableReference<PooledByteBuffer> mFinalImageReference;
private EncodedImage mIntermediateEncodedImage;
private EncodedImage mFinalEncodedImage;
private Task.TaskCompletionSource mTaskCompletionSource;
private ArgumentCaptor<AtomicBoolean> mIsCancelled;
private DiskCacheReadProducer mDiskCacheReadProducer;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mDiskCacheReadProducer = new DiskCacheReadProducer(
mDefaultBufferedDiskCache,
mSmallImageBufferedDiskCache,
mCacheKeyFactory,
mInputProducer
);
List<CacheKey> keys = new ArrayList<>(1);
keys.add(new SimpleCacheKey("http://dummy.uri"));
mCacheKey = new MultiCacheKey(keys);
mIntermediatePooledByteBuffer = mock(PooledByteBuffer.class);
mFinalPooledByteBuffer = mock(PooledByteBuffer.class);
mIntermediateImageReference = CloseableReference.of(mIntermediatePooledByteBuffer);
mFinalImageReference = CloseableReference.of(mFinalPooledByteBuffer);
mIntermediateEncodedImage = new EncodedImage(mIntermediateImageReference);
mFinalEncodedImage = new EncodedImage(mFinalImageReference);
mIsCancelled = ArgumentCaptor.forClass(AtomicBoolean.class);
mProducerContext = new SettableProducerContext(
mImageRequest,
mRequestId,
mProducerListener,
mCallerContext,
ImageRequest.RequestLevel.FULL_FETCH,
false,
true,
Priority.MEDIUM);
mLowestLevelProducerContext = new SettableProducerContext(
mImageRequest,
mRequestId,
mProducerListener,
mCallerContext,
ImageRequest.RequestLevel.DISK_CACHE,
false,
true,
Priority.MEDIUM);
when(mProducerListener.requiresExtraMap(mRequestId)).thenReturn(true);
when(mCacheKeyFactory.getEncodedCacheKey(mImageRequest, mCallerContext)).thenReturn(mCacheKey);
when(mImageRequest.getCacheChoice()).thenReturn(ImageRequest.CacheChoice.DEFAULT);
when(mImageRequest.isDiskCacheEnabled()).thenReturn(true);
}
@Test
public void testStartInputProducerIfNotEnabled() {
when(mImageRequest.isDiskCacheEnabled()).thenReturn(false);
mDiskCacheReadProducer.produceResults(mConsumer, mProducerContext);
verify(mInputProducer).produceResults(mConsumer, mProducerContext);
verifyNoMoreInteractions(
mConsumer,
mProducerListener,
mCacheKeyFactory,
mDefaultBufferedDiskCache,
mSmallImageBufferedDiskCache);
}
@Test
public void testNotEnabledAndLowestLevel() {
when(mImageRequest.isDiskCacheEnabled()).thenReturn(false);
mDiskCacheReadProducer.produceResults(mConsumer, mLowestLevelProducerContext);
verify(mConsumer).onNewResult(null, Consumer.IS_LAST);
verifyNoMoreInteractions(
mProducerListener,
mInputProducer,
mCacheKeyFactory,
mDefaultBufferedDiskCache,
mSmallImageBufferedDiskCache);
}
@Test
public void testDefaultDiskCacheGetSuccessful() {
setupDiskCacheGetSuccess(mDefaultBufferedDiskCache);
mDiskCacheReadProducer.produceResults(mConsumer, mProducerContext);
verify(mConsumer).onNewResult(mFinalEncodedImage, Consumer.IS_LAST);
verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME);
ArgumentCaptor<HashMap> captor = ArgumentCaptor.forClass(HashMap.class);
verify(mProducerListener).onProducerFinishWithSuccess(
eq(mRequestId), eq(PRODUCER_NAME), captor.capture());
Map<String, String> resultMap = captor.getValue();
assertEquals("true", resultMap.get(DiskCacheReadProducer.EXTRA_CACHED_VALUE_FOUND));
assertEquals(
"0",
resultMap.get(DiskCacheReadProducer.ENCODED_IMAGE_SIZE));
verify(mProducerListener).onUltimateProducerReached(mRequestId, PRODUCER_NAME, true);
Assert.assertFalse(EncodedImage.isValid(mFinalEncodedImage));
}
@Test
public void testSmallImageDiskCacheGetSuccessful() {
when(mImageRequest.getCacheChoice()).thenReturn(ImageRequest.CacheChoice.SMALL);
setupDiskCacheGetSuccess(mSmallImageBufferedDiskCache);
mDiskCacheReadProducer.produceResults(mConsumer, mProducerContext);
verify(mConsumer).onNewResult(mFinalEncodedImage, Consumer.IS_LAST);
verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME);
ArgumentCaptor<HashMap> captor = ArgumentCaptor.forClass(HashMap.class);
verify(mProducerListener).onProducerFinishWithSuccess(
eq(mRequestId), eq(PRODUCER_NAME), captor.capture());
Map<String, String> resultMap = captor.getValue();
assertEquals("true", resultMap.get(DiskCacheReadProducer.EXTRA_CACHED_VALUE_FOUND));
assertEquals(
"0",
resultMap.get(DiskCacheReadProducer.ENCODED_IMAGE_SIZE));
verify(mProducerListener).onUltimateProducerReached(mRequestId, PRODUCER_NAME, true);
Assert.assertFalse(EncodedImage.isValid(mFinalEncodedImage));
}
@Test
public void testDiskCacheGetSuccessfulNoExtraMap() {
setupDiskCacheGetSuccess(mDefaultBufferedDiskCache);
when(mProducerListener.requiresExtraMap(mRequestId)).thenReturn(false);
mDiskCacheReadProducer.produceResults(mConsumer, mProducerContext);
verify(mConsumer).onNewResult(mFinalEncodedImage, Consumer.IS_LAST);
verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME);
verify(mProducerListener).onProducerFinishWithSuccess(
mRequestId, PRODUCER_NAME, null);
verify(mProducerListener).onUltimateProducerReached(mRequestId, PRODUCER_NAME, true);
Assert.assertFalse(EncodedImage.isValid(mFinalEncodedImage));
}
@Test
public void testDiskCacheGetSuccessfulLowestLevelReached() {
setupDiskCacheGetSuccess(mDefaultBufferedDiskCache);
when(mProducerListener.requiresExtraMap(mRequestId)).thenReturn(false);
mDiskCacheReadProducer.produceResults(mConsumer, mLowestLevelProducerContext);
verify(mConsumer).onNewResult(mFinalEncodedImage, Consumer.IS_LAST);
verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME);
verify(mProducerListener).onProducerFinishWithSuccess(
mRequestId, PRODUCER_NAME, null);
verify(mProducerListener).onUltimateProducerReached(mRequestId, PRODUCER_NAME, true);
Assert.assertFalse(EncodedImage.isValid(mFinalEncodedImage));
}
@Test
public void testDiskCacheGetFailureInputProducerSuccess() {
setupDiskCacheGetFailure(mDefaultBufferedDiskCache);
setupInputProducerSuccess();
mDiskCacheReadProducer.produceResults(mConsumer, mProducerContext);
verify(mConsumer).onNewResult(mFinalEncodedImage, Consumer.IS_LAST);
verify(mProducerListener).onProducerFinishWithFailure(
mRequestId, PRODUCER_NAME, mException, null);
verify(mProducerListener, never())
.onUltimateProducerReached(anyString(), anyString(), anyBoolean());
}
@Test
public void testDiskCacheGetFailureInputProducerNotFound() {
setupDiskCacheGetFailure(mDefaultBufferedDiskCache);
setupInputProducerNotFound();
mDiskCacheReadProducer.produceResults(mConsumer, mProducerContext);
verify(mConsumer).onNewResult(null, Consumer.IS_LAST);
}
@Test
public void testDiskCacheGetFailureInputProducerFailure() {
setupDiskCacheGetFailure(mDefaultBufferedDiskCache);
setupInputProducerFailure();
mDiskCacheReadProducer.produceResults(mConsumer, mProducerContext);
verify(mConsumer).onFailure(mException);
verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME);
verify(mProducerListener).onProducerFinishWithFailure(
mRequestId, PRODUCER_NAME, mException, null);
verify(mProducerListener, never())
.onUltimateProducerReached(anyString(), anyString(), anyBoolean());
}
@Test
public void testDiskCacheGetFailureLowestLevelReached() {
setupDiskCacheGetFailure(mDefaultBufferedDiskCache);
mDiskCacheReadProducer.produceResults(mConsumer, mLowestLevelProducerContext);
verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME);
verify(mProducerListener).onProducerFinishWithFailure(
mRequestId, PRODUCER_NAME, mException, null);
verify(mProducerListener, never())
.onUltimateProducerReached(anyString(), anyString(), anyBoolean());
verify(mInputProducer).produceResults(mConsumer, mLowestLevelProducerContext);
}
@Test
public void testDefaultDiskCacheGetNotFoundInputProducerSuccess() {
setupDiskCacheGetNotFound(mDefaultBufferedDiskCache);
setupInputProducerSuccess();
mDiskCacheReadProducer.produceResults(mConsumer, mProducerContext);
verify(mInputProducer).produceResults(mConsumer, mProducerContext);
verify(mConsumer).onNewResult(mFinalEncodedImage, Consumer.IS_LAST);
verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME);
ArgumentCaptor<HashMap> captor = ArgumentCaptor.forClass(HashMap.class);
verify(mProducerListener).onProducerFinishWithSuccess(
eq(mRequestId), eq(PRODUCER_NAME), captor.capture());
Map<String, String> resultMap = captor.getValue();
assertEquals("false", resultMap.get(DiskCacheReadProducer.EXTRA_CACHED_VALUE_FOUND));
verify(mProducerListener, never())
.onUltimateProducerReached(anyString(), anyString(), anyBoolean());
assertNull(resultMap.get(DiskCacheReadProducer.ENCODED_IMAGE_SIZE));
}
@Test
public void testSmallImageDiskCacheGetNotFoundInputProducerSuccess() {
when(mImageRequest.getCacheChoice()).thenReturn(ImageRequest.CacheChoice.SMALL);
setupDiskCacheGetNotFound(mSmallImageBufferedDiskCache);
setupInputProducerSuccess();
mDiskCacheReadProducer.produceResults(mConsumer, mProducerContext);
verify(mConsumer).onNewResult(mFinalEncodedImage, Consumer.IS_LAST);
verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME);
ArgumentCaptor<HashMap> captor = ArgumentCaptor.forClass(HashMap.class);
verify(mProducerListener).onProducerFinishWithSuccess(
eq(mRequestId), eq(PRODUCER_NAME), captor.capture());
Map<String, String> resultMap = captor.getValue();
assertEquals("false", resultMap.get(DiskCacheReadProducer.EXTRA_CACHED_VALUE_FOUND));
assertNull(resultMap.get(DiskCacheReadProducer.ENCODED_IMAGE_SIZE));
verify(mProducerListener, never())
.onUltimateProducerReached(anyString(), anyString(), anyBoolean());
}
@Test
public void testDiskCacheGetNotFoundInputProducerSuccessNoExtraMap() {
setupDiskCacheGetNotFound(mDefaultBufferedDiskCache);
setupInputProducerSuccess();
when(mProducerListener.requiresExtraMap(mRequestId)).thenReturn(false);
mDiskCacheReadProducer.produceResults(mConsumer, mProducerContext);
verify(mInputProducer).produceResults(mConsumer, mProducerContext);
verify(mConsumer).onNewResult(mFinalEncodedImage, Consumer.IS_LAST);
verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME);
verify(mProducerListener).onProducerFinishWithSuccess(
mRequestId, PRODUCER_NAME, null);
verify(mProducerListener, never())
.onUltimateProducerReached(anyString(), anyString(), anyBoolean());
}
@Test
public void testDiskCacheGetNotFoundInputProducerNotFound() {
setupDiskCacheGetNotFound(mDefaultBufferedDiskCache);
setupInputProducerNotFound();
mDiskCacheReadProducer.produceResults(mConsumer, mProducerContext);
verify(mConsumer).onNewResult(null, Consumer.IS_LAST);
}
@Test
public void testDiskCacheGetNotFoundInputProducerFailure() {
setupDiskCacheGetNotFound(mDefaultBufferedDiskCache);
setupInputProducerFailure();
mDiskCacheReadProducer.produceResults(mConsumer, mProducerContext);
verify(mConsumer).onFailure(mException);
verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME);
ArgumentCaptor<HashMap> captor = ArgumentCaptor.forClass(HashMap.class);
verify(mProducerListener).onProducerFinishWithSuccess(
eq(mRequestId), eq(PRODUCER_NAME), captor.capture());
Map<String, String> resultMap = captor.getValue();
assertEquals("false", resultMap.get(DiskCacheReadProducer.EXTRA_CACHED_VALUE_FOUND));
assertNull(resultMap.get(DiskCacheReadProducer.ENCODED_IMAGE_SIZE));
verify(mProducerListener, never())
.onUltimateProducerReached(anyString(), anyString(), anyBoolean());
}
@Test
public void testDiskCacheGetNotFoundLowestLevelReached() {
setupDiskCacheGetNotFound(mDefaultBufferedDiskCache);
when(mProducerListener.requiresExtraMap(mRequestId)).thenReturn(false);
mDiskCacheReadProducer.produceResults(mConsumer, mLowestLevelProducerContext);
verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME);
verify(mProducerListener).onProducerFinishWithSuccess(
mRequestId, PRODUCER_NAME, null);
verify(mProducerListener, never())
.onUltimateProducerReached(anyString(), anyString(), anyBoolean());
verify(mInputProducer).produceResults(mConsumer, mLowestLevelProducerContext);
}
@Test
public void testGetExtraMap() {
final Map<String, String> trueValue = ImmutableMap.of(
DiskCacheReadProducer.EXTRA_CACHED_VALUE_FOUND,
"true",
DiskCacheReadProducer.ENCODED_IMAGE_SIZE,
"123");
assertEquals(
trueValue,
DiskCacheReadProducer.getExtraMap(mProducerListener, mRequestId, true, 123));
final Map<String, String> falseValue = ImmutableMap.of(
DiskCacheReadProducer.EXTRA_CACHED_VALUE_FOUND,
"false");
assertEquals(
falseValue,
DiskCacheReadProducer.getExtraMap(mProducerListener, mRequestId, false, 0));
}
@Test
public void testDiskCacheGetCancelled() {
setupDiskCacheGetWait(mDefaultBufferedDiskCache);
mDiskCacheReadProducer.produceResults(mConsumer, mProducerContext);
verify(mConsumer, never()).onCancellation();
assertFalse(mIsCancelled.getValue().get());
mProducerContext.cancel();
assertTrue(mIsCancelled.getValue().get());
mTaskCompletionSource.trySetCancelled();
verify(mConsumer).onCancellation();
verify(mInputProducer, never()).produceResults(any(Consumer.class), eq(mProducerContext));
verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME);
verify(mProducerListener).onProducerFinishWithCancellation(mRequestId, PRODUCER_NAME, null);
verify(mProducerListener, never()).onProducerFinishWithFailure(
eq(mRequestId),
any(String.class),
any(Exception.class),
any(Map.class));
verify(mProducerListener, never()).onProducerFinishWithSuccess(
eq(mRequestId),
any(String.class),
any(Map.class));
verify(mProducerListener, never())
.onUltimateProducerReached(anyString(), anyString(), anyBoolean());
}
private void setupDiskCacheGetWait(BufferedDiskCache bufferedDiskCache) {
mTaskCompletionSource = Task.create();
when(bufferedDiskCache.get(eq(mCacheKey), mIsCancelled.capture()))
.thenReturn(mTaskCompletionSource.getTask());
}
private void setupDiskCacheGetSuccess(BufferedDiskCache bufferedDiskCache) {
when(bufferedDiskCache.get(eq(mCacheKey), any(AtomicBoolean.class)))
.thenReturn(Task.forResult(mFinalEncodedImage));
}
private void setupDiskCacheGetNotFound(BufferedDiskCache bufferedDiskCache) {
when(bufferedDiskCache.get(eq(mCacheKey), any(AtomicBoolean.class)))
.thenReturn(Task.<EncodedImage>forResult(null));
}
private void setupDiskCacheGetFailure(BufferedDiskCache bufferedDiskCache) {
when(bufferedDiskCache.get(eq(mCacheKey), any(AtomicBoolean.class)))
.thenReturn(Task.<EncodedImage>forError(mException));
}
private void setupInputProducerSuccess() {
doAnswer(
new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Consumer consumer = (Consumer) invocation.getArguments()[0];
consumer.onNewResult(mIntermediateEncodedImage, Consumer.NO_FLAGS);
consumer.onNewResult(mFinalEncodedImage, Consumer.IS_LAST);
return null;
}
}).when(mInputProducer).produceResults(any(Consumer.class), eq(mProducerContext));
}
private void setupInputProducerFailure() {
doAnswer(
new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Consumer consumer = (Consumer) invocation.getArguments()[0];
consumer.onFailure(mException);
return null;
}
}).when(mInputProducer).produceResults(any(Consumer.class), eq(mProducerContext));
}
private void setupInputProducerNotFound() {
doAnswer(
new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Consumer consumer = (Consumer) invocation.getArguments()[0];
consumer.onNewResult(null, Consumer.IS_LAST);
return null;
}
}).when(mInputProducer).produceResults(any(Consumer.class), eq(mProducerContext));
}
}