/* * 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> mCacheQueue; private @Mock BlockingQueue> 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 = 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 = 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 = 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(); } }