blob: 3d8d1f17fe42d7018abcd1139bd4414f10b4aa9f [file] [log] [blame]
/*
* Copyright (C) 2013 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.toolbox;
import com.android.volley.Cache;
import com.android.volley.toolbox.DiskBasedCache.CacheHeader;
import com.android.volley.toolbox.DiskBasedCache.CountingInputStream;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import static org.hamcrest.Matchers.arrayWithSize;
import static org.hamcrest.Matchers.emptyArray;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@RunWith(RobolectricTestRunner.class)
@Config(manifest="src/main/AndroidManifest.xml", sdk=16)
public class DiskBasedCacheTest {
private static final int MAX_SIZE = 1024 * 1024;
private Cache cache;
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Rule
public ExpectedException exception = ExpectedException.none();
@Before
public void setup() throws IOException {
// Initialize empty cache
cache = new DiskBasedCache(temporaryFolder.getRoot(), MAX_SIZE);
cache.initialize();
}
@After
public void teardown() {
cache = null;
}
@Test
public void testEmptyInitialize() {
assertThat(cache.get("key"), is(nullValue()));
}
@Test
public void testPutGetZeroBytes() {
Cache.Entry entry = new Cache.Entry();
entry.data = new byte[0];
entry.serverDate = 1234567L;
entry.lastModified = 13572468L;
entry.ttl = 9876543L;
entry.softTtl = 8765432L;
entry.etag = "etag";
entry.responseHeaders = new HashMap<>();
entry.responseHeaders.put("fruit", "banana");
entry.responseHeaders.put("color", "yellow");
cache.put("my-magical-key", entry);
assertThatEntriesAreEqual(cache.get("my-magical-key"), entry);
assertThat(cache.get("unknown-key"), is(nullValue()));
}
@Test
public void testPutRemoveGet() {
Cache.Entry entry = randomData(511);
cache.put("key", entry);
assertThatEntriesAreEqual(cache.get("key"), entry);
cache.remove("key");
assertThat(cache.get("key"), is(nullValue()));
assertThat(listCachedFiles(), is(emptyArray()));
}
@Test
public void testPutClearGet() {
Cache.Entry entry = randomData(511);
cache.put("key", entry);
assertThatEntriesAreEqual(cache.get("key"), entry);
cache.clear();
assertThat(cache.get("key"), is(nullValue()));
assertThat(listCachedFiles(), is(emptyArray()));
}
@Test
public void testReinitialize() {
Cache.Entry entry = randomData(1023);
cache.put("key", entry);
Cache copy = new DiskBasedCache(temporaryFolder.getRoot(), MAX_SIZE);
copy.initialize();
assertThatEntriesAreEqual(copy.get("key"), entry);
}
@Test
public void testInvalidate() {
Cache.Entry entry = randomData(32);
entry.softTtl = 8765432L;
entry.ttl = 9876543L;
cache.put("key", entry);
cache.invalidate("key", false);
entry.softTtl = 0; // expired
assertThatEntriesAreEqual(cache.get("key"), entry);
}
@Test
public void testInvalidateFullExpire() {
Cache.Entry entry = randomData(32);
entry.softTtl = 8765432L;
entry.ttl = 9876543L;
cache.put("key", entry);
cache.invalidate("key", true);
entry.softTtl = 0; // expired
entry.ttl = 0; // expired
assertThatEntriesAreEqual(cache.get("key"), entry);
}
@Test
public void testTrim() {
Cache.Entry entry = randomData(2 * MAX_SIZE);
cache.put("oversize", entry);
assertThatEntriesAreEqual(cache.get("oversize"), entry);
entry = randomData(1024);
cache.put("kilobyte", entry);
assertThat(cache.get("oversize"), is(nullValue()));
assertThatEntriesAreEqual(cache.get("kilobyte"), entry);
Cache.Entry entry2 = randomData(1024);
cache.put("kilobyte2", entry2);
Cache.Entry entry3 = randomData(1024);
cache.put("kilobyte3", entry3);
assertThatEntriesAreEqual(cache.get("kilobyte"), entry);
assertThatEntriesAreEqual(cache.get("kilobyte2"), entry2);
assertThatEntriesAreEqual(cache.get("kilobyte3"), entry3);
entry = randomData(MAX_SIZE);
cache.put("max", entry);
assertThat(cache.get("kilobyte"), is(nullValue()));
assertThat(cache.get("kilobyte2"), is(nullValue()));
assertThat(cache.get("kilobyte3"), is(nullValue()));
assertThatEntriesAreEqual(cache.get("max"), entry);
}
@Test
@SuppressWarnings("TryFinallyCanBeTryWithResources")
public void testGetBadMagic() throws IOException {
// Cache something
Cache.Entry entry = randomData(1023);
cache.put("key", entry);
assertThatEntriesAreEqual(cache.get("key"), entry);
// Overwrite the magic header
File cacheFolder = temporaryFolder.getRoot();
File file = cacheFolder.listFiles()[0];
FileOutputStream fos = new FileOutputStream(file);
try {
DiskBasedCache.writeInt(fos, 0); // overwrite magic
} finally {
//noinspection ThrowFromFinallyBlock
fos.close();
}
assertThat(cache.get("key"), is(nullValue()));
assertThat(listCachedFiles(), is(emptyArray()));
}
@Test
@SuppressWarnings("TryFinallyCanBeTryWithResources")
public void testGetWrongKey() throws IOException {
// Cache something
Cache.Entry entry = randomData(1023);
cache.put("key", entry);
assertThatEntriesAreEqual(cache.get("key"), entry);
// Access the cached file
File cacheFolder = temporaryFolder.getRoot();
File file = cacheFolder.listFiles()[0];
FileOutputStream fos = new FileOutputStream(file);
try {
// Overwrite with a different key
CacheHeader wrongHeader = new CacheHeader("bad", entry);
wrongHeader.writeHeader(fos);
} finally {
//noinspection ThrowFromFinallyBlock
fos.close();
}
// key is gone, but file is still there
assertThat(cache.get("key"), is(nullValue()));
assertThat(listCachedFiles(), is(arrayWithSize(1)));
// Note: file is now a zombie because its key does not map to its name
}
@Test
public void testStreamToBytesNegativeLength() throws IOException {
byte[] data = new byte[1];
CountingInputStream cis =
new CountingInputStream(new ByteArrayInputStream(data), data.length);
exception.expect(IOException.class);
DiskBasedCache.streamToBytes(cis, -1);
}
@Test
public void testStreamToBytesExcessiveLength() throws IOException {
byte[] data = new byte[1];
CountingInputStream cis =
new CountingInputStream(new ByteArrayInputStream(data), data.length);
exception.expect(IOException.class);
DiskBasedCache.streamToBytes(cis, 2);
}
@Test
public void testStreamToBytesOverflow() throws IOException {
byte[] data = new byte[0];
CountingInputStream cis =
new CountingInputStream(new ByteArrayInputStream(data), 0x100000000L);
exception.expect(IOException.class);
DiskBasedCache.streamToBytes(cis, 0x100000000L); // int value is 0
}
@Test
public void testFileIsDeletedWhenWriteHeaderFails() throws IOException {
// Create DataOutputStream that throws IOException
OutputStream mockedOutputStream = spy(OutputStream.class);
doThrow(IOException.class).when(mockedOutputStream).write(anyInt());
// Create read-only copy that fails to write anything
DiskBasedCache readonly = spy((DiskBasedCache) cache);
doReturn(mockedOutputStream).when(readonly).createOutputStream(any(File.class));
// Attempt to write
readonly.put("key", randomData(1111));
// write is called at least once because each linked stream flushes when closed
verify(mockedOutputStream, atLeastOnce()).write(anyInt());
assertThat(readonly.get("key"), is(nullValue()));
assertThat(listCachedFiles(), is(emptyArray()));
// Note: original cache will try (without success) to read from file
assertThat(cache.get("key"), is(nullValue()));
}
@Test
public void testIOExceptionInInitialize() throws IOException {
// Cache a few kilobytes
cache.put("kilobyte", randomData(1024));
cache.put("kilobyte2", randomData(1024));
cache.put("kilobyte3", randomData(1024));
// Create DataInputStream that throws IOException
InputStream mockedInputStream = spy(InputStream.class);
//noinspection ResultOfMethodCallIgnored
doThrow(IOException.class).when(mockedInputStream).read();
// Create broken cache that fails to read anything
DiskBasedCache broken =
spy(new DiskBasedCache(temporaryFolder.getRoot()));
doReturn(mockedInputStream).when(broken).createInputStream(any(File.class));
// Attempt to initialize
broken.initialize();
// Everything is gone
assertThat(broken.get("kilobyte"), is(nullValue()));
assertThat(broken.get("kilobyte2"), is(nullValue()));
assertThat(broken.get("kilobyte3"), is(nullValue()));
assertThat(listCachedFiles(), is(emptyArray()));
// Verify that original cache can cope with missing files
assertThat(cache.get("kilobyte"), is(nullValue()));
assertThat(cache.get("kilobyte2"), is(nullValue()));
assertThat(cache.get("kilobyte3"), is(nullValue()));
}
@Test
public void testManyResponseHeaders() {
Cache.Entry entry = new Cache.Entry();
entry.data = new byte[0];
entry.responseHeaders = new HashMap<>();
for (int i = 0; i < 0xFFFF; i++) {
entry.responseHeaders.put(Integer.toString(i), "");
}
cache.put("key", entry);
}
@Test
@SuppressWarnings("TryFinallyCanBeTryWithResources")
public void testCountingInputStreamByteCount() throws IOException {
// Write some bytes
ByteArrayOutputStream out = new ByteArrayOutputStream();
//noinspection ThrowFromFinallyBlock
try {
DiskBasedCache.writeInt(out, 1);
DiskBasedCache.writeLong(out, -1L);
DiskBasedCache.writeString(out, "hamburger");
} finally {
//noinspection ThrowFromFinallyBlock
out.close();
}
long bytesWritten = out.size();
// Read the bytes and compare the counts
CountingInputStream cis =
new CountingInputStream(new ByteArrayInputStream(out.toByteArray()), bytesWritten);
try {
assertThat(cis.bytesRemaining(), is(bytesWritten));
assertThat(cis.bytesRead(), is(0L));
assertThat(DiskBasedCache.readInt(cis), is(1));
assertThat(DiskBasedCache.readLong(cis), is(-1L));
assertThat(DiskBasedCache.readString(cis), is("hamburger"));
assertThat(cis.bytesRead(), is(bytesWritten));
assertThat(cis.bytesRemaining(), is(0L));
} finally {
//noinspection ThrowFromFinallyBlock
cis.close();
}
}
/* Serialization tests */
@Test public void testEmptyReadThrowsEOF() throws IOException {
ByteArrayInputStream empty = new ByteArrayInputStream(new byte[]{});
exception.expect(EOFException.class);
DiskBasedCache.readInt(empty);
}
@Test public void serializeInt() throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DiskBasedCache.writeInt(baos, 0);
DiskBasedCache.writeInt(baos, 19791214);
DiskBasedCache.writeInt(baos, -20050711);
DiskBasedCache.writeInt(baos, Integer.MIN_VALUE);
DiskBasedCache.writeInt(baos, Integer.MAX_VALUE);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
assertEquals(DiskBasedCache.readInt(bais), 0);
assertEquals(DiskBasedCache.readInt(bais), 19791214);
assertEquals(DiskBasedCache.readInt(bais), -20050711);
assertEquals(DiskBasedCache.readInt(bais), Integer.MIN_VALUE);
assertEquals(DiskBasedCache.readInt(bais), Integer.MAX_VALUE);
}
@Test public void serializeLong() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DiskBasedCache.writeLong(baos, 0);
DiskBasedCache.writeLong(baos, 31337);
DiskBasedCache.writeLong(baos, -4160);
DiskBasedCache.writeLong(baos, 4295032832L);
DiskBasedCache.writeLong(baos, -4314824046L);
DiskBasedCache.writeLong(baos, Long.MIN_VALUE);
DiskBasedCache.writeLong(baos, Long.MAX_VALUE);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
assertEquals(DiskBasedCache.readLong(bais), 0);
assertEquals(DiskBasedCache.readLong(bais), 31337);
assertEquals(DiskBasedCache.readLong(bais), -4160);
assertEquals(DiskBasedCache.readLong(bais), 4295032832L);
assertEquals(DiskBasedCache.readLong(bais), -4314824046L);
assertEquals(DiskBasedCache.readLong(bais), Long.MIN_VALUE);
assertEquals(DiskBasedCache.readLong(bais), Long.MAX_VALUE);
}
@Test public void serializeString() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DiskBasedCache.writeString(baos, "");
DiskBasedCache.writeString(baos, "This is a string.");
DiskBasedCache.writeString(baos, "ファイカス");
CountingInputStream cis =
new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size());
assertEquals(DiskBasedCache.readString(cis), "");
assertEquals(DiskBasedCache.readString(cis), "This is a string.");
assertEquals(DiskBasedCache.readString(cis), "ファイカス");
}
@Test public void serializeMap() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Map<String, String> empty = new HashMap<>();
DiskBasedCache.writeStringStringMap(empty, baos);
DiskBasedCache.writeStringStringMap(null, baos);
Map<String, String> twoThings = new HashMap<>();
twoThings.put("first", "thing");
twoThings.put("second", "item");
DiskBasedCache.writeStringStringMap(twoThings, baos);
Map<String, String> emptyKey = new HashMap<>();
emptyKey.put("", "value");
DiskBasedCache.writeStringStringMap(emptyKey, baos);
Map<String, String> emptyValue = new HashMap<>();
emptyValue.put("key", "");
DiskBasedCache.writeStringStringMap(emptyValue, baos);
CountingInputStream cis =
new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size());
assertEquals(DiskBasedCache.readStringStringMap(cis), empty);
assertEquals(DiskBasedCache.readStringStringMap(cis), empty); // null reads back empty
assertEquals(DiskBasedCache.readStringStringMap(cis), twoThings);
assertEquals(DiskBasedCache.readStringStringMap(cis), emptyKey);
assertEquals(DiskBasedCache.readStringStringMap(cis), emptyValue);
}
@Test
public void publicMethods() throws Exception {
// Catch-all test to find API-breaking changes.
assertNotNull(DiskBasedCache.class.getConstructor(File.class, int.class));
assertNotNull(DiskBasedCache.class.getConstructor(File.class));
assertNotNull(DiskBasedCache.class.getMethod("getFileForKey", String.class));
}
/* Test helpers */
private void assertThatEntriesAreEqual(Cache.Entry actual, Cache.Entry expected) {
assertThat(actual.data, is(equalTo(expected.data)));
assertThat(actual.etag, is(equalTo(expected.etag)));
assertThat(actual.lastModified, is(equalTo(expected.lastModified)));
assertThat(actual.responseHeaders, is(equalTo(expected.responseHeaders)));
assertThat(actual.serverDate, is(equalTo(expected.serverDate)));
assertThat(actual.softTtl, is(equalTo(expected.softTtl)));
assertThat(actual.ttl, is(equalTo(expected.ttl)));
}
private Cache.Entry randomData(int length) {
Cache.Entry entry = new Cache.Entry();
byte[] data = new byte[length];
new Random(42).nextBytes(data); // explicit seed for reproducible results
entry.data = data;
return entry;
}
private File[] listCachedFiles() {
return temporaryFolder.getRoot().listFiles();
}
}