/*
* 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.bitmaps;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.Random;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Rect;
import android.os.Build;
import com.facebook.common.internal.ByteStreams;
import com.facebook.common.internal.Throwables;
import com.facebook.common.references.CloseableReference;
import com.facebook.common.soloader.SoLoaderShim;
import com.facebook.imagepipeline.memory.BitmapPool;
import com.facebook.imagepipeline.memory.PooledByteBuffer;
import com.facebook.imagepipeline.nativecode.Bitmaps;
import com.facebook.imagepipeline.testing.MockBitmapFactory;
import com.facebook.imageutils.JfifUtil;
import com.facebook.testing.robolectric.v2.WithTestDefaultsRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.mockito.stubbing.OngoingStubbing;
import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.verifyStatic;
/**
* Tests for {@link ArtBitmapFactory}.
*/
@RunWith(WithTestDefaultsRunner.class)
@PrepareOnlyThisForTest({BitmapFactory.class})
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class ArtBitmapFactoryTest {
static {
SoLoaderShim.setInTestMode();
}
private static final int RANDOM_SEED = 10101;
private static final int ENCODED_BYTES_LENGTH = 128;
private BitmapPool mBitmapPool;
private PooledByteBuffer mPooledByteBuffer;
private ArtBitmapFactory mArtBitmapFactory;
public Bitmap mBitmap;
public Answer<Bitmap> mBitmapFactoryDefaultAnswer;
private CloseableReference<PooledByteBuffer> mEncodedImageRef;
private byte[] mEncodedBytes;
@Before
public void setUp() throws Exception {
mBitmapPool = mock(BitmapPool.class);
mPooledByteBuffer = mock(PooledByteBuffer.class);
final Random random = new Random();
random.setSeed(RANDOM_SEED);
mEncodedBytes = new byte[ENCODED_BYTES_LENGTH];
random.nextBytes(mEncodedBytes);
mArtBitmapFactory = new ArtBitmapFactory(mBitmapPool);
doReturn(new ByteArrayInputStream(mEncodedBytes))
.when(mPooledByteBuffer)
.getStream();
doReturn(ENCODED_BYTES_LENGTH)
.when(mPooledByteBuffer)
.size();
mEncodedImageRef = CloseableReference.of(mPooledByteBuffer);
mBitmap = MockBitmapFactory.create();
doReturn(mBitmap).when(mBitmapPool).get(MockBitmapFactory.DEFAULT_BITMAP_PIXELS);
mBitmapFactoryDefaultAnswer = new Answer<Bitmap>() {
@Override
public Bitmap answer(InvocationOnMock invocation) throws Throwable {
final BitmapFactory.Options options = (BitmapFactory.Options) invocation.getArguments()[2];
options.outWidth = MockBitmapFactory.DEFAULT_BITMAP_WIDTH;
options.outHeight = MockBitmapFactory.DEFAULT_BITMAP_HEIGHT;
verifyBitmapFactoryOptions(options);
return options.inJustDecodeBounds ? null : mBitmap;
}
};
whenBitmapFactoryDecodeStream().thenAnswer(mBitmapFactoryDefaultAnswer);
}
@Test
public void testDecodeStaticDecodesFromStream() {
mArtBitmapFactory.decodeFromPooledByteBuffer(mEncodedImageRef);
verifyDecodedFromStream();
}
@Test
public void testDecodeStaticDoesNotLeak() {
mArtBitmapFactory.decodeFromPooledByteBuffer(mEncodedImageRef);
verifyNoLeaks();
}
@Test
public void testStaticImageUsesPooledByteBufferWithPixels() {
CloseableReference<Bitmap> decodedImage =
mArtBitmapFactory.decodeFromPooledByteBuffer(mEncodedImageRef);
closeAndVerifyClosed(decodedImage);
}
@Test(expected = NullPointerException.class)
public void testPoolsReturnsNull() {
doReturn(null).when(mBitmapPool).get(anyInt());
mArtBitmapFactory.decodeFromPooledByteBuffer(mEncodedImageRef);
}
@Test(expected = IllegalStateException.class)
public void testBitmapFactoryReturnsNewBitmap() {
whenBitmapFactoryDecodeStream()
.thenAnswer(mBitmapFactoryDefaultAnswer)
.thenReturn(MockBitmapFactory.create());
try {
mArtBitmapFactory.decodeFromPooledByteBuffer(mEncodedImageRef);
} finally {
verify(mBitmapPool).release(mBitmap);
}
}
@Test(expected = ConcurrentModificationException.class)
public void testBitmapFactoryThrowsAnException() {
whenBitmapFactoryDecodeStream()
.thenAnswer(mBitmapFactoryDefaultAnswer)
.thenThrow(new ConcurrentModificationException());
try {
mArtBitmapFactory.decodeFromPooledByteBuffer(mEncodedImageRef);
} finally {
verify(mBitmapPool).release(mBitmap);
}
}
@Test
public void testDecodeJpeg_allBytes_complete() {
jpegTestCase(true, ENCODED_BYTES_LENGTH);
}
@Test
public void testDecodeJpeg_notAllBytes_complete() {
jpegTestCase(true, ENCODED_BYTES_LENGTH / 2);
}
@Test
public void testDecodeJpeg_allBytes_incomplete() {
jpegTestCase(false, ENCODED_BYTES_LENGTH);
}
@Test
public void testDecodeJpeg_notAllBytes_incomplete() {
jpegTestCase(false, ENCODED_BYTES_LENGTH / 2);
}
private void jpegTestCase(boolean complete, int dataLength) {
if (complete) {
mEncodedBytes[dataLength - 2] = (byte) JfifUtil.MARKER_FIRST_BYTE;
mEncodedBytes[dataLength - 1] = (byte) JfifUtil.MARKER_EOI;
}
CloseableReference<Bitmap> result =
mArtBitmapFactory.decodeJPEGFromPooledByteBuffer(mEncodedImageRef, dataLength);
verifyDecodedFromStream();
verifyNoLeaks();
verifyDecodedBytes(complete, dataLength);
closeAndVerifyClosed(result);
}
private byte[] getDecodedBytes() {
ArgumentCaptor<InputStream> inputStreamArgumentCaptor =
ArgumentCaptor.forClass(InputStream.class);
verifyStatic(times(2));
BitmapFactory.decodeStream(
inputStreamArgumentCaptor.capture(),
isNull(Rect.class),
any(BitmapFactory.Options.class));
InputStream decodedStream = inputStreamArgumentCaptor.getValue();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
ByteStreams.copy(decodedStream, baos);
} catch (IOException ioe) {
throw Throwables.propagate(ioe);
}
return baos.toByteArray();
}
private void verifyBitmapFactoryOptions(BitmapFactory.Options options) {
assertNotNull(options.inTempStorage);
if (!options.inJustDecodeBounds) {
assertTrue(options.inDither);
assertTrue(options.inMutable);
assertSame(Bitmaps.BITMAP_CONFIG, options.inPreferredConfig);
assertNotNull(options.inBitmap);
final int inBitmapWidth = options.inBitmap.getWidth();
final int inBitmapHeight = options.inBitmap.getHeight();
assertTrue(inBitmapWidth * inBitmapHeight >= MockBitmapFactory.DEFAULT_BITMAP_PIXELS);
}
}
private OngoingStubbing<Bitmap> whenBitmapFactoryDecodeStream() {
mockStatic(BitmapFactory.class);
return when(BitmapFactory.decodeStream(
any(InputStream.class),
isNull(Rect.class),
any(BitmapFactory.Options.class)));
}
private void closeAndVerifyClosed(CloseableReference<Bitmap> closeableImage) {
verify(mBitmapPool, never()).release(mBitmap);
closeableImage.close();
verify(mBitmapPool).release(mBitmap);
}
private void verifyNoLeaks() {
assertEquals(1, mEncodedImageRef.getUnderlyingReferenceTestOnly().getRefCountTestOnly());
}
private void verifyDecodedFromStream() {
verifyStatic(times(2));
BitmapFactory.decodeStream(
any(ByteArrayInputStream.class),
isNull(Rect.class),
any(BitmapFactory.Options.class));
}
private void verifyDecodedBytes(boolean complete, int length) {
byte[] decodedBytes = getDecodedBytes();
assertArrayEquals(
Arrays.copyOfRange(
mEncodedBytes,
0,
length),
Arrays.copyOfRange(
decodedBytes,
0,
length));
if (complete) {
assertEquals(length, decodedBytes.length);
} else {
assertEquals(length + 2, decodedBytes.length);
assertEquals((byte) JfifUtil.MARKER_FIRST_BYTE, decodedBytes[length]);
assertEquals((byte) JfifUtil.MARKER_EOI, decodedBytes[length + 1]);
}
}
}