1 /* 2 * Copyright (C) 2013 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.toolbox; 18 19 import com.android.volley.Cache; 20 import com.android.volley.Header; 21 import com.android.volley.toolbox.DiskBasedCache.CacheHeader; 22 import com.android.volley.toolbox.DiskBasedCache.CountingInputStream; 23 24 import org.junit.After; 25 import org.junit.Before; 26 import org.junit.Rule; 27 import org.junit.Test; 28 import org.junit.rules.ExpectedException; 29 import org.junit.rules.TemporaryFolder; 30 import org.junit.runner.RunWith; 31 import org.robolectric.RobolectricTestRunner; 32 import org.robolectric.annotation.Config; 33 34 import java.io.ByteArrayInputStream; 35 import java.io.ByteArrayOutputStream; 36 import java.io.EOFException; 37 import java.io.File; 38 import java.io.FileOutputStream; 39 import java.io.IOException; 40 import java.io.InputStream; 41 import java.io.OutputStream; 42 import java.util.ArrayList; 43 import java.util.HashMap; 44 import java.util.List; 45 import java.util.Map; 46 import java.util.Random; 47 48 import static org.hamcrest.Matchers.arrayWithSize; 49 import static org.hamcrest.Matchers.emptyArray; 50 import static org.hamcrest.Matchers.equalTo; 51 import static org.hamcrest.Matchers.is; 52 import static org.hamcrest.Matchers.nullValue; 53 import static org.junit.Assert.assertEquals; 54 import static org.junit.Assert.assertNotNull; 55 import static org.junit.Assert.assertThat; 56 import static org.mockito.ArgumentMatchers.any; 57 import static org.mockito.ArgumentMatchers.anyInt; 58 import static org.mockito.Mockito.atLeastOnce; 59 import static org.mockito.Mockito.doReturn; 60 import static org.mockito.Mockito.doThrow; 61 import static org.mockito.Mockito.spy; 62 import static org.mockito.Mockito.verify; 63 64 @RunWith(RobolectricTestRunner.class) 65 @Config(manifest="src/main/AndroidManifest.xml", sdk=16) 66 public class DiskBasedCacheTest { 67 68 private static final int MAX_SIZE = 1024 * 1024; 69 70 private Cache cache; 71 72 @Rule 73 public TemporaryFolder temporaryFolder = new TemporaryFolder(); 74 75 @Rule 76 public ExpectedException exception = ExpectedException.none(); 77 78 @Before 79 public void setup() throws IOException { 80 // Initialize empty cache 81 cache = new DiskBasedCache(temporaryFolder.getRoot(), MAX_SIZE); 82 cache.initialize(); 83 } 84 85 @After 86 public void teardown() { 87 cache = null; 88 } 89 90 @Test 91 public void testEmptyInitialize() { 92 assertThat(cache.get("key"), is(nullValue())); 93 } 94 95 @Test 96 public void testPutGetZeroBytes() { 97 Cache.Entry entry = new Cache.Entry(); 98 entry.data = new byte[0]; 99 entry.serverDate = 1234567L; 100 entry.lastModified = 13572468L; 101 entry.ttl = 9876543L; 102 entry.softTtl = 8765432L; 103 entry.etag = "etag"; 104 entry.responseHeaders = new HashMap<>(); 105 entry.responseHeaders.put("fruit", "banana"); 106 entry.responseHeaders.put("color", "yellow"); 107 cache.put("my-magical-key", entry); 108 109 assertThatEntriesAreEqual(cache.get("my-magical-key"), entry); 110 assertThat(cache.get("unknown-key"), is(nullValue())); 111 } 112 113 @Test 114 public void testPutRemoveGet() { 115 Cache.Entry entry = randomData(511); 116 cache.put("key", entry); 117 118 assertThatEntriesAreEqual(cache.get("key"), entry); 119 120 cache.remove("key"); 121 assertThat(cache.get("key"), is(nullValue())); 122 assertThat(listCachedFiles(), is(emptyArray())); 123 } 124 125 @Test 126 public void testPutClearGet() { 127 Cache.Entry entry = randomData(511); 128 cache.put("key", entry); 129 130 assertThatEntriesAreEqual(cache.get("key"), entry); 131 132 cache.clear(); 133 assertThat(cache.get("key"), is(nullValue())); 134 assertThat(listCachedFiles(), is(emptyArray())); 135 } 136 137 @Test 138 public void testReinitialize() { 139 Cache.Entry entry = randomData(1023); 140 cache.put("key", entry); 141 142 Cache copy = new DiskBasedCache(temporaryFolder.getRoot(), MAX_SIZE); 143 copy.initialize(); 144 145 assertThatEntriesAreEqual(copy.get("key"), entry); 146 } 147 148 @Test 149 public void testInvalidate() { 150 Cache.Entry entry = randomData(32); 151 entry.softTtl = 8765432L; 152 entry.ttl = 9876543L; 153 cache.put("key", entry); 154 155 cache.invalidate("key", false); 156 entry.softTtl = 0; // expired 157 assertThatEntriesAreEqual(cache.get("key"), entry); 158 } 159 160 @Test 161 public void testInvalidateFullExpire() { 162 Cache.Entry entry = randomData(32); 163 entry.softTtl = 8765432L; 164 entry.ttl = 9876543L; 165 cache.put("key", entry); 166 167 cache.invalidate("key", true); 168 entry.softTtl = 0; // expired 169 entry.ttl = 0; // expired 170 assertThatEntriesAreEqual(cache.get("key"), entry); 171 } 172 173 @Test 174 public void testTrim() { 175 Cache.Entry entry = randomData(2 * MAX_SIZE); 176 cache.put("oversize", entry); 177 178 assertThatEntriesAreEqual(cache.get("oversize"), entry); 179 180 entry = randomData(1024); 181 cache.put("kilobyte", entry); 182 183 assertThat(cache.get("oversize"), is(nullValue())); 184 assertThatEntriesAreEqual(cache.get("kilobyte"), entry); 185 186 Cache.Entry entry2 = randomData(1024); 187 cache.put("kilobyte2", entry2); 188 Cache.Entry entry3 = randomData(1024); 189 cache.put("kilobyte3", entry3); 190 191 assertThatEntriesAreEqual(cache.get("kilobyte"), entry); 192 assertThatEntriesAreEqual(cache.get("kilobyte2"), entry2); 193 assertThatEntriesAreEqual(cache.get("kilobyte3"), entry3); 194 195 entry = randomData(MAX_SIZE); 196 cache.put("max", entry); 197 198 assertThat(cache.get("kilobyte"), is(nullValue())); 199 assertThat(cache.get("kilobyte2"), is(nullValue())); 200 assertThat(cache.get("kilobyte3"), is(nullValue())); 201 assertThatEntriesAreEqual(cache.get("max"), entry); 202 } 203 204 @Test 205 @SuppressWarnings("TryFinallyCanBeTryWithResources") 206 public void testGetBadMagic() throws IOException { 207 // Cache something 208 Cache.Entry entry = randomData(1023); 209 cache.put("key", entry); 210 assertThatEntriesAreEqual(cache.get("key"), entry); 211 212 // Overwrite the magic header 213 File cacheFolder = temporaryFolder.getRoot(); 214 File file = cacheFolder.listFiles()[0]; 215 FileOutputStream fos = new FileOutputStream(file); 216 try { 217 DiskBasedCache.writeInt(fos, 0); // overwrite magic 218 } finally { 219 //noinspection ThrowFromFinallyBlock 220 fos.close(); 221 } 222 223 assertThat(cache.get("key"), is(nullValue())); 224 assertThat(listCachedFiles(), is(emptyArray())); 225 } 226 227 @Test 228 @SuppressWarnings("TryFinallyCanBeTryWithResources") 229 public void testGetWrongKey() throws IOException { 230 // Cache something 231 Cache.Entry entry = randomData(1023); 232 cache.put("key", entry); 233 assertThatEntriesAreEqual(cache.get("key"), entry); 234 235 // Access the cached file 236 File cacheFolder = temporaryFolder.getRoot(); 237 File file = cacheFolder.listFiles()[0]; 238 FileOutputStream fos = new FileOutputStream(file); 239 try { 240 // Overwrite with a different key 241 CacheHeader wrongHeader = new CacheHeader("bad", entry); 242 wrongHeader.writeHeader(fos); 243 } finally { 244 //noinspection ThrowFromFinallyBlock 245 fos.close(); 246 } 247 248 // key is gone, but file is still there 249 assertThat(cache.get("key"), is(nullValue())); 250 assertThat(listCachedFiles(), is(arrayWithSize(1))); 251 252 // Note: file is now a zombie because its key does not map to its name 253 } 254 255 @Test 256 public void testStreamToBytesNegativeLength() throws IOException { 257 byte[] data = new byte[1]; 258 CountingInputStream cis = 259 new CountingInputStream(new ByteArrayInputStream(data), data.length); 260 exception.expect(IOException.class); 261 DiskBasedCache.streamToBytes(cis, -1); 262 } 263 264 @Test 265 public void testStreamToBytesExcessiveLength() throws IOException { 266 byte[] data = new byte[1]; 267 CountingInputStream cis = 268 new CountingInputStream(new ByteArrayInputStream(data), data.length); 269 exception.expect(IOException.class); 270 DiskBasedCache.streamToBytes(cis, 2); 271 } 272 273 @Test 274 public void testStreamToBytesOverflow() throws IOException { 275 byte[] data = new byte[0]; 276 CountingInputStream cis = 277 new CountingInputStream(new ByteArrayInputStream(data), 0x100000000L); 278 exception.expect(IOException.class); 279 DiskBasedCache.streamToBytes(cis, 0x100000000L); // int value is 0 280 } 281 282 @Test 283 public void testFileIsDeletedWhenWriteHeaderFails() throws IOException { 284 // Create DataOutputStream that throws IOException 285 OutputStream mockedOutputStream = spy(OutputStream.class); 286 doThrow(IOException.class).when(mockedOutputStream).write(anyInt()); 287 288 // Create read-only copy that fails to write anything 289 DiskBasedCache readonly = spy((DiskBasedCache) cache); 290 doReturn(mockedOutputStream).when(readonly).createOutputStream(any(File.class)); 291 292 // Attempt to write 293 readonly.put("key", randomData(1111)); 294 295 // write is called at least once because each linked stream flushes when closed 296 verify(mockedOutputStream, atLeastOnce()).write(anyInt()); 297 assertThat(readonly.get("key"), is(nullValue())); 298 assertThat(listCachedFiles(), is(emptyArray())); 299 300 // Note: original cache will try (without success) to read from file 301 assertThat(cache.get("key"), is(nullValue())); 302 } 303 304 @Test 305 public void testIOExceptionInInitialize() throws IOException { 306 // Cache a few kilobytes 307 cache.put("kilobyte", randomData(1024)); 308 cache.put("kilobyte2", randomData(1024)); 309 cache.put("kilobyte3", randomData(1024)); 310 311 // Create DataInputStream that throws IOException 312 InputStream mockedInputStream = spy(InputStream.class); 313 //noinspection ResultOfMethodCallIgnored 314 doThrow(IOException.class).when(mockedInputStream).read(); 315 316 // Create broken cache that fails to read anything 317 DiskBasedCache broken = 318 spy(new DiskBasedCache(temporaryFolder.getRoot())); 319 doReturn(mockedInputStream).when(broken).createInputStream(any(File.class)); 320 321 // Attempt to initialize 322 broken.initialize(); 323 324 // Everything is gone 325 assertThat(broken.get("kilobyte"), is(nullValue())); 326 assertThat(broken.get("kilobyte2"), is(nullValue())); 327 assertThat(broken.get("kilobyte3"), is(nullValue())); 328 assertThat(listCachedFiles(), is(emptyArray())); 329 330 // Verify that original cache can cope with missing files 331 assertThat(cache.get("kilobyte"), is(nullValue())); 332 assertThat(cache.get("kilobyte2"), is(nullValue())); 333 assertThat(cache.get("kilobyte3"), is(nullValue())); 334 } 335 336 @Test 337 public void testManyResponseHeaders() { 338 Cache.Entry entry = new Cache.Entry(); 339 entry.data = new byte[0]; 340 entry.responseHeaders = new HashMap<>(); 341 for (int i = 0; i < 0xFFFF; i++) { 342 entry.responseHeaders.put(Integer.toString(i), ""); 343 } 344 cache.put("key", entry); 345 } 346 347 @Test 348 @SuppressWarnings("TryFinallyCanBeTryWithResources") 349 public void testCountingInputStreamByteCount() throws IOException { 350 // Write some bytes 351 ByteArrayOutputStream out = new ByteArrayOutputStream(); 352 //noinspection ThrowFromFinallyBlock 353 try { 354 DiskBasedCache.writeInt(out, 1); 355 DiskBasedCache.writeLong(out, -1L); 356 DiskBasedCache.writeString(out, "hamburger"); 357 } finally { 358 //noinspection ThrowFromFinallyBlock 359 out.close(); 360 } 361 long bytesWritten = out.size(); 362 363 // Read the bytes and compare the counts 364 CountingInputStream cis = 365 new CountingInputStream(new ByteArrayInputStream(out.toByteArray()), bytesWritten); 366 try { 367 assertThat(cis.bytesRemaining(), is(bytesWritten)); 368 assertThat(cis.bytesRead(), is(0L)); 369 assertThat(DiskBasedCache.readInt(cis), is(1)); 370 assertThat(DiskBasedCache.readLong(cis), is(-1L)); 371 assertThat(DiskBasedCache.readString(cis), is("hamburger")); 372 assertThat(cis.bytesRead(), is(bytesWritten)); 373 assertThat(cis.bytesRemaining(), is(0L)); 374 } finally { 375 //noinspection ThrowFromFinallyBlock 376 cis.close(); 377 } 378 } 379 380 /* Serialization tests */ 381 382 @Test public void testEmptyReadThrowsEOF() throws IOException { 383 ByteArrayInputStream empty = new ByteArrayInputStream(new byte[]{}); 384 exception.expect(EOFException.class); 385 DiskBasedCache.readInt(empty); 386 } 387 388 @Test public void serializeInt() throws IOException { 389 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 390 DiskBasedCache.writeInt(baos, 0); 391 DiskBasedCache.writeInt(baos, 19791214); 392 DiskBasedCache.writeInt(baos, -20050711); 393 DiskBasedCache.writeInt(baos, Integer.MIN_VALUE); 394 DiskBasedCache.writeInt(baos, Integer.MAX_VALUE); 395 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); 396 assertEquals(DiskBasedCache.readInt(bais), 0); 397 assertEquals(DiskBasedCache.readInt(bais), 19791214); 398 assertEquals(DiskBasedCache.readInt(bais), -20050711); 399 assertEquals(DiskBasedCache.readInt(bais), Integer.MIN_VALUE); 400 assertEquals(DiskBasedCache.readInt(bais), Integer.MAX_VALUE); 401 } 402 403 @Test public void serializeLong() throws Exception { 404 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 405 DiskBasedCache.writeLong(baos, 0); 406 DiskBasedCache.writeLong(baos, 31337); 407 DiskBasedCache.writeLong(baos, -4160); 408 DiskBasedCache.writeLong(baos, 4295032832L); 409 DiskBasedCache.writeLong(baos, -4314824046L); 410 DiskBasedCache.writeLong(baos, Long.MIN_VALUE); 411 DiskBasedCache.writeLong(baos, Long.MAX_VALUE); 412 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); 413 assertEquals(DiskBasedCache.readLong(bais), 0); 414 assertEquals(DiskBasedCache.readLong(bais), 31337); 415 assertEquals(DiskBasedCache.readLong(bais), -4160); 416 assertEquals(DiskBasedCache.readLong(bais), 4295032832L); 417 assertEquals(DiskBasedCache.readLong(bais), -4314824046L); 418 assertEquals(DiskBasedCache.readLong(bais), Long.MIN_VALUE); 419 assertEquals(DiskBasedCache.readLong(bais), Long.MAX_VALUE); 420 } 421 422 @Test public void serializeString() throws Exception { 423 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 424 DiskBasedCache.writeString(baos, ""); 425 DiskBasedCache.writeString(baos, "This is a string."); 426 DiskBasedCache.writeString(baos, ""); 427 CountingInputStream cis = 428 new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size()); 429 assertEquals(DiskBasedCache.readString(cis), ""); 430 assertEquals(DiskBasedCache.readString(cis), "This is a string."); 431 assertEquals(DiskBasedCache.readString(cis), ""); 432 } 433 434 @Test public void serializeHeaders() throws Exception { 435 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 436 List<Header> empty = new ArrayList<>(); 437 DiskBasedCache.writeHeaderList(empty, baos); 438 DiskBasedCache.writeHeaderList(null, baos); 439 List<Header> twoThings = new ArrayList<>(); 440 twoThings.add(new Header("first", "thing")); 441 twoThings.add(new Header("second", "item")); 442 DiskBasedCache.writeHeaderList(twoThings, baos); 443 List<Header> emptyKey = new ArrayList<>(); 444 emptyKey.add(new Header("", "value")); 445 DiskBasedCache.writeHeaderList(emptyKey, baos); 446 List<Header> emptyValue = new ArrayList<>(); 447 emptyValue.add(new Header("key", "")); 448 DiskBasedCache.writeHeaderList(emptyValue, baos); 449 List<Header> sameKeys = new ArrayList<>(); 450 sameKeys.add(new Header("key", "value")); 451 sameKeys.add(new Header("key", "value2")); 452 DiskBasedCache.writeHeaderList(sameKeys, baos); 453 CountingInputStream cis = 454 new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size()); 455 assertEquals(DiskBasedCache.readHeaderList(cis), empty); 456 assertEquals(DiskBasedCache.readHeaderList(cis), empty); // null reads back empty 457 assertEquals(DiskBasedCache.readHeaderList(cis), twoThings); 458 assertEquals(DiskBasedCache.readHeaderList(cis), emptyKey); 459 assertEquals(DiskBasedCache.readHeaderList(cis), emptyValue); 460 assertEquals(DiskBasedCache.readHeaderList(cis), sameKeys); 461 } 462 463 @Test 464 public void publicMethods() throws Exception { 465 // Catch-all test to find API-breaking changes. 466 assertNotNull(DiskBasedCache.class.getConstructor(File.class, int.class)); 467 assertNotNull(DiskBasedCache.class.getConstructor(File.class)); 468 469 assertNotNull(DiskBasedCache.class.getMethod("getFileForKey", String.class)); 470 } 471 472 /* Test helpers */ 473 474 private void assertThatEntriesAreEqual(Cache.Entry actual, Cache.Entry expected) { 475 assertThat(actual.data, is(equalTo(expected.data))); 476 assertThat(actual.etag, is(equalTo(expected.etag))); 477 assertThat(actual.lastModified, is(equalTo(expected.lastModified))); 478 assertThat(actual.responseHeaders, is(equalTo(expected.responseHeaders))); 479 assertThat(actual.serverDate, is(equalTo(expected.serverDate))); 480 assertThat(actual.softTtl, is(equalTo(expected.softTtl))); 481 assertThat(actual.ttl, is(equalTo(expected.ttl))); 482 } 483 484 private Cache.Entry randomData(int length) { 485 Cache.Entry entry = new Cache.Entry(); 486 byte[] data = new byte[length]; 487 new Random(42).nextBytes(data); // explicit seed for reproducible results 488 entry.data = data; 489 return entry; 490 } 491 492 private File[] listCachedFiles() { 493 return temporaryFolder.getRoot().listFiles(); 494 } 495 } 496