package com.bumptech.glide.load.engine;
import static com.bumptech.glide.tests.Util.anyResource;
import static com.bumptech.glide.tests.Util.isADataSource;
import static com.bumptech.glide.tests.Util.mockResource;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isNull;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.os.Handler;
import android.support.v4.util.Pools;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.Key;
import com.bumptech.glide.load.engine.executor.GlideExecutor;
import com.bumptech.glide.load.engine.executor.MockGlideExecutor;
import com.bumptech.glide.request.ResourceCallback;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, sdk = 18)
public class EngineJobTest {
private EngineJobHarness harness;
@Before
public void setUp() {
harness = new EngineJobHarness();
}
@Test
public void testOnResourceReadyPassedToCallbacks() throws Exception {
EngineJob<Object> job = harness.getJob();
job.start(harness.decodeJob);
job.onResourceReady(harness.resource, harness.dataSource);
ShadowLooper.runUiThreadTasks();
verify(harness.cb).onResourceReady(eq(harness.engineResource), eq(harness.dataSource));
}
@Test
public void testListenerNotifiedJobCompleteOnOnResourceReady() {
EngineJob<Object> job = harness.getJob();
job.start(harness.decodeJob);
job.onResourceReady(harness.resource, harness.dataSource);
ShadowLooper.runUiThreadTasks();
verify(harness.listener).onEngineJobComplete(eq(harness.key), eq(harness.engineResource));
}
@Test
public void testNotifiesAllCallbacksOnReady() {
MultiCbHarness harness = new MultiCbHarness();
harness.job.start(harness.decodeJob);
harness.job.onResourceReady(harness.resource, harness.dataSource);
for (ResourceCallback cb : harness.cbs) {
verify(cb).onResourceReady(eq(harness.engineResource), eq(harness.dataSource));
}
}
@Test
public void testNotifiesAllCallbacksOnException() {
MultiCbHarness harness = new MultiCbHarness();
harness.job.start(harness.decodeJob);
GlideException exception = new GlideException("test");
harness.job.onLoadFailed(exception);
for (ResourceCallback cb : harness.cbs) {
verify(cb).onLoadFailed(eq(exception));
}
}
@Test
public void testAcquiresResourceOncePerCallback() {
MultiCbHarness harness = new MultiCbHarness();
harness.job.start(harness.decodeJob);
harness.job.onResourceReady(harness.resource, harness.dataSource);
// Acquired once and then released while notifying.
InOrder order = inOrder(harness.engineResource);
order.verify(harness.engineResource, times(harness.numCbs + 1)).acquire();
order.verify(harness.engineResource, times(1)).release();
}
@Test
public void testListenerNotifiedJobCompleteOnException() {
harness = new EngineJobHarness();
EngineJob<Object> job = harness.getJob();
job.start(harness.decodeJob);
job.onLoadFailed(new GlideException("test"));
ShadowLooper.runUiThreadTasks();
verify(harness.listener).onEngineJobComplete(eq(harness.key), isNull(EngineResource.class));
}
@Test
public void testResourceIsCacheableWhenIsCacheableOnReady() {
harness.isCacheable = true;
EngineJob<Object> job = harness.getJob();
job.start(harness.decodeJob);
job.onResourceReady(harness.resource, harness.dataSource);
ShadowLooper.runUiThreadTasks();
verify(harness.factory).build(anyResource(), eq(harness.isCacheable));
}
@Test
public void testResourceIsCacheableWhenNotIsCacheableOnReady() {
harness.isCacheable = false;
EngineJob<Object> job = harness.getJob();
job.start(harness.decodeJob);
job.onResourceReady(harness.resource, harness.dataSource);
ShadowLooper.runUiThreadTasks();
verify(harness.factory).build(anyResource(), eq(harness.isCacheable));
}
@Test
public void testListenerNotifiedOfCancelOnCancel() {
EngineJob<Object> job = harness.getJob();
job.start(harness.decodeJob);
job.cancel();
verify(harness.listener).onEngineJobCancelled(eq(job), eq(harness.key));
}
@Test
public void testOnResourceReadyNotDeliveredAfterCancel() {
EngineJob<Object> job = harness.getJob();
job.start(harness.decodeJob);
job.cancel();
job.onResourceReady(harness.resource, harness.dataSource);
ShadowLooper.runUiThreadTasks();
verify(harness.cb, never()).onResourceReady(anyResource(), isADataSource());
}
@Test
public void testOnExceptionNotDeliveredAfterCancel() {
harness = new EngineJobHarness();
EngineJob<Object> job = harness.getJob();
job.start(harness.decodeJob);
job.cancel();
job.onLoadFailed(new GlideException("test"));
ShadowLooper.runUiThreadTasks();
verify(harness.cb, never()).onLoadFailed(any(GlideException.class));
}
@Test
public void testRemovingAllCallbacksCancelsRunner() {
EngineJob<Object> job = harness.getJob();
job.start(harness.decodeJob);
job.removeCallback(harness.cb);
assertTrue(job.isCancelled());
}
@SuppressWarnings("unchecked")
@Test
public void removingSomeCallbacksDoesNotCancelRunner() {
EngineJob<Object> job = harness.getJob();
job.addCallback(mock(ResourceCallback.class));
job.removeCallback(harness.cb);
assertFalse(job.isCancelled());
}
@Test
public void testResourceIsAcquiredOncePerConsumerAndOnceForCache() {
EngineJob<Object> job = harness.getJob();
job.start(harness.decodeJob);
job.onResourceReady(harness.resource, harness.dataSource);
// Once while notifying and once for single callback.
verify(harness.engineResource, times(2)).acquire();
}
@Test
public void testDoesNotNotifyCancelledIfCompletes() {
EngineJob<Object> job = harness.getJob();
job.start(harness.decodeJob);
job.onResourceReady(harness.resource, harness.dataSource);
verify(harness.listener, never()).onEngineJobCancelled(eq(job), eq(harness.key));
}
@Test
public void testDoesNotNotifyCancelledIfAlreadyCancelled() {
EngineJob<Object> job = harness.getJob();
job.start(harness.decodeJob);
job.cancel();
job.cancel();
verify(harness.listener, times(1)).onEngineJobCancelled(eq(job), eq(harness.key));
}
@Test
public void testDoesNotNotifyCancelledIfReceivedException() {
EngineJob<Object> job = harness.getJob();
job.start(harness.decodeJob);
job.onLoadFailed(new GlideException("test"));
verify(harness.listener).onEngineJobComplete(eq(harness.key), isNull(EngineResource.class));
verify(harness.listener, never()).onEngineJobCancelled(any(EngineJob.class), any(Key.class));
}
@Test
public void testReleasesResourceIfCancelledOnReady() {
ShadowLooper shadowLooper = Shadows.shadowOf(harness.mainHandler.getLooper());
shadowLooper.pause();
EngineJob<Object> job = harness.getJob();
job.start(harness.decodeJob);
job.onResourceReady(harness.resource, harness.dataSource);
job.cancel();
shadowLooper.runOneTask();
verify(harness.resource).recycle();
}
@Test
public void testDoesNotAcquireOnceForMemoryCacheIfNotCacheable() {
harness.isCacheable = false;
EngineJob<Object> job = harness.getJob();
job.start(harness.decodeJob);
job.onResourceReady(harness.resource, harness.dataSource);
verify(harness.engineResource, times(2)).acquire();
}
@Test
public void testNotifiesNewCallbackOfResourceIfCallbackIsAddedDuringOnResourceReady() {
final EngineJob<Object> job = harness.getJob();
final ResourceCallback existingCallback = mock(ResourceCallback.class);
final ResourceCallback newCallback = mock(ResourceCallback.class);
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
job.addCallback(newCallback);
return null;
}
}).when(existingCallback).onResourceReady(anyResource(), isADataSource());
job.addCallback(existingCallback);
job.start(harness.decodeJob);
job.onResourceReady(harness.resource, harness.dataSource);
verify(newCallback).onResourceReady(eq(harness.engineResource), eq(harness.dataSource));
}
@Test
public void testNotifiesNewCallbackOfExceptionIfCallbackIsAddedDuringOnException() {
harness = new EngineJobHarness();
final EngineJob<Object> job = harness.getJob();
final ResourceCallback existingCallback = mock(ResourceCallback.class);
final ResourceCallback newCallback = mock(ResourceCallback.class);
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
job.addCallback(newCallback);
return null;
}
}).when(existingCallback).onLoadFailed(any(GlideException.class));
GlideException exception = new GlideException("test");
job.addCallback(existingCallback);
job.start(harness.decodeJob);
job.onLoadFailed(exception);
verify(newCallback).onLoadFailed(eq(exception));
}
@Test
public void testRemovingCallbackDuringOnResourceReadyIsIgnoredIfCallbackHasAlreadyBeenCalled() {
final EngineJob<Object> job = harness.getJob();
final ResourceCallback cb = mock(ResourceCallback.class);
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
job.removeCallback(cb);
return null;
}
}).when(cb).onResourceReady(anyResource(), isADataSource());
job.addCallback(cb);
job.start(harness.decodeJob);
job.onResourceReady(harness.resource, harness.dataSource);
verify(cb, times(1)).onResourceReady(anyResource(), isADataSource());
}
@Test
public void testRemovingCallbackDuringOnExceptionIsIgnoredIfCallbackHasAlreadyBeenCalled() {
harness = new EngineJobHarness();
final EngineJob<Object> job = harness.getJob();
final ResourceCallback cb = mock(ResourceCallback.class);
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
job.removeCallback(cb);
return null;
}
}).when(cb).onLoadFailed(any(GlideException.class));
GlideException exception = new GlideException("test");
job.addCallback(cb);
job.start(harness.decodeJob);
job.onLoadFailed(exception);
verify(cb, times(1)).onLoadFailed(eq(exception));
}
@Test
public void
testRemovingCallbackDuringOnResourceReadyPreventsCallbackFromBeingCalledIfNotYetCalled() {
final EngineJob<Object> job = harness.getJob();
final ResourceCallback notYetCalled = mock(ResourceCallback.class);
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
job.removeCallback(notYetCalled);
return null;
}
}).when(harness.cb).onResourceReady(anyResource(), isADataSource());
job.addCallback(notYetCalled);
job.start(harness.decodeJob);
job.onResourceReady(harness.resource, harness.dataSource);
verify(notYetCalled, never()).onResourceReady(anyResource(), isADataSource());
}
@Test
public void
testRemovingCallbackDuringOnResourceReadyPreventsResourceFromBeingAcquiredForCallback() {
final EngineJob<Object> job = harness.getJob();
final ResourceCallback notYetCalled = mock(ResourceCallback.class);
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
job.removeCallback(notYetCalled);
return null;
}
}).when(harness.cb).onResourceReady(anyResource(), isADataSource());
job.addCallback(notYetCalled);
job.start(harness.decodeJob);
job.onResourceReady(harness.resource, harness.dataSource);
// Once for notifying, once for called.
verify(harness.engineResource, times(2)).acquire();
}
@Test
public void testRemovingCallbackDuringOnExceptionPreventsCallbackFromBeingCalledIfNotYetCalled() {
harness = new EngineJobHarness();
final EngineJob<Object> job = harness.getJob();
final ResourceCallback called = mock(ResourceCallback.class);
final ResourceCallback notYetCalled = mock(ResourceCallback.class);
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
job.removeCallback(notYetCalled);
return null;
}
}).when(called).onLoadFailed(any(GlideException.class));
job.addCallback(called);
job.addCallback(notYetCalled);
job.start(harness.decodeJob);
job.onLoadFailed(new GlideException("test"));
verify(notYetCalled, never()).onResourceReady(anyResource(), isADataSource());
}
@Test
public void testCancelsDecodeJobOnCancel() {
EngineJob<Object> job = harness.getJob();
job.start(harness.decodeJob);
job.cancel();
verify(harness.decodeJob).cancel();
}
@Test
public void testSubmitsDecodeJobToSourceServiceOnSubmitForSource() {
EngineJob<Object> job = harness.getJob();
harness.diskCacheService.shutdownNow();
job.reschedule(harness.decodeJob);
verify(harness.decodeJob).run();
}
@Test
public void testSubmitsDecodeJobToDiskCacheServiceWhenDecodingFromCacheOnStart() {
EngineJob<Object> job = harness.getJob();
when(harness.decodeJob.willDecodeFromCache()).thenReturn(true);
harness.diskCacheService.shutdownNow();
job.start(harness.decodeJob);
verify(harness.decodeJob).run();
}
@Test
public void testSubmitsDecodeJobToSourceServiceWhenDecodingFromSourceOnlyOnStart() {
EngineJob<Object> job = harness.getJob();
when(harness.decodeJob.willDecodeFromCache()).thenReturn(false);
harness.diskCacheService.shutdownNow();
job.start(harness.decodeJob);
verify(harness.decodeJob).run();
}
@Test
public void testSubmitsDecodeJobToUnlimitedSourceServiceWhenDecodingFromSourceOnlyOnStart() {
harness.useUnlimitedSourceGeneratorPool = true;
EngineJob<Object> job = harness.getJob();
when(harness.decodeJob.willDecodeFromCache()).thenReturn(false);
harness.diskCacheService.shutdownNow();
job.start(harness.decodeJob);
verify(harness.decodeJob).run();
}
@SuppressWarnings("unchecked")
private static class MultiCbHarness {
Key key = mock(Key.class);
Resource<Object> resource = mockResource();
EngineResource<Object> engineResource = mock(EngineResource.class);
EngineJobListener listener = mock(EngineJobListener.class);
boolean isCacheable = true;
boolean useUnlimitedSourceGeneratorPool = false;
int numCbs = 10;
List<ResourceCallback> cbs = new ArrayList<>();
EngineJob.EngineResourceFactory factory = mock(EngineJob.EngineResourceFactory.class);
EngineJob<Object> job;
GlideExecutor diskCacheService = MockGlideExecutor.newMainThreadExecutor();
GlideExecutor sourceService = MockGlideExecutor.newMainThreadExecutor();
GlideExecutor sourceUnlimitedService = MockGlideExecutor.newMainThreadUnlimitedExecutor();
Pools.Pool<EngineJob<?>> pool = new Pools.SimplePool<>(1);
DecodeJob<Object> decodeJob = mock(DecodeJob.class);
DataSource dataSource = DataSource.LOCAL;
public MultiCbHarness() {
when(factory.build(eq(resource), eq(isCacheable))).thenReturn(engineResource);
job = new EngineJob<>(diskCacheService, sourceService, sourceUnlimitedService, listener, pool,
factory).init(key, isCacheable, useUnlimitedSourceGeneratorPool);
for (int i = 0; i < numCbs; i++) {
cbs.add(mock(ResourceCallback.class));
}
for (ResourceCallback cb : cbs) {
job.addCallback(cb);
}
}
}
@SuppressWarnings("unchecked")
private static class EngineJobHarness {
EngineJob.EngineResourceFactory factory = mock(EngineJob.EngineResourceFactory.class);
Key key = mock(Key.class);
Handler mainHandler = new Handler();
ResourceCallback cb = mock(ResourceCallback.class);
Resource<Object> resource = mockResource();
EngineResource<Object> engineResource = mock(EngineResource.class);
EngineJobListener listener = mock(EngineJobListener.class);
GlideExecutor diskCacheService = MockGlideExecutor.newMainThreadExecutor();
GlideExecutor sourceService = MockGlideExecutor.newMainThreadExecutor();
GlideExecutor sourceUnlimitedService = MockGlideExecutor.newMainThreadUnlimitedExecutor();
boolean isCacheable = true;
boolean useUnlimitedSourceGeneratorPool = false;
DecodeJob<Object> decodeJob = mock(DecodeJob.class);
Pools.Pool<EngineJob<?>> pool = new Pools.SimplePool<>(1);
DataSource dataSource = DataSource.DATA_DISK_CACHE;
public EngineJob<Object> getJob() {
when(factory.build(eq(resource), eq(isCacheable))).thenReturn(engineResource);
EngineJob<Object> result = new EngineJob<>(
diskCacheService, sourceService, sourceUnlimitedService, listener, pool, factory)
.init(key, isCacheable, useUnlimitedSourceGeneratorPool);
result.addCallback(cb);
return result;
}
}
}