blob: c996d202ddbdc9299d5732b7a490f9b0499a4e45 [file] [log] [blame]
/*
* 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.squareup.okhttp.internal;
import com.squareup.okhttp.internal.io.FileSystem;
import java.io.File;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.Executor;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.Okio;
import okio.Source;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.Timeout;
import static com.squareup.okhttp.internal.DiskLruCache.JOURNAL_FILE;
import static com.squareup.okhttp.internal.DiskLruCache.JOURNAL_FILE_BACKUP;
import static com.squareup.okhttp.internal.DiskLruCache.MAGIC;
import static com.squareup.okhttp.internal.DiskLruCache.VERSION_1;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public final class DiskLruCacheTest {
@Rule public final TemporaryFolder tempDir = new TemporaryFolder();
@Rule public final Timeout timeout = new Timeout(30 * 1000);
private final FaultyFileSystem fileSystem = new FaultyFileSystem(FileSystem.SYSTEM);
private final int appVersion = 100;
private File cacheDir;
private File journalFile;
private File journalBkpFile;
private final TestExecutor executor = new TestExecutor();
private DiskLruCache cache;
private final Deque<DiskLruCache> toClose = new ArrayDeque<>();
private void createNewCache() throws IOException {
createNewCacheWithSize(Integer.MAX_VALUE);
}
private void createNewCacheWithSize(int maxSize) throws IOException {
cache = new DiskLruCache(fileSystem, cacheDir, appVersion, 2, maxSize, executor);
synchronized (cache) {
cache.initialize();
}
toClose.add(cache);
}
@Before public void setUp() throws Exception {
cacheDir = tempDir.getRoot();
journalFile = new File(cacheDir, JOURNAL_FILE);
journalBkpFile = new File(cacheDir, JOURNAL_FILE_BACKUP);
createNewCache();
}
@After public void tearDown() throws Exception {
while (!toClose.isEmpty()) {
toClose.pop().close();
}
}
@Test public void emptyCache() throws Exception {
cache.close();
assertJournalEquals();
}
@Test public void validateKey() throws Exception {
String key = null;
try {
key = "has_space ";
cache.edit(key);
fail("Exepcting an IllegalArgumentException as the key was invalid.");
} catch (IllegalArgumentException iae) {
assertEquals("keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"", iae.getMessage());
}
try {
key = "has_CR\r";
cache.edit(key);
fail("Exepcting an IllegalArgumentException as the key was invalid.");
} catch (IllegalArgumentException iae) {
assertEquals("keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"", iae.getMessage());
}
try {
key = "has_LF\n";
cache.edit(key);
fail("Exepcting an IllegalArgumentException as the key was invalid.");
} catch (IllegalArgumentException iae) {
assertEquals("keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"", iae.getMessage());
}
try {
key = "has_invalid/";
cache.edit(key);
fail("Exepcting an IllegalArgumentException as the key was invalid.");
} catch (IllegalArgumentException iae) {
assertEquals("keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"", iae.getMessage());
}
try {
key = "has_invalid\u2603";
cache.edit(key);
fail("Exepcting an IllegalArgumentException as the key was invalid.");
} catch (IllegalArgumentException iae) {
assertEquals("keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"", iae.getMessage());
}
try {
key = "this_is_way_too_long_this_is_way_too_long_this_is_way_too_long_"
+ "this_is_way_too_long_this_is_way_too_long_this_is_way_too_long";
cache.edit(key);
fail("Exepcting an IllegalArgumentException as the key was too long.");
} catch (IllegalArgumentException iae) {
assertEquals("keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"", iae.getMessage());
}
// Test valid cases.
// Exactly 120.
key = "0123456789012345678901234567890123456789012345678901234567890123456789"
+ "01234567890123456789012345678901234567890123456789";
cache.edit(key).abort();
// Contains all valid characters.
key = "abcdefghijklmnopqrstuvwxyz_0123456789";
cache.edit(key).abort();
// Contains dash.
key = "-20384573948576";
cache.edit(key).abort();
}
@Test public void writeAndReadEntry() throws Exception {
DiskLruCache.Editor creator = cache.edit("k1");
setString(creator, 0, "ABC");
setString(creator, 1, "DE");
assertNull(creator.newSource(0));
assertNull(creator.newSource(1));
creator.commit();
DiskLruCache.Snapshot snapshot = cache.get("k1");
assertSnapshotValue(snapshot, 0, "ABC");
assertSnapshotValue(snapshot, 1, "DE");
}
@Test public void readAndWriteEntryAcrossCacheOpenAndClose() throws Exception {
DiskLruCache.Editor creator = cache.edit("k1");
setString(creator, 0, "A");
setString(creator, 1, "B");
creator.commit();
cache.close();
createNewCache();
DiskLruCache.Snapshot snapshot = cache.get("k1");
assertSnapshotValue(snapshot, 0, "A");
assertSnapshotValue(snapshot, 1, "B");
snapshot.close();
}
@Test public void readAndWriteEntryWithoutProperClose() throws Exception {
DiskLruCache.Editor creator = cache.edit("k1");
setString(creator, 0, "A");
setString(creator, 1, "B");
creator.commit();
// Simulate a dirty close of 'cache' by opening the cache directory again.
createNewCache();
DiskLruCache.Snapshot snapshot = cache.get("k1");
assertSnapshotValue(snapshot, 0, "A");
assertSnapshotValue(snapshot, 1, "B");
snapshot.close();
}
@Test public void journalWithEditAndPublish() throws Exception {
DiskLruCache.Editor creator = cache.edit("k1");
assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed.
setString(creator, 0, "AB");
setString(creator, 1, "C");
creator.commit();
cache.close();
assertJournalEquals("DIRTY k1", "CLEAN k1 2 1");
}
@Test public void revertedNewFileIsRemoveInJournal() throws Exception {
DiskLruCache.Editor creator = cache.edit("k1");
assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed.
setString(creator, 0, "AB");
setString(creator, 1, "C");
creator.abort();
cache.close();
assertJournalEquals("DIRTY k1", "REMOVE k1");
}
@Test public void unterminatedEditIsRevertedOnClose() throws Exception {
cache.edit("k1");
cache.close();
assertJournalEquals("DIRTY k1", "REMOVE k1");
}
@Test public void journalDoesNotIncludeReadOfYetUnpublishedValue() throws Exception {
DiskLruCache.Editor creator = cache.edit("k1");
assertNull(cache.get("k1"));
setString(creator, 0, "A");
setString(creator, 1, "BC");
creator.commit();
cache.close();
assertJournalEquals("DIRTY k1", "CLEAN k1 1 2");
}
@Test public void journalWithEditAndPublishAndRead() throws Exception {
DiskLruCache.Editor k1Creator = cache.edit("k1");
setString(k1Creator, 0, "AB");
setString(k1Creator, 1, "C");
k1Creator.commit();
DiskLruCache.Editor k2Creator = cache.edit("k2");
setString(k2Creator, 0, "DEF");
setString(k2Creator, 1, "G");
k2Creator.commit();
DiskLruCache.Snapshot k1Snapshot = cache.get("k1");
k1Snapshot.close();
cache.close();
assertJournalEquals("DIRTY k1", "CLEAN k1 2 1", "DIRTY k2", "CLEAN k2 3 1", "READ k1");
}
@Test public void cannotOperateOnEditAfterPublish() throws Exception {
DiskLruCache.Editor editor = cache.edit("k1");
setString(editor, 0, "A");
setString(editor, 1, "B");
editor.commit();
assertInoperable(editor);
}
@Test public void cannotOperateOnEditAfterRevert() throws Exception {
DiskLruCache.Editor editor = cache.edit("k1");
setString(editor, 0, "A");
setString(editor, 1, "B");
editor.abort();
assertInoperable(editor);
}
@Test public void explicitRemoveAppliedToDiskImmediately() throws Exception {
DiskLruCache.Editor editor = cache.edit("k1");
setString(editor, 0, "ABC");
setString(editor, 1, "B");
editor.commit();
File k1 = getCleanFile("k1", 0);
assertEquals("ABC", readFile(k1));
cache.remove("k1");
assertFalse(fileSystem.exists(k1));
}
@Test public void removePreventsActiveEditFromStoringAValue() throws Exception {
set("a", "a", "a");
DiskLruCache.Editor a = cache.edit("a");
setString(a, 0, "a1");
assertTrue(cache.remove("a"));
setString(a, 1, "a2");
a.commit();
assertAbsent("a");
}
/**
* Each read sees a snapshot of the file at the time read was called.
* This means that two reads of the same key can see different data.
*/
@Test public void readAndWriteOverlapsMaintainConsistency() throws Exception {
DiskLruCache.Editor v1Creator = cache.edit("k1");
setString(v1Creator, 0, "AAaa");
setString(v1Creator, 1, "BBbb");
v1Creator.commit();
DiskLruCache.Snapshot snapshot1 = cache.get("k1");
BufferedSource inV1 = Okio.buffer(snapshot1.getSource(0));
assertEquals('A', inV1.readByte());
assertEquals('A', inV1.readByte());
DiskLruCache.Editor v1Updater = cache.edit("k1");
setString(v1Updater, 0, "CCcc");
setString(v1Updater, 1, "DDdd");
v1Updater.commit();
DiskLruCache.Snapshot snapshot2 = cache.get("k1");
assertSnapshotValue(snapshot2, 0, "CCcc");
assertSnapshotValue(snapshot2, 1, "DDdd");
snapshot2.close();
assertEquals('a', inV1.readByte());
assertEquals('a', inV1.readByte());
assertSnapshotValue(snapshot1, 1, "BBbb");
snapshot1.close();
}
@Test public void openWithDirtyKeyDeletesAllFilesForThatKey() throws Exception {
cache.close();
File cleanFile0 = getCleanFile("k1", 0);
File cleanFile1 = getCleanFile("k1", 1);
File dirtyFile0 = getDirtyFile("k1", 0);
File dirtyFile1 = getDirtyFile("k1", 1);
writeFile(cleanFile0, "A");
writeFile(cleanFile1, "B");
writeFile(dirtyFile0, "C");
writeFile(dirtyFile1, "D");
createJournal("CLEAN k1 1 1", "DIRTY k1");
createNewCache();
assertFalse(fileSystem.exists(cleanFile0));
assertFalse(fileSystem.exists(cleanFile1));
assertFalse(fileSystem.exists(dirtyFile0));
assertFalse(fileSystem.exists(dirtyFile1));
assertNull(cache.get("k1"));
}
@Test public void openWithInvalidVersionClearsDirectory() throws Exception {
cache.close();
generateSomeGarbageFiles();
createJournalWithHeader(MAGIC, "0", "100", "2", "");
createNewCache();
assertGarbageFilesAllDeleted();
}
@Test public void openWithInvalidAppVersionClearsDirectory() throws Exception {
cache.close();
generateSomeGarbageFiles();
createJournalWithHeader(MAGIC, "1", "101", "2", "");
createNewCache();
assertGarbageFilesAllDeleted();
}
@Test public void openWithInvalidValueCountClearsDirectory() throws Exception {
cache.close();
generateSomeGarbageFiles();
createJournalWithHeader(MAGIC, "1", "100", "1", "");
createNewCache();
assertGarbageFilesAllDeleted();
}
@Test public void openWithInvalidBlankLineClearsDirectory() throws Exception {
cache.close();
generateSomeGarbageFiles();
createJournalWithHeader(MAGIC, "1", "100", "2", "x");
createNewCache();
assertGarbageFilesAllDeleted();
}
@Test public void openWithInvalidJournalLineClearsDirectory() throws Exception {
cache.close();
generateSomeGarbageFiles();
createJournal("CLEAN k1 1 1", "BOGUS");
createNewCache();
assertGarbageFilesAllDeleted();
assertNull(cache.get("k1"));
}
@Test public void openWithInvalidFileSizeClearsDirectory() throws Exception {
cache.close();
generateSomeGarbageFiles();
createJournal("CLEAN k1 0000x001 1");
createNewCache();
assertGarbageFilesAllDeleted();
assertNull(cache.get("k1"));
}
@Test public void openWithTruncatedLineDiscardsThatLine() throws Exception {
cache.close();
writeFile(getCleanFile("k1", 0), "A");
writeFile(getCleanFile("k1", 1), "B");
BufferedSink sink = Okio.buffer(fileSystem.sink(journalFile));
sink.writeUtf8(MAGIC + "\n" + VERSION_1 + "\n100\n2\n\nCLEAN k1 1 1"); // no trailing newline
sink.close();
createNewCache();
assertNull(cache.get("k1"));
// The journal is not corrupt when editing after a truncated line.
set("k1", "C", "D");
cache.close();
createNewCache();
assertValue("k1", "C", "D");
}
@Test public void openWithTooManyFileSizesClearsDirectory() throws Exception {
cache.close();
generateSomeGarbageFiles();
createJournal("CLEAN k1 1 1 1");
createNewCache();
assertGarbageFilesAllDeleted();
assertNull(cache.get("k1"));
}
@Test public void keyWithSpaceNotPermitted() throws Exception {
try {
cache.edit("my key");
fail();
} catch (IllegalArgumentException expected) {
}
}
@Test public void keyWithNewlineNotPermitted() throws Exception {
try {
cache.edit("my\nkey");
fail();
} catch (IllegalArgumentException expected) {
}
}
@Test public void keyWithCarriageReturnNotPermitted() throws Exception {
try {
cache.edit("my\rkey");
fail();
} catch (IllegalArgumentException expected) {
}
}
@Test public void nullKeyThrows() throws Exception {
try {
cache.edit(null);
fail();
} catch (NullPointerException expected) {
}
}
@Test public void createNewEntryWithTooFewValuesFails() throws Exception {
DiskLruCache.Editor creator = cache.edit("k1");
setString(creator, 1, "A");
try {
creator.commit();
fail();
} catch (IllegalStateException expected) {
}
assertFalse(fileSystem.exists(getCleanFile("k1", 0)));
assertFalse(fileSystem.exists(getCleanFile("k1", 1)));
assertFalse(fileSystem.exists(getDirtyFile("k1", 0)));
assertFalse(fileSystem.exists(getDirtyFile("k1", 1)));
assertNull(cache.get("k1"));
DiskLruCache.Editor creator2 = cache.edit("k1");
setString(creator2, 0, "B");
setString(creator2, 1, "C");
creator2.commit();
}
@Test public void revertWithTooFewValues() throws Exception {
DiskLruCache.Editor creator = cache.edit("k1");
setString(creator, 1, "A");
creator.abort();
assertFalse(fileSystem.exists(getCleanFile("k1", 0)));
assertFalse(fileSystem.exists(getCleanFile("k1", 1)));
assertFalse(fileSystem.exists(getDirtyFile("k1", 0)));
assertFalse(fileSystem.exists(getDirtyFile("k1", 1)));
assertNull(cache.get("k1"));
}
@Test public void updateExistingEntryWithTooFewValuesReusesPreviousValues() throws Exception {
DiskLruCache.Editor creator = cache.edit("k1");
setString(creator, 0, "A");
setString(creator, 1, "B");
creator.commit();
DiskLruCache.Editor updater = cache.edit("k1");
setString(updater, 0, "C");
updater.commit();
DiskLruCache.Snapshot snapshot = cache.get("k1");
assertSnapshotValue(snapshot, 0, "C");
assertSnapshotValue(snapshot, 1, "B");
snapshot.close();
}
@Test public void growMaxSize() throws Exception {
cache.close();
createNewCacheWithSize(10);
set("a", "a", "aaa"); // size 4
set("b", "bb", "bbbb"); // size 6
cache.setMaxSize(20);
set("c", "c", "c"); // size 12
assertEquals(12, cache.size());
}
@Test public void shrinkMaxSizeEvicts() throws Exception {
cache.close();
createNewCacheWithSize(20);
set("a", "a", "aaa"); // size 4
set("b", "bb", "bbbb"); // size 6
set("c", "c", "c"); // size 12
cache.setMaxSize(10);
assertEquals(1, executor.jobs.size());
}
@Test public void evictOnInsert() throws Exception {
cache.close();
createNewCacheWithSize(10);
set("a", "a", "aaa"); // size 4
set("b", "bb", "bbbb"); // size 6
assertEquals(10, cache.size());
// Cause the size to grow to 12 should evict 'A'.
set("c", "c", "c");
cache.flush();
assertEquals(8, cache.size());
assertAbsent("a");
assertValue("b", "bb", "bbbb");
assertValue("c", "c", "c");
// Causing the size to grow to 10 should evict nothing.
set("d", "d", "d");
cache.flush();
assertEquals(10, cache.size());
assertAbsent("a");
assertValue("b", "bb", "bbbb");
assertValue("c", "c", "c");
assertValue("d", "d", "d");
// Causing the size to grow to 18 should evict 'B' and 'C'.
set("e", "eeee", "eeee");
cache.flush();
assertEquals(10, cache.size());
assertAbsent("a");
assertAbsent("b");
assertAbsent("c");
assertValue("d", "d", "d");
assertValue("e", "eeee", "eeee");
}
@Test public void evictOnUpdate() throws Exception {
cache.close();
createNewCacheWithSize(10);
set("a", "a", "aa"); // size 3
set("b", "b", "bb"); // size 3
set("c", "c", "cc"); // size 3
assertEquals(9, cache.size());
// Causing the size to grow to 11 should evict 'A'.
set("b", "b", "bbbb");
cache.flush();
assertEquals(8, cache.size());
assertAbsent("a");
assertValue("b", "b", "bbbb");
assertValue("c", "c", "cc");
}
@Test public void evictionHonorsLruFromCurrentSession() throws Exception {
cache.close();
createNewCacheWithSize(10);
set("a", "a", "a");
set("b", "b", "b");
set("c", "c", "c");
set("d", "d", "d");
set("e", "e", "e");
cache.get("b").close(); // 'B' is now least recently used.
// Causing the size to grow to 12 should evict 'A'.
set("f", "f", "f");
// Causing the size to grow to 12 should evict 'C'.
set("g", "g", "g");
cache.flush();
assertEquals(10, cache.size());
assertAbsent("a");
assertValue("b", "b", "b");
assertAbsent("c");
assertValue("d", "d", "d");
assertValue("e", "e", "e");
assertValue("f", "f", "f");
}
@Test public void evictionHonorsLruFromPreviousSession() throws Exception {
set("a", "a", "a");
set("b", "b", "b");
set("c", "c", "c");
set("d", "d", "d");
set("e", "e", "e");
set("f", "f", "f");
cache.get("b").close(); // 'B' is now least recently used.
assertEquals(12, cache.size());
cache.close();
createNewCacheWithSize(10);
set("g", "g", "g");
cache.flush();
assertEquals(10, cache.size());
assertAbsent("a");
assertValue("b", "b", "b");
assertAbsent("c");
assertValue("d", "d", "d");
assertValue("e", "e", "e");
assertValue("f", "f", "f");
assertValue("g", "g", "g");
}
@Test public void cacheSingleEntryOfSizeGreaterThanMaxSize() throws Exception {
cache.close();
createNewCacheWithSize(10);
set("a", "aaaaa", "aaaaaa"); // size=11
cache.flush();
assertAbsent("a");
}
@Test public void cacheSingleValueOfSizeGreaterThanMaxSize() throws Exception {
cache.close();
createNewCacheWithSize(10);
set("a", "aaaaaaaaaaa", "a"); // size=12
cache.flush();
assertAbsent("a");
}
@Test public void constructorDoesNotAllowZeroCacheSize() throws Exception {
try {
DiskLruCache.create(fileSystem, cacheDir, appVersion, 2, 0);
fail();
} catch (IllegalArgumentException expected) {
}
}
@Test public void constructorDoesNotAllowZeroValuesPerEntry() throws Exception {
try {
DiskLruCache.create(fileSystem, cacheDir, appVersion, 0, 10);
fail();
} catch (IllegalArgumentException expected) {
}
}
@Test public void removeAbsentElement() throws Exception {
cache.remove("a");
}
@Test public void readingTheSameStreamMultipleTimes() throws Exception {
set("a", "a", "b");
DiskLruCache.Snapshot snapshot = cache.get("a");
assertSame(snapshot.getSource(0), snapshot.getSource(0));
snapshot.close();
}
@Test public void rebuildJournalOnRepeatedReads() throws Exception {
set("a", "a", "a");
set("b", "b", "b");
while (executor.jobs.isEmpty()) {
assertValue("a", "a", "a");
assertValue("b", "b", "b");
}
}
@Test public void rebuildJournalOnRepeatedEdits() throws Exception {
while (executor.jobs.isEmpty()) {
set("a", "a", "a");
set("b", "b", "b");
}
executor.jobs.removeFirst().run();
// Sanity check that a rebuilt journal behaves normally.
assertValue("a", "a", "a");
assertValue("b", "b", "b");
}
/** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/28">Issue #28</a> */
@Test public void rebuildJournalOnRepeatedReadsWithOpenAndClose() throws Exception {
set("a", "a", "a");
set("b", "b", "b");
while (executor.jobs.isEmpty()) {
assertValue("a", "a", "a");
assertValue("b", "b", "b");
cache.close();
createNewCache();
}
}
/** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/28">Issue #28</a> */
@Test public void rebuildJournalOnRepeatedEditsWithOpenAndClose() throws Exception {
while (executor.jobs.isEmpty()) {
set("a", "a", "a");
set("b", "b", "b");
cache.close();
createNewCache();
}
}
@Test public void restoreBackupFile() throws Exception {
DiskLruCache.Editor creator = cache.edit("k1");
setString(creator, 0, "ABC");
setString(creator, 1, "DE");
creator.commit();
cache.close();
fileSystem.rename(journalFile, journalBkpFile);
assertFalse(fileSystem.exists(journalFile));
createNewCache();
DiskLruCache.Snapshot snapshot = cache.get("k1");
assertSnapshotValue(snapshot, 0, "ABC");
assertSnapshotValue(snapshot, 1, "DE");
assertFalse(fileSystem.exists(journalBkpFile));
assertTrue(fileSystem.exists(journalFile));
}
@Test public void journalFileIsPreferredOverBackupFile() throws Exception {
DiskLruCache.Editor creator = cache.edit("k1");
setString(creator, 0, "ABC");
setString(creator, 1, "DE");
creator.commit();
cache.flush();
copyFile(journalFile, journalBkpFile);
creator = cache.edit("k2");
setString(creator, 0, "F");
setString(creator, 1, "GH");
creator.commit();
cache.close();
assertTrue(fileSystem.exists(journalFile));
assertTrue(fileSystem.exists(journalBkpFile));
createNewCache();
DiskLruCache.Snapshot snapshotA = cache.get("k1");
assertSnapshotValue(snapshotA, 0, "ABC");
assertSnapshotValue(snapshotA, 1, "DE");
DiskLruCache.Snapshot snapshotB = cache.get("k2");
assertSnapshotValue(snapshotB, 0, "F");
assertSnapshotValue(snapshotB, 1, "GH");
assertFalse(fileSystem.exists(journalBkpFile));
assertTrue(fileSystem.exists(journalFile));
}
@Test public void openCreatesDirectoryIfNecessary() throws Exception {
cache.close();
File dir = tempDir.newFolder("testOpenCreatesDirectoryIfNecessary");
cache = DiskLruCache.create(fileSystem, dir, appVersion, 2, Integer.MAX_VALUE);
set("a", "a", "a");
assertTrue(fileSystem.exists(new File(dir, "a.0")));
assertTrue(fileSystem.exists(new File(dir, "a.1")));
assertTrue(fileSystem.exists(new File(dir, "journal")));
}
@Test public void fileDeletedExternally() throws Exception {
set("a", "a", "a");
fileSystem.delete(getCleanFile("a", 1));
assertNull(cache.get("a"));
}
@Test public void editSameVersion() throws Exception {
set("a", "a", "a");
DiskLruCache.Snapshot snapshot = cache.get("a");
DiskLruCache.Editor editor = snapshot.edit();
setString(editor, 1, "a2");
editor.commit();
assertValue("a", "a", "a2");
}
@Test public void editSnapshotAfterChangeAborted() throws Exception {
set("a", "a", "a");
DiskLruCache.Snapshot snapshot = cache.get("a");
DiskLruCache.Editor toAbort = snapshot.edit();
setString(toAbort, 0, "b");
toAbort.abort();
DiskLruCache.Editor editor = snapshot.edit();
setString(editor, 1, "a2");
editor.commit();
assertValue("a", "a", "a2");
}
@Test public void editSnapshotAfterChangeCommitted() throws Exception {
set("a", "a", "a");
DiskLruCache.Snapshot snapshot = cache.get("a");
DiskLruCache.Editor toAbort = snapshot.edit();
setString(toAbort, 0, "b");
toAbort.commit();
assertNull(snapshot.edit());
}
@Test public void editSinceEvicted() throws Exception {
cache.close();
createNewCacheWithSize(10);
set("a", "aa", "aaa"); // size 5
DiskLruCache.Snapshot snapshot = cache.get("a");
set("b", "bb", "bbb"); // size 5
set("c", "cc", "ccc"); // size 5; will evict 'A'
cache.flush();
assertNull(snapshot.edit());
}
@Test public void editSinceEvictedAndRecreated() throws Exception {
cache.close();
createNewCacheWithSize(10);
set("a", "aa", "aaa"); // size 5
DiskLruCache.Snapshot snapshot = cache.get("a");
set("b", "bb", "bbb"); // size 5
set("c", "cc", "ccc"); // size 5; will evict 'A'
set("a", "a", "aaaa"); // size 5; will evict 'B'
cache.flush();
assertNull(snapshot.edit());
}
/** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/2">Issue #2</a> */
@Test public void aggressiveClearingHandlesWrite() throws Exception {
fileSystem.deleteContents(tempDir.getRoot());
set("a", "a", "a");
assertValue("a", "a", "a");
}
/** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/2">Issue #2</a> */
@Test public void aggressiveClearingHandlesEdit() throws Exception {
set("a", "a", "a");
DiskLruCache.Editor a = cache.get("a").edit();
fileSystem.deleteContents(tempDir.getRoot());
setString(a, 1, "a2");
a.commit();
}
@Test public void removeHandlesMissingFile() throws Exception {
set("a", "a", "a");
getCleanFile("a", 0).delete();
cache.remove("a");
}
/** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/2">Issue #2</a> */
@Test public void aggressiveClearingHandlesPartialEdit() throws Exception {
set("a", "a", "a");
set("b", "b", "b");
DiskLruCache.Editor a = cache.get("a").edit();
setString(a, 0, "a1");
fileSystem.deleteContents(tempDir.getRoot());
setString(a, 1, "a2");
a.commit();
assertNull(cache.get("a"));
}
/** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/2">Issue #2</a> */
@Test public void aggressiveClearingHandlesRead() throws Exception {
fileSystem.deleteContents(tempDir.getRoot());
assertNull(cache.get("a"));
}
/**
* We had a long-lived bug where {@link DiskLruCache#trimToSize} could
* infinite loop if entries being edited required deletion for the operation
* to complete.
*/
@Test public void trimToSizeWithActiveEdit() throws Exception {
set("a", "a1234", "a1234");
DiskLruCache.Editor a = cache.edit("a");
setString(a, 0, "a123");
cache.setMaxSize(8); // Smaller than the sum of active edits!
cache.flush(); // Force trimToSize().
assertEquals(0, cache.size());
assertNull(cache.get("a"));
// After the edit is completed, its entry is still gone.
setString(a, 1, "a1");
a.commit();
assertAbsent("a");
assertEquals(0, cache.size());
}
@Test public void evictAll() throws Exception {
set("a", "a", "a");
set("b", "b", "b");
cache.evictAll();
assertEquals(0, cache.size());
assertAbsent("a");
assertAbsent("b");
}
@Test public void evictAllWithPartialCreate() throws Exception {
DiskLruCache.Editor a = cache.edit("a");
setString(a, 0, "a1");
setString(a, 1, "a2");
cache.evictAll();
assertEquals(0, cache.size());
a.commit();
assertAbsent("a");
}
@Test public void evictAllWithPartialEditDoesNotStoreAValue() throws Exception {
set("a", "a", "a");
DiskLruCache.Editor a = cache.edit("a");
setString(a, 0, "a1");
setString(a, 1, "a2");
cache.evictAll();
assertEquals(0, cache.size());
a.commit();
assertAbsent("a");
}
@Test public void evictAllDoesntInterruptPartialRead() throws Exception {
set("a", "a", "a");
DiskLruCache.Snapshot a = cache.get("a");
assertSnapshotValue(a, 0, "a");
cache.evictAll();
assertEquals(0, cache.size());
assertAbsent("a");
assertSnapshotValue(a, 1, "a");
a.close();
}
@Test public void editSnapshotAfterEvictAllReturnsNullDueToStaleValue() throws Exception {
set("a", "a", "a");
DiskLruCache.Snapshot a = cache.get("a");
cache.evictAll();
assertEquals(0, cache.size());
assertAbsent("a");
assertNull(a.edit());
a.close();
}
@Test public void iterator() throws Exception {
set("a", "a1", "a2");
set("b", "b1", "b2");
set("c", "c1", "c2");
Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
assertTrue(iterator.hasNext());
DiskLruCache.Snapshot a = iterator.next();
assertEquals("a", a.key());
assertSnapshotValue(a, 0, "a1");
assertSnapshotValue(a, 1, "a2");
a.close();
assertTrue(iterator.hasNext());
DiskLruCache.Snapshot b = iterator.next();
assertEquals("b", b.key());
assertSnapshotValue(b, 0, "b1");
assertSnapshotValue(b, 1, "b2");
b.close();
assertTrue(iterator.hasNext());
DiskLruCache.Snapshot c = iterator.next();
assertEquals("c", c.key());
assertSnapshotValue(c, 0, "c1");
assertSnapshotValue(c, 1, "c2");
c.close();
assertFalse(iterator.hasNext());
try {
iterator.next();
fail();
} catch (NoSuchElementException expected) {
}
}
@Test public void iteratorElementsAddedDuringIterationAreOmitted() throws Exception {
set("a", "a1", "a2");
set("b", "b1", "b2");
Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
DiskLruCache.Snapshot a = iterator.next();
assertEquals("a", a.key());
a.close();
set("c", "c1", "c2");
DiskLruCache.Snapshot b = iterator.next();
assertEquals("b", b.key());
b.close();
assertFalse(iterator.hasNext());
}
@Test public void iteratorElementsUpdatedDuringIterationAreUpdated() throws Exception {
set("a", "a1", "a2");
set("b", "b1", "b2");
Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
DiskLruCache.Snapshot a = iterator.next();
assertEquals("a", a.key());
a.close();
set("b", "b3", "b4");
DiskLruCache.Snapshot b = iterator.next();
assertEquals("b", b.key());
assertSnapshotValue(b, 0, "b3");
assertSnapshotValue(b, 1, "b4");
b.close();
}
@Test public void iteratorElementsRemovedDuringIterationAreOmitted() throws Exception {
set("a", "a1", "a2");
set("b", "b1", "b2");
Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
cache.remove("b");
DiskLruCache.Snapshot a = iterator.next();
assertEquals("a", a.key());
a.close();
assertFalse(iterator.hasNext());
}
@Test public void iteratorRemove() throws Exception {
set("a", "a1", "a2");
Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
DiskLruCache.Snapshot a = iterator.next();
a.close();
iterator.remove();
assertEquals(null, cache.get("a"));
}
@Test public void iteratorRemoveBeforeNext() throws Exception {
set("a", "a1", "a2");
Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
try {
iterator.remove();
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void iteratorRemoveOncePerCallToNext() throws Exception {
set("a", "a1", "a2");
Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
DiskLruCache.Snapshot a = iterator.next();
iterator.remove();
a.close();
try {
iterator.remove();
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void cacheClosedTruncatesIterator() throws Exception {
set("a", "a1", "a2");
Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
cache.close();
assertFalse(iterator.hasNext());
}
@Test public void isClosed_uninitializedCache() throws Exception {
// Create an uninitialized cache.
cache = new DiskLruCache(fileSystem, cacheDir, appVersion, 2, Integer.MAX_VALUE, executor);
toClose.add(cache);
assertFalse(cache.isClosed());
cache.close();
assertTrue(cache.isClosed());
}
@Test public void journalWriteFailsDuringEdit() throws Exception {
set("a", "a", "a");
set("b", "b", "b");
// We can't begin the edit if writing 'DIRTY' fails.
fileSystem.setFaulty(journalFile, true);
assertNull(cache.edit("c"));
// Once the journal has a failure, subsequent writes aren't permitted.
fileSystem.setFaulty(journalFile, false);
assertNull(cache.edit("d"));
// Confirm that the fault didn't corrupt entries stored before the fault was introduced.
cache.close();
cache = new DiskLruCache(fileSystem, cacheDir, appVersion, 2, Integer.MAX_VALUE, executor);
assertValue("a", "a", "a");
assertValue("b", "b", "b");
assertAbsent("c");
assertAbsent("d");
}
/**
* We had a bug where the cache was left in an inconsistent state after a journal write failed.
* https://github.com/square/okhttp/issues/1211
*/
@Test public void journalWriteFailsDuringEditorCommit() throws Exception {
set("a", "a", "a");
set("b", "b", "b");
// Create an entry that fails to write to the journal during commit.
DiskLruCache.Editor editor = cache.edit("c");
setString(editor, 0, "c");
setString(editor, 1, "c");
fileSystem.setFaulty(journalFile, true);
editor.commit();
// Once the journal has a failure, subsequent writes aren't permitted.
fileSystem.setFaulty(journalFile, false);
assertNull(cache.edit("d"));
// Confirm that the fault didn't corrupt entries stored before the fault was introduced.
cache.close();
cache = new DiskLruCache(fileSystem, cacheDir, appVersion, 2, Integer.MAX_VALUE, executor);
assertValue("a", "a", "a");
assertValue("b", "b", "b");
assertAbsent("c");
assertAbsent("d");
}
@Test public void journalWriteFailsDuringEditorAbort() throws Exception {
set("a", "a", "a");
set("b", "b", "b");
// Create an entry that fails to write to the journal during abort.
DiskLruCache.Editor editor = cache.edit("c");
setString(editor, 0, "c");
setString(editor, 1, "c");
fileSystem.setFaulty(journalFile, true);
editor.abort();
// Once the journal has a failure, subsequent writes aren't permitted.
fileSystem.setFaulty(journalFile, false);
assertNull(cache.edit("d"));
// Confirm that the fault didn't corrupt entries stored before the fault was introduced.
cache.close();
cache = new DiskLruCache(fileSystem, cacheDir, appVersion, 2, Integer.MAX_VALUE, executor);
assertValue("a", "a", "a");
assertValue("b", "b", "b");
assertAbsent("c");
assertAbsent("d");
}
@Test public void journalWriteFailsDuringRemove() throws Exception {
set("a", "a", "a");
set("b", "b", "b");
// Remove, but the journal write will fail.
fileSystem.setFaulty(journalFile, true);
assertTrue(cache.remove("a"));
// Confirm that the entry was still removed.
fileSystem.setFaulty(journalFile, false);
cache.close();
cache = new DiskLruCache(fileSystem, cacheDir, appVersion, 2, Integer.MAX_VALUE, executor);
assertAbsent("a");
assertValue("b", "b", "b");
}
private void assertJournalEquals(String... expectedBodyLines) throws Exception {
List<String> expectedLines = new ArrayList<>();
expectedLines.add(MAGIC);
expectedLines.add(VERSION_1);
expectedLines.add("100");
expectedLines.add("2");
expectedLines.add("");
expectedLines.addAll(Arrays.asList(expectedBodyLines));
assertEquals(expectedLines, readJournalLines());
}
private void createJournal(String... bodyLines) throws Exception {
createJournalWithHeader(MAGIC, VERSION_1, "100", "2", "", bodyLines);
}
private void createJournalWithHeader(String magic, String version, String appVersion,
String valueCount, String blank, String... bodyLines) throws Exception {
BufferedSink sink = Okio.buffer(fileSystem.sink(journalFile));
sink.writeUtf8(magic + "\n");
sink.writeUtf8(version + "\n");
sink.writeUtf8(appVersion + "\n");
sink.writeUtf8(valueCount + "\n");
sink.writeUtf8(blank + "\n");
for (String line : bodyLines) {
sink.writeUtf8(line);
sink.writeUtf8("\n");
}
sink.close();
}
private List<String> readJournalLines() throws Exception {
List<String> result = new ArrayList<>();
BufferedSource source = Okio.buffer(fileSystem.source(journalFile));
for (String line; (line = source.readUtf8Line()) != null; ) {
result.add(line);
}
source.close();
return result;
}
private File getCleanFile(String key, int index) {
return new File(cacheDir, key + "." + index);
}
private File getDirtyFile(String key, int index) {
return new File(cacheDir, key + "." + index + ".tmp");
}
private String readFile(File file) throws Exception {
BufferedSource source = Okio.buffer(fileSystem.source(file));
String result = source.readUtf8();
source.close();
return result;
}
public void writeFile(File file, String content) throws Exception {
BufferedSink sink = Okio.buffer(fileSystem.sink(file));
sink.writeUtf8(content);
sink.close();
}
private static void assertInoperable(DiskLruCache.Editor editor) throws Exception {
try {
setString(editor, 0, "A");
fail();
} catch (IllegalStateException expected) {
}
try {
editor.newSource(0);
fail();
} catch (IllegalStateException expected) {
}
try {
editor.newSink(0);
fail();
} catch (IllegalStateException expected) {
}
try {
editor.commit();
fail();
} catch (IllegalStateException expected) {
}
try {
editor.abort();
fail();
} catch (IllegalStateException expected) {
}
}
private void generateSomeGarbageFiles() throws Exception {
File dir1 = new File(cacheDir, "dir1");
File dir2 = new File(dir1, "dir2");
writeFile(getCleanFile("g1", 0), "A");
writeFile(getCleanFile("g1", 1), "B");
writeFile(getCleanFile("g2", 0), "C");
writeFile(getCleanFile("g2", 1), "D");
writeFile(getCleanFile("g2", 1), "D");
writeFile(new File(cacheDir, "otherFile0"), "E");
writeFile(new File(dir2, "otherFile1"), "F");
}
private void assertGarbageFilesAllDeleted() throws Exception {
assertFalse(fileSystem.exists(getCleanFile("g1", 0)));
assertFalse(fileSystem.exists(getCleanFile("g1", 1)));
assertFalse(fileSystem.exists(getCleanFile("g2", 0)));
assertFalse(fileSystem.exists(getCleanFile("g2", 1)));
assertFalse(fileSystem.exists(new File(cacheDir, "otherFile0")));
assertFalse(fileSystem.exists(new File(cacheDir, "dir1")));
}
private void set(String key, String value0, String value1) throws Exception {
DiskLruCache.Editor editor = cache.edit(key);
setString(editor, 0, value0);
setString(editor, 1, value1);
editor.commit();
}
public static void setString(DiskLruCache.Editor editor, int index, String value) throws IOException {
BufferedSink writer = Okio.buffer(editor.newSink(index));
writer.writeUtf8(value);
writer.close();
}
private void assertAbsent(String key) throws Exception {
DiskLruCache.Snapshot snapshot = cache.get(key);
if (snapshot != null) {
snapshot.close();
fail();
}
assertFalse(fileSystem.exists(getCleanFile(key, 0)));
assertFalse(fileSystem.exists(getCleanFile(key, 1)));
assertFalse(fileSystem.exists(getDirtyFile(key, 0)));
assertFalse(fileSystem.exists(getDirtyFile(key, 1)));
}
private void assertValue(String key, String value0, String value1) throws Exception {
DiskLruCache.Snapshot snapshot = cache.get(key);
assertSnapshotValue(snapshot, 0, value0);
assertSnapshotValue(snapshot, 1, value1);
assertTrue(fileSystem.exists(getCleanFile(key, 0)));
assertTrue(fileSystem.exists(getCleanFile(key, 1)));
snapshot.close();
}
private void assertSnapshotValue(DiskLruCache.Snapshot snapshot, int index, String value)
throws IOException {
assertEquals(value, sourceAsString(snapshot.getSource(index)));
assertEquals(value.length(), snapshot.getLength(index));
}
private String sourceAsString(Source source) throws IOException {
return source != null ? Okio.buffer(source).readUtf8() : null;
}
private void copyFile(File from, File to) throws IOException {
Source source = fileSystem.source(from);
BufferedSink sink = Okio.buffer(fileSystem.sink(to));
sink.writeAll(source);
source.close();
sink.close();
}
private static class TestExecutor implements Executor {
final Deque<Runnable> jobs = new ArrayDeque<>();
@Override public void execute(Runnable command) {
jobs.addLast(command);
}
}
}