Home | History | Annotate | Download | only in toolbox
      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