/* * 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.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import com.facebook.cache.common.CacheKey; import com.facebook.common.internal.ImmutableMap; import com.facebook.common.references.CloseableReference; import com.facebook.imagepipeline.cache.BitmapMemoryCacheKey; import com.facebook.imagepipeline.cache.CacheKeyFactory; import com.facebook.imagepipeline.cache.MemoryCache; import com.facebook.imagepipeline.image.CloseableImage; import com.facebook.imagepipeline.image.ImmutableQualityInfo; import com.facebook.imagepipeline.request.ImageRequest; import org.junit.*; import org.junit.runner.*; import org.mockito.*; import org.mockito.invocation.*; import org.mockito.stubbing.*; import org.robolectric.*; import org.robolectric.annotation.*; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.*; /** * Checks basic properties of bitmap memory cache producer operation, that is: * - it delegates to the {@link MemoryCache#get(Object)} * - if {@link MemoryCache#get(Object)} is unsuccessful, then it passes the * request to the next producer in the sequence. * - if the next producer returns the value of higher quality, * then it is put into the bitmap cache. * - responses from the next producer are passed back down to the consumer. */ @RunWith(RobolectricTestRunner.class) @Config(manifest= Config.NONE) public class BitmapMemoryCacheProducerTest { private static final String PRODUCER_NAME = BitmapMemoryCacheProducer.PRODUCER_NAME; private static final int INTERMEDIATE_SCAN_1 = 2; private static final int INTERMEDIATE_SCAN_2 = 5; @Mock public MemoryCache<CacheKey, CloseableImage> mMemoryCache; @Mock public CacheKeyFactory mCacheKeyFactory; @Mock public Producer mInputProducer; @Mock public Consumer mConsumer; @Mock public ProducerContext mProducerContext; @Mock public ImageRequest mImageRequest; @Mock public ProducerListener mProducerListener; @Mock public Exception mException; private final String mRequestId = "mRequestId"; private BitmapMemoryCacheKey mBitmapMemoryCacheKey; private CloseableImage mCloseableImage1; private CloseableImage mCloseableImage2; private CloseableReference<CloseableImage> mFinalImageReference; private CloseableReference<CloseableImage> mIntermediateImageReference; private CloseableReference<CloseableImage> mFinalImageReferenceClone; private CloseableReference<CloseableImage> mIntermediateImageReferenceClone; private BitmapMemoryCacheProducer mBitmapMemoryCacheProducer; @Before public void setUp() { MockitoAnnotations.initMocks(this); mBitmapMemoryCacheProducer = new BitmapMemoryCacheProducer(mMemoryCache, mCacheKeyFactory, mInputProducer); mBitmapMemoryCacheKey = mock(BitmapMemoryCacheKey.class); mCloseableImage1 = mock(CloseableImage.class); when(mCloseableImage1.getQualityInfo()).thenReturn(ImmutableQualityInfo.FULL_QUALITY); mCloseableImage2 = mock(CloseableImage.class); when(mCloseableImage2.getQualityInfo()) .thenReturn(ImmutableQualityInfo.of(INTERMEDIATE_SCAN_2, true, false)); mFinalImageReference = CloseableReference.of(mCloseableImage1); mIntermediateImageReference = CloseableReference.of(mCloseableImage2); mFinalImageReferenceClone = mFinalImageReference.clone(); mIntermediateImageReferenceClone = mIntermediateImageReference.clone(); when(mMemoryCache.cache(mBitmapMemoryCacheKey, mIntermediateImageReference)) .thenReturn(mIntermediateImageReferenceClone); when(mMemoryCache.cache(mBitmapMemoryCacheKey, mFinalImageReference)) .thenReturn(mFinalImageReferenceClone); when(mProducerContext.getImageRequest()).thenReturn(mImageRequest); when(mProducerContext.getListener()).thenReturn(mProducerListener); when(mProducerContext.getId()).thenReturn(mRequestId); when(mProducerContext.getLowestPermittedRequestLevel()) .thenReturn(ImageRequest.RequestLevel.FULL_FETCH); when(mProducerContext.getCallerContext()).thenReturn(PRODUCER_NAME); when(mProducerListener.requiresExtraMap(mRequestId)).thenReturn(true); when(mCacheKeyFactory.getBitmapCacheKey(mImageRequest, PRODUCER_NAME)) .thenReturn(mBitmapMemoryCacheKey); } @Test public void testBitmapMemoryCacheGetSuccessful() { setupBitmapMemoryCacheGetSuccess(); when(mProducerContext.getLowestPermittedRequestLevel()) .thenReturn(ImageRequest.RequestLevel.BITMAP_MEMORY_CACHE); mBitmapMemoryCacheProducer.produceResults(mConsumer, mProducerContext); verify(mConsumer).onNewResult(mFinalImageReference, Consumer.IS_LAST); verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME); Map<String, String> extraMap = ImmutableMap.of(BitmapMemoryCacheProducer.EXTRA_CACHED_VALUE_FOUND, "true"); verify(mProducerListener).onProducerFinishWithSuccess(mRequestId, PRODUCER_NAME, extraMap); verify(mProducerListener).onUltimateProducerReached(mRequestId, PRODUCER_NAME, true); Assert.assertTrue(!mFinalImageReference.isValid()); } /** * Verify that stateful image results, both intermediate and final, are never cached. */ @Test public void testDoNotCacheStatefulImage() { when(mCloseableImage1.isStateful()).thenReturn(true); when(mCloseableImage2.isStateful()).thenReturn(true); setupBitmapMemoryCacheGetNotFound(); setupInputProducerStreamingSuccess(); when(mMemoryCache.get(mBitmapMemoryCacheKey)).thenReturn(null); mBitmapMemoryCacheProducer.produceResults(mConsumer, mProducerContext); verify(mConsumer).onNewResult(mIntermediateImageReference, Consumer.NO_FLAGS); verify(mConsumer).onNewResult(mFinalImageReference, Consumer.IS_LAST); verify(mMemoryCache, never()).cache( any(BitmapMemoryCacheKey.class), any(CloseableReference.class)); } @Test public void testBitmapMemoryCacheGetIntermediateImage() { setupBitmapMemoryCacheGetIntermediateImage(); setupInputProducerNotFound(); mBitmapMemoryCacheProducer.produceResults(mConsumer, mProducerContext); verify(mConsumer).onNewResult(mIntermediateImageReference, Consumer.NO_FLAGS); verify(mConsumer).onNewResult(null, Consumer.IS_LAST); verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME); Map<String, String> extraMap = ImmutableMap.of(BitmapMemoryCacheProducer.EXTRA_CACHED_VALUE_FOUND, "false"); verify(mProducerListener).onProducerFinishWithSuccess(mRequestId, PRODUCER_NAME, extraMap); verify(mProducerListener, never()) .onUltimateProducerReached(anyString(), anyString(), anyBoolean()); Assert.assertTrue(!mIntermediateImageReference.isValid()); } @Test public void testCacheIntermediateImageAsNoImagePresent() { setupBitmapMemoryCacheGetNotFound(); setupInputProducerStreamingSuccess(); when(mMemoryCache.get(mBitmapMemoryCacheKey)).thenReturn(null); mBitmapMemoryCacheProducer.produceResults(mConsumer, mProducerContext); verify(mMemoryCache).cache(mBitmapMemoryCacheKey, mIntermediateImageReference); verify(mMemoryCache).cache(mBitmapMemoryCacheKey, mFinalImageReference); verify(mConsumer).onNewResult(mIntermediateImageReferenceClone, Consumer.NO_FLAGS); verify(mConsumer).onNewResult(mFinalImageReferenceClone, Consumer.IS_LAST); Assert.assertTrue(!mIntermediateImageReferenceClone.isValid()); Assert.assertTrue(!mFinalImageReferenceClone.isValid()); verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME); Map<String, String> extraMap = ImmutableMap.of(BitmapMemoryCacheProducer.EXTRA_CACHED_VALUE_FOUND, "false"); verify(mProducerListener).onProducerFinishWithSuccess(mRequestId, PRODUCER_NAME, extraMap); verify(mProducerListener, never()) .onUltimateProducerReached(anyString(), anyString(), anyBoolean()); } @Test public void testCacheIntermediateImageAsBetterScan() { setupBitmapMemoryCacheGetNotFound(); setupInputProducerStreamingSuccess(); CloseableImage closeableImage = mock(CloseableImage.class); when(closeableImage.getQualityInfo()) .thenReturn(ImmutableQualityInfo.of(INTERMEDIATE_SCAN_1, false, false)); CloseableReference<CloseableImage> closeableImageRef = CloseableReference.of(closeableImage); when(mMemoryCache.get(mBitmapMemoryCacheKey)) .thenReturn(null) .thenReturn(closeableImageRef); mBitmapMemoryCacheProducer.produceResults(mConsumer, mProducerContext); verify(mMemoryCache).cache(mBitmapMemoryCacheKey, mIntermediateImageReference); verify(mMemoryCache).cache(mBitmapMemoryCacheKey, mFinalImageReference); verify(mConsumer).onNewResult(mIntermediateImageReferenceClone, Consumer.NO_FLAGS); verify(mConsumer).onNewResult(mFinalImageReferenceClone, Consumer.IS_LAST); Assert.assertTrue(!mIntermediateImageReferenceClone.isValid()); Assert.assertTrue(!mFinalImageReferenceClone.isValid()); Assert.assertEquals( 0, closeableImageRef.getUnderlyingReferenceTestOnly().getRefCountTestOnly()); verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME); Map<String, String> extraMap = ImmutableMap.of(BitmapMemoryCacheProducer.EXTRA_CACHED_VALUE_FOUND, "false"); verify(mProducerListener).onProducerFinishWithSuccess(mRequestId, PRODUCER_NAME, extraMap); verify(mProducerListener, never()) .onUltimateProducerReached(anyString(), anyString(), anyBoolean()); } @Test public void testDontCacheIntermediateImageAsAlreadyHaveSameQuality() { setupBitmapMemoryCacheGetNotFound(); setupInputProducerStreamingSuccess(); CloseableImage closeableImage = mock(CloseableImage.class); when(closeableImage.getQualityInfo()) .thenReturn(ImmutableQualityInfo.of(INTERMEDIATE_SCAN_2, true, false)); CloseableReference<CloseableImage> closeableImageRef = CloseableReference.of(closeableImage); when(mMemoryCache.get(mBitmapMemoryCacheKey)) .thenReturn(null) .thenReturn(closeableImageRef); mBitmapMemoryCacheProducer.produceResults(mConsumer, mProducerContext); verify(mMemoryCache, never()).cache(mBitmapMemoryCacheKey, mIntermediateImageReference); verify(mMemoryCache).cache(mBitmapMemoryCacheKey, mFinalImageReference); verify(mConsumer).onNewResult(closeableImageRef, Consumer.NO_FLAGS); verify(mConsumer).onNewResult(mFinalImageReferenceClone, Consumer.IS_LAST); Assert.assertTrue(!mFinalImageReferenceClone.isValid()); Assert.assertEquals( 0, closeableImageRef.getUnderlyingReferenceTestOnly().getRefCountTestOnly()); verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME); Map<String, String> extraMap = ImmutableMap.of(BitmapMemoryCacheProducer.EXTRA_CACHED_VALUE_FOUND, "false"); verify(mProducerListener).onProducerFinishWithSuccess(mRequestId, PRODUCER_NAME, extraMap); verify(mProducerListener, never()) .onUltimateProducerReached(anyString(), anyString(), anyBoolean()); } @Test public void testDontCacheIntermediateImageAsAlreadyHaveFullQuality() { setupBitmapMemoryCacheGetNotFound(); setupInputProducerStreamingSuccess(); CloseableImage closeableImage = mock(CloseableImage.class); when(closeableImage.getQualityInfo()).thenReturn(ImmutableQualityInfo.FULL_QUALITY); CloseableReference<CloseableImage> closeableImageRef = CloseableReference.of(closeableImage); when(mMemoryCache.get(mBitmapMemoryCacheKey)) .thenReturn(null) .thenReturn(closeableImageRef); mBitmapMemoryCacheProducer.produceResults(mConsumer, mProducerContext); verify(mMemoryCache, never()).cache(mBitmapMemoryCacheKey, mIntermediateImageReference); verify(mMemoryCache).cache(mBitmapMemoryCacheKey, mFinalImageReference); verify(mConsumer).onNewResult(closeableImageRef, Consumer.NO_FLAGS); verify(mConsumer).onNewResult(mFinalImageReferenceClone, Consumer.IS_LAST); Assert.assertTrue(!mFinalImageReferenceClone.isValid()); Assert.assertEquals( 0, closeableImageRef.getUnderlyingReferenceTestOnly().getRefCountTestOnly()); verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME); Map<String, String> extraMap = ImmutableMap.of(BitmapMemoryCacheProducer.EXTRA_CACHED_VALUE_FOUND, "false"); verify(mProducerListener).onProducerFinishWithSuccess(mRequestId, PRODUCER_NAME, extraMap); verify(mProducerListener, never()) .onUltimateProducerReached(anyString(), anyString(), anyBoolean()); } @Test public void testBitmapMemoryCacheGetNotFoundInputProducerNotFound() { setupBitmapMemoryCacheGetNotFound(); setupInputProducerNotFound(); mBitmapMemoryCacheProducer.produceResults(mConsumer, mProducerContext); verify(mConsumer).onNewResult(null, Consumer.IS_LAST); verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME); Map<String, String> extraMap = ImmutableMap.of(BitmapMemoryCacheProducer.EXTRA_CACHED_VALUE_FOUND, "false"); verify(mProducerListener).onProducerFinishWithSuccess(mRequestId, PRODUCER_NAME, extraMap); verify(mProducerListener, never()) .onUltimateProducerReached(anyString(), anyString(), anyBoolean()); } @Test public void testBitmapMemoryCacheGetNotFoundLowestLevelReached() { setupBitmapMemoryCacheGetNotFound(); when(mProducerContext.getLowestPermittedRequestLevel()) .thenReturn(ImageRequest.RequestLevel.BITMAP_MEMORY_CACHE); mBitmapMemoryCacheProducer.produceResults(mConsumer, mProducerContext); verify(mConsumer).onNewResult(null, Consumer.IS_LAST); verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME); Map<String, String> extraMap = ImmutableMap.of(BitmapMemoryCacheProducer.EXTRA_CACHED_VALUE_FOUND, "false"); verify(mProducerListener).onProducerFinishWithSuccess(mRequestId, PRODUCER_NAME, extraMap); verify(mProducerListener).onUltimateProducerReached(mRequestId, PRODUCER_NAME, false); verifyNoMoreInteractions(mInputProducer); } @Test public void testBitmapMemoryCacheGetIntermediateImageLowestLevelReached() { setupBitmapMemoryCacheGetIntermediateImage(); when(mProducerContext.getLowestPermittedRequestLevel()) .thenReturn(ImageRequest.RequestLevel.BITMAP_MEMORY_CACHE); mBitmapMemoryCacheProducer.produceResults(mConsumer, mProducerContext); verify(mConsumer).onNewResult(mIntermediateImageReference, Consumer.NO_FLAGS); verify(mConsumer).onNewResult(null, Consumer.IS_LAST); verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME); Map<String, String> extraMap = ImmutableMap.of(BitmapMemoryCacheProducer.EXTRA_CACHED_VALUE_FOUND, "false"); verify(mProducerListener).onProducerFinishWithSuccess(mRequestId, PRODUCER_NAME, extraMap); verify(mProducerListener).onUltimateProducerReached(mRequestId, PRODUCER_NAME, false); Assert.assertTrue(!mIntermediateImageReference.isValid()); verifyNoMoreInteractions(mInputProducer); } @Test public void testBitmapMemoryCacheGetNotFoundInputProducerFailure() { setupBitmapMemoryCacheGetNotFound(); setupInputProducerFailure(); mBitmapMemoryCacheProducer.produceResults(mConsumer, mProducerContext); verify(mConsumer).onFailure(mException); verify(mProducerListener).onProducerStart(mRequestId, PRODUCER_NAME); Map<String, String> extraMap = ImmutableMap.of(BitmapMemoryCacheProducer.EXTRA_CACHED_VALUE_FOUND, "false"); verify(mProducerListener).onProducerFinishWithSuccess(mRequestId, PRODUCER_NAME, extraMap); verify(mProducerListener, never()) .onUltimateProducerReached(anyString(), anyString(), anyBoolean()); } private void setupBitmapMemoryCacheGetSuccess() { when(mMemoryCache.get(eq(mBitmapMemoryCacheKey))) .thenReturn(mFinalImageReference); } private void setupBitmapMemoryCacheGetNotFound() { when(mMemoryCache.get(eq(mBitmapMemoryCacheKey))).thenReturn(null); } private void setupBitmapMemoryCacheGetIntermediateImage() { when(mMemoryCache.get(eq(mBitmapMemoryCacheKey))) .thenReturn(mIntermediateImageReference); } private void setupInputProducerStreamingSuccess() { doAnswer(new ProduceResultsNewResultAnswer( Arrays.asList(mIntermediateImageReference, mFinalImageReference))) .when(mInputProducer).produceResults(any(Consumer.class), eq(mProducerContext)); } private void setupInputProducerNotFound() { final List<CloseableReference<CloseableImage>> nullArray = new ArrayList<CloseableReference<CloseableImage>>(1); nullArray.add(null); doAnswer(new ProduceResultsNewResultAnswer(nullArray)) .when(mInputProducer).produceResults(any(Consumer.class), eq(mProducerContext)); } private void setupInputProducerFailure() { doAnswer(new ProduceResultsFailureAnswer()). when(mInputProducer).produceResults(any(Consumer.class), eq(mProducerContext)); } private static class ProduceResultsNewResultAnswer implements Answer<Void> { private final List<CloseableReference<CloseableImage>> mResults; private ProduceResultsNewResultAnswer(List<CloseableReference<CloseableImage>> results) { mResults = results; } @Override public Void answer(InvocationOnMock invocation) throws Throwable { Consumer consumer = (Consumer) invocation.getArguments()[0]; Iterator<CloseableReference<CloseableImage>> iterator = mResults.iterator(); while (iterator.hasNext()) { CloseableReference<CloseableImage> result = iterator.next(); consumer.onNewResult(result, iterator.hasNext() ? Consumer.NO_FLAGS : Consumer.IS_LAST); } return null; } } private class ProduceResultsFailureAnswer implements Answer<Void> { @Override public Void answer(InvocationOnMock invocation) throws Throwable { Consumer consumer = (Consumer) invocation.getArguments()[0]; consumer.onFailure(mException); return null; } } }