blob: aef6785f578bb83532e6c9ecfd8f9a852f42bb40 [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import com.android.volley.toolbox.StringRequest;
import com.android.volley.utils.CacheTestUtils;
import java.util.concurrent.BlockingQueue;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
@SuppressWarnings("rawtypes")
public class CacheDispatcherTest {
private CacheDispatcher mDispatcher;
private @Mock BlockingQueue<Request<?>> mCacheQueue;
private @Mock BlockingQueue<Request<?>> mNetworkQueue;
private @Mock Cache mCache;
private @Mock ResponseDelivery mDelivery;
private @Mock Network mNetwork;
private StringRequest mRequest;
@Before
public void setUp() throws Exception {
initMocks(this);
mRequest = new StringRequest(Request.Method.GET, "http://foo", null, null);
mDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
}
private static class WaitForever implements Answer {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
Thread.sleep(Long.MAX_VALUE);
return null;
}
}
@Test
public void runStopsOnQuit() throws Exception {
when(mCacheQueue.take()).then(new WaitForever());
mDispatcher.start();
mDispatcher.quit();
mDispatcher.join(1000);
}
private static void verifyNoResponse(ResponseDelivery delivery) {
verify(delivery, never()).postResponse(any(Request.class), any(Response.class));
verify(delivery, never())
.postResponse(any(Request.class), any(Response.class), any(Runnable.class));
verify(delivery, never()).postError(any(Request.class), any(VolleyError.class));
}
// A cancelled request should not be processed at all.
@Test
public void cancelledRequest() throws Exception {
mRequest.cancel();
mDispatcher.processRequest(mRequest);
verify(mCache, never()).get(anyString());
verifyNoResponse(mDelivery);
}
// A cache miss does not post a response and puts the request on the network queue.
@Test
public void cacheMiss() throws Exception {
mDispatcher.processRequest(mRequest);
verifyNoResponse(mDelivery);
verify(mNetworkQueue).put(mRequest);
assertNull(mRequest.getCacheEntry());
}
// A non-expired cache hit posts a response and does not queue to the network.
@Test
public void nonExpiredCacheHit() throws Exception {
Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false);
when(mCache.get(anyString())).thenReturn(entry);
mDispatcher.processRequest(mRequest);
verify(mDelivery).postResponse(any(Request.class), any(Response.class));
verify(mDelivery, never()).postError(any(Request.class), any(VolleyError.class));
}
// A soft-expired cache hit posts a response and queues to the network.
@Test
public void softExpiredCacheHit() throws Exception {
Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true);
when(mCache.get(anyString())).thenReturn(entry);
mDispatcher.processRequest(mRequest);
// Soft expiration needs to use the deferred Runnable variant of postResponse,
// so make sure it gets to run.
ArgumentCaptor<Runnable> runnable = ArgumentCaptor.forClass(Runnable.class);
verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture());
runnable.getValue().run();
// This way we can verify the behavior of the Runnable as well.
verify(mNetworkQueue).put(mRequest);
assertSame(entry, mRequest.getCacheEntry());
verify(mDelivery, never()).postError(any(Request.class), any(VolleyError.class));
}
// An expired cache hit does not post a response and queues to the network.
@Test
public void expiredCacheHit() throws Exception {
Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, true, true);
when(mCache.get(anyString())).thenReturn(entry);
mDispatcher.processRequest(mRequest);
verifyNoResponse(mDelivery);
verify(mNetworkQueue).put(mRequest);
assertSame(entry, mRequest.getCacheEntry());
}
// An fresh cache hit with parse error, does not post a response and queues to the network.
@Test
public void freshCacheHit_parseError() throws Exception {
Request request = mock(Request.class);
when(request.parseNetworkResponse(any(NetworkResponse.class)))
.thenReturn(Response.error(new ParseError()));
when(request.getCacheKey()).thenReturn("cache/key");
Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false);
when(mCache.get(anyString())).thenReturn(entry);
mDispatcher.processRequest(request);
verifyNoResponse(mDelivery);
verify(mNetworkQueue).put(request);
assertNull(request.getCacheEntry());
verify(mCache).invalidate("cache/key", true);
verify(request).addMarker("cache-parsing-failed");
}
@Test
public void duplicateCacheMiss() throws Exception {
StringRequest secondRequest =
new StringRequest(Request.Method.GET, "http://foo", null, null);
mRequest.setSequence(1);
secondRequest.setSequence(2);
mDispatcher.processRequest(mRequest);
mDispatcher.processRequest(secondRequest);
verify(mNetworkQueue).put(mRequest);
verifyNoResponse(mDelivery);
}
@Test
public void tripleCacheMiss_networkErrorOnFirst() throws Exception {
StringRequest secondRequest =
new StringRequest(Request.Method.GET, "http://foo", null, null);
StringRequest thirdRequest =
new StringRequest(Request.Method.GET, "http://foo", null, null);
mRequest.setSequence(1);
secondRequest.setSequence(2);
thirdRequest.setSequence(3);
mDispatcher.processRequest(mRequest);
mDispatcher.processRequest(secondRequest);
mDispatcher.processRequest(thirdRequest);
verify(mNetworkQueue).put(mRequest);
verifyNoResponse(mDelivery);
((Request<?>) mRequest).notifyListenerResponseNotUsable();
// Second request should now be in network queue.
verify(mNetworkQueue).put(secondRequest);
// Another unusable response, third request should now be added.
((Request<?>) secondRequest).notifyListenerResponseNotUsable();
verify(mNetworkQueue).put(thirdRequest);
}
@Test
public void duplicateSoftExpiredCacheHit_failedRequest() throws Exception {
Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true);
when(mCache.get(anyString())).thenReturn(entry);
StringRequest secondRequest =
new StringRequest(Request.Method.GET, "http://foo", null, null);
mRequest.setSequence(1);
secondRequest.setSequence(2);
mDispatcher.processRequest(mRequest);
mDispatcher.processRequest(secondRequest);
// Soft expiration needs to use the deferred Runnable variant of postResponse,
// so make sure it gets to run.
ArgumentCaptor<Runnable> runnable = ArgumentCaptor.forClass(Runnable.class);
verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture());
runnable.getValue().run();
// This way we can verify the behavior of the Runnable as well.
verify(mNetworkQueue).put(mRequest);
verify(mDelivery)
.postResponse(any(Request.class), any(Response.class), any(Runnable.class));
((Request<?>) mRequest).notifyListenerResponseNotUsable();
// Second request should now be in network queue.
verify(mNetworkQueue).put(secondRequest);
}
@Test
public void duplicateSoftExpiredCacheHit_successfulRequest() throws Exception {
Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true);
when(mCache.get(anyString())).thenReturn(entry);
StringRequest secondRequest =
new StringRequest(Request.Method.GET, "http://foo", null, null);
mRequest.setSequence(1);
secondRequest.setSequence(2);
mDispatcher.processRequest(mRequest);
mDispatcher.processRequest(secondRequest);
// Soft expiration needs to use the deferred Runnable variant of postResponse,
// so make sure it gets to run.
ArgumentCaptor<Runnable> runnable = ArgumentCaptor.forClass(Runnable.class);
verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture());
runnable.getValue().run();
// This way we can verify the behavior of the Runnable as well.
verify(mNetworkQueue).put(mRequest);
verify(mDelivery)
.postResponse(any(Request.class), any(Response.class), any(Runnable.class));
((Request<?>) mRequest).notifyListenerResponseReceived(Response.success(null, entry));
// Second request should have delivered response.
verify(mNetworkQueue, never()).put(secondRequest);
verify(mDelivery)
.postResponse(any(Request.class), any(Response.class), any(Runnable.class));
}
@Test
public void processRequestNotifiesListener() throws Exception {
RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class);
RequestQueue queue = new RequestQueue(mCache, mNetwork, 0, mDelivery);
queue.addRequestEventListener(listener);
mRequest.setRequestQueue(queue);
Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false);
when(mCache.get(anyString())).thenReturn(entry);
mDispatcher.processRequest(mRequest);
InOrder inOrder = inOrder(listener);
inOrder.verify(listener)
.onRequestEvent(mRequest, RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_STARTED);
inOrder.verify(listener)
.onRequestEvent(mRequest, RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED);
inOrder.verifyNoMoreInteractions();
}
}