1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.volley; 18 19 import static org.junit.Assert.assertNull; 20 import static org.junit.Assert.assertSame; 21 import static org.mockito.ArgumentMatchers.any; 22 import static org.mockito.Matchers.anyString; 23 import static org.mockito.Mockito.inOrder; 24 import static org.mockito.Mockito.mock; 25 import static org.mockito.Mockito.never; 26 import static org.mockito.Mockito.verify; 27 import static org.mockito.Mockito.when; 28 import static org.mockito.MockitoAnnotations.initMocks; 29 30 import com.android.volley.toolbox.StringRequest; 31 import com.android.volley.utils.CacheTestUtils; 32 import java.util.concurrent.BlockingQueue; 33 import org.junit.Before; 34 import org.junit.Test; 35 import org.junit.runner.RunWith; 36 import org.mockito.ArgumentCaptor; 37 import org.mockito.InOrder; 38 import org.mockito.Mock; 39 import org.mockito.invocation.InvocationOnMock; 40 import org.mockito.stubbing.Answer; 41 import org.robolectric.RobolectricTestRunner; 42 43 @RunWith(RobolectricTestRunner.class) 44 @SuppressWarnings("rawtypes") 45 public class CacheDispatcherTest { 46 private CacheDispatcher mDispatcher; 47 private @Mock BlockingQueue<Request<?>> mCacheQueue; 48 private @Mock BlockingQueue<Request<?>> mNetworkQueue; 49 private @Mock Cache mCache; 50 private @Mock ResponseDelivery mDelivery; 51 private @Mock Network mNetwork; 52 private StringRequest mRequest; 53 54 @Before setUp()55 public void setUp() throws Exception { 56 initMocks(this); 57 58 mRequest = new StringRequest(Request.Method.GET, "http://foo", null, null); 59 mDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery); 60 } 61 62 private static class WaitForever implements Answer { 63 @Override answer(InvocationOnMock invocationOnMock)64 public Object answer(InvocationOnMock invocationOnMock) throws Throwable { 65 Thread.sleep(Long.MAX_VALUE); 66 return null; 67 } 68 } 69 70 @Test runStopsOnQuit()71 public void runStopsOnQuit() throws Exception { 72 when(mCacheQueue.take()).then(new WaitForever()); 73 mDispatcher.start(); 74 mDispatcher.quit(); 75 mDispatcher.join(1000); 76 } 77 verifyNoResponse(ResponseDelivery delivery)78 private static void verifyNoResponse(ResponseDelivery delivery) { 79 verify(delivery, never()).postResponse(any(Request.class), any(Response.class)); 80 verify(delivery, never()) 81 .postResponse(any(Request.class), any(Response.class), any(Runnable.class)); 82 verify(delivery, never()).postError(any(Request.class), any(VolleyError.class)); 83 } 84 85 // A cancelled request should not be processed at all. 86 @Test cancelledRequest()87 public void cancelledRequest() throws Exception { 88 mRequest.cancel(); 89 mDispatcher.processRequest(mRequest); 90 verify(mCache, never()).get(anyString()); 91 verifyNoResponse(mDelivery); 92 } 93 94 // A cache miss does not post a response and puts the request on the network queue. 95 @Test cacheMiss()96 public void cacheMiss() throws Exception { 97 mDispatcher.processRequest(mRequest); 98 verifyNoResponse(mDelivery); 99 verify(mNetworkQueue).put(mRequest); 100 assertNull(mRequest.getCacheEntry()); 101 } 102 103 // A non-expired cache hit posts a response and does not queue to the network. 104 @Test nonExpiredCacheHit()105 public void nonExpiredCacheHit() throws Exception { 106 Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false); 107 when(mCache.get(anyString())).thenReturn(entry); 108 mDispatcher.processRequest(mRequest); 109 verify(mDelivery).postResponse(any(Request.class), any(Response.class)); 110 verify(mDelivery, never()).postError(any(Request.class), any(VolleyError.class)); 111 } 112 113 // A soft-expired cache hit posts a response and queues to the network. 114 @Test softExpiredCacheHit()115 public void softExpiredCacheHit() throws Exception { 116 Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true); 117 when(mCache.get(anyString())).thenReturn(entry); 118 mDispatcher.processRequest(mRequest); 119 120 // Soft expiration needs to use the deferred Runnable variant of postResponse, 121 // so make sure it gets to run. 122 ArgumentCaptor<Runnable> runnable = ArgumentCaptor.forClass(Runnable.class); 123 verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture()); 124 runnable.getValue().run(); 125 // This way we can verify the behavior of the Runnable as well. 126 verify(mNetworkQueue).put(mRequest); 127 assertSame(entry, mRequest.getCacheEntry()); 128 129 verify(mDelivery, never()).postError(any(Request.class), any(VolleyError.class)); 130 } 131 132 // An expired cache hit does not post a response and queues to the network. 133 @Test expiredCacheHit()134 public void expiredCacheHit() throws Exception { 135 Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, true, true); 136 when(mCache.get(anyString())).thenReturn(entry); 137 mDispatcher.processRequest(mRequest); 138 verifyNoResponse(mDelivery); 139 verify(mNetworkQueue).put(mRequest); 140 assertSame(entry, mRequest.getCacheEntry()); 141 } 142 143 // An fresh cache hit with parse error, does not post a response and queues to the network. 144 @Test freshCacheHit_parseError()145 public void freshCacheHit_parseError() throws Exception { 146 Request request = mock(Request.class); 147 when(request.parseNetworkResponse(any(NetworkResponse.class))) 148 .thenReturn(Response.error(new ParseError())); 149 when(request.getCacheKey()).thenReturn("cache/key"); 150 Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false); 151 when(mCache.get(anyString())).thenReturn(entry); 152 153 mDispatcher.processRequest(request); 154 155 verifyNoResponse(mDelivery); 156 verify(mNetworkQueue).put(request); 157 assertNull(request.getCacheEntry()); 158 verify(mCache).invalidate("cache/key", true); 159 verify(request).addMarker("cache-parsing-failed"); 160 } 161 162 @Test duplicateCacheMiss()163 public void duplicateCacheMiss() throws Exception { 164 StringRequest secondRequest = 165 new StringRequest(Request.Method.GET, "http://foo", null, null); 166 mRequest.setSequence(1); 167 secondRequest.setSequence(2); 168 mDispatcher.processRequest(mRequest); 169 mDispatcher.processRequest(secondRequest); 170 verify(mNetworkQueue).put(mRequest); 171 verifyNoResponse(mDelivery); 172 } 173 174 @Test tripleCacheMiss_networkErrorOnFirst()175 public void tripleCacheMiss_networkErrorOnFirst() throws Exception { 176 StringRequest secondRequest = 177 new StringRequest(Request.Method.GET, "http://foo", null, null); 178 StringRequest thirdRequest = 179 new StringRequest(Request.Method.GET, "http://foo", null, null); 180 mRequest.setSequence(1); 181 secondRequest.setSequence(2); 182 thirdRequest.setSequence(3); 183 mDispatcher.processRequest(mRequest); 184 mDispatcher.processRequest(secondRequest); 185 mDispatcher.processRequest(thirdRequest); 186 187 verify(mNetworkQueue).put(mRequest); 188 verifyNoResponse(mDelivery); 189 190 ((Request<?>) mRequest).notifyListenerResponseNotUsable(); 191 // Second request should now be in network queue. 192 verify(mNetworkQueue).put(secondRequest); 193 // Another unusable response, third request should now be added. 194 ((Request<?>) secondRequest).notifyListenerResponseNotUsable(); 195 verify(mNetworkQueue).put(thirdRequest); 196 } 197 198 @Test duplicateSoftExpiredCacheHit_failedRequest()199 public void duplicateSoftExpiredCacheHit_failedRequest() throws Exception { 200 Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true); 201 when(mCache.get(anyString())).thenReturn(entry); 202 203 StringRequest secondRequest = 204 new StringRequest(Request.Method.GET, "http://foo", null, null); 205 mRequest.setSequence(1); 206 secondRequest.setSequence(2); 207 208 mDispatcher.processRequest(mRequest); 209 mDispatcher.processRequest(secondRequest); 210 211 // Soft expiration needs to use the deferred Runnable variant of postResponse, 212 // so make sure it gets to run. 213 ArgumentCaptor<Runnable> runnable = ArgumentCaptor.forClass(Runnable.class); 214 verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture()); 215 runnable.getValue().run(); 216 // This way we can verify the behavior of the Runnable as well. 217 218 verify(mNetworkQueue).put(mRequest); 219 verify(mDelivery) 220 .postResponse(any(Request.class), any(Response.class), any(Runnable.class)); 221 222 ((Request<?>) mRequest).notifyListenerResponseNotUsable(); 223 // Second request should now be in network queue. 224 verify(mNetworkQueue).put(secondRequest); 225 } 226 227 @Test duplicateSoftExpiredCacheHit_successfulRequest()228 public void duplicateSoftExpiredCacheHit_successfulRequest() throws Exception { 229 Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true); 230 when(mCache.get(anyString())).thenReturn(entry); 231 232 StringRequest secondRequest = 233 new StringRequest(Request.Method.GET, "http://foo", null, null); 234 mRequest.setSequence(1); 235 secondRequest.setSequence(2); 236 237 mDispatcher.processRequest(mRequest); 238 mDispatcher.processRequest(secondRequest); 239 240 // Soft expiration needs to use the deferred Runnable variant of postResponse, 241 // so make sure it gets to run. 242 ArgumentCaptor<Runnable> runnable = ArgumentCaptor.forClass(Runnable.class); 243 verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture()); 244 runnable.getValue().run(); 245 // This way we can verify the behavior of the Runnable as well. 246 247 verify(mNetworkQueue).put(mRequest); 248 verify(mDelivery) 249 .postResponse(any(Request.class), any(Response.class), any(Runnable.class)); 250 251 ((Request<?>) mRequest).notifyListenerResponseReceived(Response.success(null, entry)); 252 // Second request should have delivered response. 253 verify(mNetworkQueue, never()).put(secondRequest); 254 verify(mDelivery) 255 .postResponse(any(Request.class), any(Response.class), any(Runnable.class)); 256 } 257 258 @Test processRequestNotifiesListener()259 public void processRequestNotifiesListener() throws Exception { 260 RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); 261 RequestQueue queue = new RequestQueue(mCache, mNetwork, 0, mDelivery); 262 queue.addRequestEventListener(listener); 263 mRequest.setRequestQueue(queue); 264 265 Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false); 266 when(mCache.get(anyString())).thenReturn(entry); 267 mDispatcher.processRequest(mRequest); 268 269 InOrder inOrder = inOrder(listener); 270 inOrder.verify(listener) 271 .onRequestEvent(mRequest, RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_STARTED); 272 inOrder.verify(listener) 273 .onRequestEvent(mRequest, RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED); 274 inOrder.verifyNoMoreInteractions(); 275 } 276 } 277