1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.squareup.okhttp.internal;
17
18import com.squareup.okhttp.internal.io.FileSystem;
19import java.io.File;
20import java.io.IOException;
21import java.util.ArrayDeque;
22import java.util.ArrayList;
23import java.util.Arrays;
24import java.util.Deque;
25import java.util.Iterator;
26import java.util.List;
27import java.util.NoSuchElementException;
28import java.util.concurrent.Executor;
29import okio.BufferedSink;
30import okio.BufferedSource;
31import okio.Okio;
32import okio.Source;
33import org.junit.After;
34import org.junit.Before;
35import org.junit.Rule;
36import org.junit.Test;
37import org.junit.rules.TemporaryFolder;
38import org.junit.rules.Timeout;
39
40import static com.squareup.okhttp.internal.DiskLruCache.JOURNAL_FILE;
41import static com.squareup.okhttp.internal.DiskLruCache.JOURNAL_FILE_BACKUP;
42import static com.squareup.okhttp.internal.DiskLruCache.MAGIC;
43import static com.squareup.okhttp.internal.DiskLruCache.VERSION_1;
44import static org.junit.Assert.assertEquals;
45import static org.junit.Assert.assertFalse;
46import static org.junit.Assert.assertNull;
47import static org.junit.Assert.assertSame;
48import static org.junit.Assert.assertTrue;
49import static org.junit.Assert.fail;
50
51public final class DiskLruCacheTest {
52  @Rule public final TemporaryFolder tempDir = new TemporaryFolder();
53  @Rule public final Timeout timeout = new Timeout(30 * 1000);
54
55  private final FaultyFileSystem fileSystem = new FaultyFileSystem(FileSystem.SYSTEM);
56  private final int appVersion = 100;
57  private File cacheDir;
58  private File journalFile;
59  private File journalBkpFile;
60  private final TestExecutor executor = new TestExecutor();
61
62  private DiskLruCache cache;
63  private final Deque<DiskLruCache> toClose = new ArrayDeque<>();
64
65  private void createNewCache() throws IOException {
66    createNewCacheWithSize(Integer.MAX_VALUE);
67  }
68
69  private void createNewCacheWithSize(int maxSize) throws IOException {
70    cache = new DiskLruCache(fileSystem, cacheDir, appVersion, 2, maxSize, executor);
71    synchronized (cache) {
72      cache.initialize();
73    }
74    toClose.add(cache);
75  }
76
77  @Before public void setUp() throws Exception {
78    cacheDir = tempDir.getRoot();
79    journalFile = new File(cacheDir, JOURNAL_FILE);
80    journalBkpFile = new File(cacheDir, JOURNAL_FILE_BACKUP);
81    createNewCache();
82  }
83
84  @After public void tearDown() throws Exception {
85    while (!toClose.isEmpty()) {
86      toClose.pop().close();
87    }
88  }
89
90  @Test public void emptyCache() throws Exception {
91    cache.close();
92    assertJournalEquals();
93  }
94
95  @Test public void validateKey() throws Exception {
96    String key = null;
97    try {
98      key = "has_space ";
99      cache.edit(key);
100      fail("Exepcting an IllegalArgumentException as the key was invalid.");
101    } catch (IllegalArgumentException iae) {
102      assertEquals("keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"", iae.getMessage());
103    }
104    try {
105      key = "has_CR\r";
106      cache.edit(key);
107      fail("Exepcting an IllegalArgumentException as the key was invalid.");
108    } catch (IllegalArgumentException iae) {
109      assertEquals("keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"", iae.getMessage());
110    }
111    try {
112      key = "has_LF\n";
113      cache.edit(key);
114      fail("Exepcting an IllegalArgumentException as the key was invalid.");
115    } catch (IllegalArgumentException iae) {
116      assertEquals("keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"", iae.getMessage());
117    }
118    try {
119      key = "has_invalid/";
120      cache.edit(key);
121      fail("Exepcting an IllegalArgumentException as the key was invalid.");
122    } catch (IllegalArgumentException iae) {
123      assertEquals("keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"", iae.getMessage());
124    }
125    try {
126      key = "has_invalid\u2603";
127      cache.edit(key);
128      fail("Exepcting an IllegalArgumentException as the key was invalid.");
129    } catch (IllegalArgumentException iae) {
130      assertEquals("keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"", iae.getMessage());
131    }
132    try {
133      key = "this_is_way_too_long_this_is_way_too_long_this_is_way_too_long_"
134          + "this_is_way_too_long_this_is_way_too_long_this_is_way_too_long";
135      cache.edit(key);
136      fail("Exepcting an IllegalArgumentException as the key was too long.");
137    } catch (IllegalArgumentException iae) {
138      assertEquals("keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"", iae.getMessage());
139    }
140
141    // Test valid cases.
142
143    // Exactly 120.
144    key = "0123456789012345678901234567890123456789012345678901234567890123456789"
145        + "01234567890123456789012345678901234567890123456789";
146    cache.edit(key).abort();
147    // Contains all valid characters.
148    key = "abcdefghijklmnopqrstuvwxyz_0123456789";
149    cache.edit(key).abort();
150    // Contains dash.
151    key = "-20384573948576";
152    cache.edit(key).abort();
153  }
154
155  @Test public void writeAndReadEntry() throws Exception {
156    DiskLruCache.Editor creator = cache.edit("k1");
157    setString(creator, 0, "ABC");
158    setString(creator, 1, "DE");
159    assertNull(creator.newSource(0));
160    assertNull(creator.newSource(1));
161    creator.commit();
162
163    DiskLruCache.Snapshot snapshot = cache.get("k1");
164    assertSnapshotValue(snapshot, 0, "ABC");
165    assertSnapshotValue(snapshot, 1, "DE");
166  }
167
168  @Test public void readAndWriteEntryAcrossCacheOpenAndClose() throws Exception {
169    DiskLruCache.Editor creator = cache.edit("k1");
170    setString(creator, 0, "A");
171    setString(creator, 1, "B");
172    creator.commit();
173    cache.close();
174
175    createNewCache();
176    DiskLruCache.Snapshot snapshot = cache.get("k1");
177    assertSnapshotValue(snapshot, 0, "A");
178    assertSnapshotValue(snapshot, 1, "B");
179    snapshot.close();
180  }
181
182  @Test public void readAndWriteEntryWithoutProperClose() throws Exception {
183    DiskLruCache.Editor creator = cache.edit("k1");
184    setString(creator, 0, "A");
185    setString(creator, 1, "B");
186    creator.commit();
187
188    // Simulate a dirty close of 'cache' by opening the cache directory again.
189    createNewCache();
190    DiskLruCache.Snapshot snapshot = cache.get("k1");
191    assertSnapshotValue(snapshot, 0, "A");
192    assertSnapshotValue(snapshot, 1, "B");
193    snapshot.close();
194  }
195
196  @Test public void journalWithEditAndPublish() throws Exception {
197    DiskLruCache.Editor creator = cache.edit("k1");
198    assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed.
199    setString(creator, 0, "AB");
200    setString(creator, 1, "C");
201    creator.commit();
202    cache.close();
203    assertJournalEquals("DIRTY k1", "CLEAN k1 2 1");
204  }
205
206  @Test public void revertedNewFileIsRemoveInJournal() throws Exception {
207    DiskLruCache.Editor creator = cache.edit("k1");
208    assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed.
209    setString(creator, 0, "AB");
210    setString(creator, 1, "C");
211    creator.abort();
212    cache.close();
213    assertJournalEquals("DIRTY k1", "REMOVE k1");
214  }
215
216  @Test public void unterminatedEditIsRevertedOnClose() throws Exception {
217    cache.edit("k1");
218    cache.close();
219    assertJournalEquals("DIRTY k1", "REMOVE k1");
220  }
221
222  @Test public void journalDoesNotIncludeReadOfYetUnpublishedValue() throws Exception {
223    DiskLruCache.Editor creator = cache.edit("k1");
224    assertNull(cache.get("k1"));
225    setString(creator, 0, "A");
226    setString(creator, 1, "BC");
227    creator.commit();
228    cache.close();
229    assertJournalEquals("DIRTY k1", "CLEAN k1 1 2");
230  }
231
232  @Test public void journalWithEditAndPublishAndRead() throws Exception {
233    DiskLruCache.Editor k1Creator = cache.edit("k1");
234    setString(k1Creator, 0, "AB");
235    setString(k1Creator, 1, "C");
236    k1Creator.commit();
237    DiskLruCache.Editor k2Creator = cache.edit("k2");
238    setString(k2Creator, 0, "DEF");
239    setString(k2Creator, 1, "G");
240    k2Creator.commit();
241    DiskLruCache.Snapshot k1Snapshot = cache.get("k1");
242    k1Snapshot.close();
243    cache.close();
244    assertJournalEquals("DIRTY k1", "CLEAN k1 2 1", "DIRTY k2", "CLEAN k2 3 1", "READ k1");
245  }
246
247  @Test public void cannotOperateOnEditAfterPublish() throws Exception {
248    DiskLruCache.Editor editor = cache.edit("k1");
249    setString(editor, 0, "A");
250    setString(editor, 1, "B");
251    editor.commit();
252    assertInoperable(editor);
253  }
254
255  @Test public void cannotOperateOnEditAfterRevert() throws Exception {
256    DiskLruCache.Editor editor = cache.edit("k1");
257    setString(editor, 0, "A");
258    setString(editor, 1, "B");
259    editor.abort();
260    assertInoperable(editor);
261  }
262
263  @Test public void explicitRemoveAppliedToDiskImmediately() throws Exception {
264    DiskLruCache.Editor editor = cache.edit("k1");
265    setString(editor, 0, "ABC");
266    setString(editor, 1, "B");
267    editor.commit();
268    File k1 = getCleanFile("k1", 0);
269    assertEquals("ABC", readFile(k1));
270    cache.remove("k1");
271    assertFalse(fileSystem.exists(k1));
272  }
273
274  @Test public void removePreventsActiveEditFromStoringAValue() throws Exception {
275    set("a", "a", "a");
276    DiskLruCache.Editor a = cache.edit("a");
277    setString(a, 0, "a1");
278    assertTrue(cache.remove("a"));
279    setString(a, 1, "a2");
280    a.commit();
281    assertAbsent("a");
282  }
283
284  /**
285   * Each read sees a snapshot of the file at the time read was called.
286   * This means that two reads of the same key can see different data.
287   */
288  @Test public void readAndWriteOverlapsMaintainConsistency() throws Exception {
289    DiskLruCache.Editor v1Creator = cache.edit("k1");
290    setString(v1Creator, 0, "AAaa");
291    setString(v1Creator, 1, "BBbb");
292    v1Creator.commit();
293
294    DiskLruCache.Snapshot snapshot1 = cache.get("k1");
295    BufferedSource inV1 = Okio.buffer(snapshot1.getSource(0));
296    assertEquals('A', inV1.readByte());
297    assertEquals('A', inV1.readByte());
298
299    DiskLruCache.Editor v1Updater = cache.edit("k1");
300    setString(v1Updater, 0, "CCcc");
301    setString(v1Updater, 1, "DDdd");
302    v1Updater.commit();
303
304    DiskLruCache.Snapshot snapshot2 = cache.get("k1");
305    assertSnapshotValue(snapshot2, 0, "CCcc");
306    assertSnapshotValue(snapshot2, 1, "DDdd");
307    snapshot2.close();
308
309    assertEquals('a', inV1.readByte());
310    assertEquals('a', inV1.readByte());
311    assertSnapshotValue(snapshot1, 1, "BBbb");
312    snapshot1.close();
313  }
314
315  @Test public void openWithDirtyKeyDeletesAllFilesForThatKey() throws Exception {
316    cache.close();
317    File cleanFile0 = getCleanFile("k1", 0);
318    File cleanFile1 = getCleanFile("k1", 1);
319    File dirtyFile0 = getDirtyFile("k1", 0);
320    File dirtyFile1 = getDirtyFile("k1", 1);
321    writeFile(cleanFile0, "A");
322    writeFile(cleanFile1, "B");
323    writeFile(dirtyFile0, "C");
324    writeFile(dirtyFile1, "D");
325    createJournal("CLEAN k1 1 1", "DIRTY   k1");
326    createNewCache();
327    assertFalse(fileSystem.exists(cleanFile0));
328    assertFalse(fileSystem.exists(cleanFile1));
329    assertFalse(fileSystem.exists(dirtyFile0));
330    assertFalse(fileSystem.exists(dirtyFile1));
331    assertNull(cache.get("k1"));
332  }
333
334  @Test public void openWithInvalidVersionClearsDirectory() throws Exception {
335    cache.close();
336    generateSomeGarbageFiles();
337    createJournalWithHeader(MAGIC, "0", "100", "2", "");
338    createNewCache();
339    assertGarbageFilesAllDeleted();
340  }
341
342  @Test public void openWithInvalidAppVersionClearsDirectory() throws Exception {
343    cache.close();
344    generateSomeGarbageFiles();
345    createJournalWithHeader(MAGIC, "1", "101", "2", "");
346    createNewCache();
347    assertGarbageFilesAllDeleted();
348  }
349
350  @Test public void openWithInvalidValueCountClearsDirectory() throws Exception {
351    cache.close();
352    generateSomeGarbageFiles();
353    createJournalWithHeader(MAGIC, "1", "100", "1", "");
354    createNewCache();
355    assertGarbageFilesAllDeleted();
356  }
357
358  @Test public void openWithInvalidBlankLineClearsDirectory() throws Exception {
359    cache.close();
360    generateSomeGarbageFiles();
361    createJournalWithHeader(MAGIC, "1", "100", "2", "x");
362    createNewCache();
363    assertGarbageFilesAllDeleted();
364  }
365
366  @Test public void openWithInvalidJournalLineClearsDirectory() throws Exception {
367    cache.close();
368    generateSomeGarbageFiles();
369    createJournal("CLEAN k1 1 1", "BOGUS");
370    createNewCache();
371    assertGarbageFilesAllDeleted();
372    assertNull(cache.get("k1"));
373  }
374
375  @Test public void openWithInvalidFileSizeClearsDirectory() throws Exception {
376    cache.close();
377    generateSomeGarbageFiles();
378    createJournal("CLEAN k1 0000x001 1");
379    createNewCache();
380    assertGarbageFilesAllDeleted();
381    assertNull(cache.get("k1"));
382  }
383
384  @Test public void openWithTruncatedLineDiscardsThatLine() throws Exception {
385    cache.close();
386    writeFile(getCleanFile("k1", 0), "A");
387    writeFile(getCleanFile("k1", 1), "B");
388
389    BufferedSink sink = Okio.buffer(fileSystem.sink(journalFile));
390    sink.writeUtf8(MAGIC + "\n" + VERSION_1 + "\n100\n2\n\nCLEAN k1 1 1"); // no trailing newline
391    sink.close();
392    createNewCache();
393    assertNull(cache.get("k1"));
394
395    // The journal is not corrupt when editing after a truncated line.
396    set("k1", "C", "D");
397
398    cache.close();
399    createNewCache();
400    assertValue("k1", "C", "D");
401  }
402
403  @Test public void openWithTooManyFileSizesClearsDirectory() throws Exception {
404    cache.close();
405    generateSomeGarbageFiles();
406    createJournal("CLEAN k1 1 1 1");
407    createNewCache();
408    assertGarbageFilesAllDeleted();
409    assertNull(cache.get("k1"));
410  }
411
412  @Test public void keyWithSpaceNotPermitted() throws Exception {
413    try {
414      cache.edit("my key");
415      fail();
416    } catch (IllegalArgumentException expected) {
417    }
418  }
419
420  @Test public void keyWithNewlineNotPermitted() throws Exception {
421    try {
422      cache.edit("my\nkey");
423      fail();
424    } catch (IllegalArgumentException expected) {
425    }
426  }
427
428  @Test public void keyWithCarriageReturnNotPermitted() throws Exception {
429    try {
430      cache.edit("my\rkey");
431      fail();
432    } catch (IllegalArgumentException expected) {
433    }
434  }
435
436  @Test public void nullKeyThrows() throws Exception {
437    try {
438      cache.edit(null);
439      fail();
440    } catch (NullPointerException expected) {
441    }
442  }
443
444  @Test public void createNewEntryWithTooFewValuesFails() throws Exception {
445    DiskLruCache.Editor creator = cache.edit("k1");
446    setString(creator, 1, "A");
447    try {
448      creator.commit();
449      fail();
450    } catch (IllegalStateException expected) {
451    }
452
453    assertFalse(fileSystem.exists(getCleanFile("k1", 0)));
454    assertFalse(fileSystem.exists(getCleanFile("k1", 1)));
455    assertFalse(fileSystem.exists(getDirtyFile("k1", 0)));
456    assertFalse(fileSystem.exists(getDirtyFile("k1", 1)));
457    assertNull(cache.get("k1"));
458
459    DiskLruCache.Editor creator2 = cache.edit("k1");
460    setString(creator2, 0, "B");
461    setString(creator2, 1, "C");
462    creator2.commit();
463  }
464
465  @Test public void revertWithTooFewValues() throws Exception {
466    DiskLruCache.Editor creator = cache.edit("k1");
467    setString(creator, 1, "A");
468    creator.abort();
469    assertFalse(fileSystem.exists(getCleanFile("k1", 0)));
470    assertFalse(fileSystem.exists(getCleanFile("k1", 1)));
471    assertFalse(fileSystem.exists(getDirtyFile("k1", 0)));
472    assertFalse(fileSystem.exists(getDirtyFile("k1", 1)));
473    assertNull(cache.get("k1"));
474  }
475
476  @Test public void updateExistingEntryWithTooFewValuesReusesPreviousValues() throws Exception {
477    DiskLruCache.Editor creator = cache.edit("k1");
478    setString(creator, 0, "A");
479    setString(creator, 1, "B");
480    creator.commit();
481
482    DiskLruCache.Editor updater = cache.edit("k1");
483    setString(updater, 0, "C");
484    updater.commit();
485
486    DiskLruCache.Snapshot snapshot = cache.get("k1");
487    assertSnapshotValue(snapshot, 0, "C");
488    assertSnapshotValue(snapshot, 1, "B");
489    snapshot.close();
490  }
491
492  @Test public void growMaxSize() throws Exception {
493    cache.close();
494    createNewCacheWithSize(10);
495    set("a", "a", "aaa"); // size 4
496    set("b", "bb", "bbbb"); // size 6
497    cache.setMaxSize(20);
498    set("c", "c", "c"); // size 12
499    assertEquals(12, cache.size());
500  }
501
502  @Test public void shrinkMaxSizeEvicts() throws Exception {
503    cache.close();
504    createNewCacheWithSize(20);
505    set("a", "a", "aaa"); // size 4
506    set("b", "bb", "bbbb"); // size 6
507    set("c", "c", "c"); // size 12
508    cache.setMaxSize(10);
509    assertEquals(1, executor.jobs.size());
510  }
511
512  @Test public void evictOnInsert() throws Exception {
513    cache.close();
514    createNewCacheWithSize(10);
515
516    set("a", "a", "aaa"); // size 4
517    set("b", "bb", "bbbb"); // size 6
518    assertEquals(10, cache.size());
519
520    // Cause the size to grow to 12 should evict 'A'.
521    set("c", "c", "c");
522    cache.flush();
523    assertEquals(8, cache.size());
524    assertAbsent("a");
525    assertValue("b", "bb", "bbbb");
526    assertValue("c", "c", "c");
527
528    // Causing the size to grow to 10 should evict nothing.
529    set("d", "d", "d");
530    cache.flush();
531    assertEquals(10, cache.size());
532    assertAbsent("a");
533    assertValue("b", "bb", "bbbb");
534    assertValue("c", "c", "c");
535    assertValue("d", "d", "d");
536
537    // Causing the size to grow to 18 should evict 'B' and 'C'.
538    set("e", "eeee", "eeee");
539    cache.flush();
540    assertEquals(10, cache.size());
541    assertAbsent("a");
542    assertAbsent("b");
543    assertAbsent("c");
544    assertValue("d", "d", "d");
545    assertValue("e", "eeee", "eeee");
546  }
547
548  @Test public void evictOnUpdate() throws Exception {
549    cache.close();
550    createNewCacheWithSize(10);
551
552    set("a", "a", "aa"); // size 3
553    set("b", "b", "bb"); // size 3
554    set("c", "c", "cc"); // size 3
555    assertEquals(9, cache.size());
556
557    // Causing the size to grow to 11 should evict 'A'.
558    set("b", "b", "bbbb");
559    cache.flush();
560    assertEquals(8, cache.size());
561    assertAbsent("a");
562    assertValue("b", "b", "bbbb");
563    assertValue("c", "c", "cc");
564  }
565
566  @Test public void evictionHonorsLruFromCurrentSession() throws Exception {
567    cache.close();
568    createNewCacheWithSize(10);
569    set("a", "a", "a");
570    set("b", "b", "b");
571    set("c", "c", "c");
572    set("d", "d", "d");
573    set("e", "e", "e");
574    cache.get("b").close(); // 'B' is now least recently used.
575
576    // Causing the size to grow to 12 should evict 'A'.
577    set("f", "f", "f");
578    // Causing the size to grow to 12 should evict 'C'.
579    set("g", "g", "g");
580    cache.flush();
581    assertEquals(10, cache.size());
582    assertAbsent("a");
583    assertValue("b", "b", "b");
584    assertAbsent("c");
585    assertValue("d", "d", "d");
586    assertValue("e", "e", "e");
587    assertValue("f", "f", "f");
588  }
589
590  @Test public void evictionHonorsLruFromPreviousSession() throws Exception {
591    set("a", "a", "a");
592    set("b", "b", "b");
593    set("c", "c", "c");
594    set("d", "d", "d");
595    set("e", "e", "e");
596    set("f", "f", "f");
597    cache.get("b").close(); // 'B' is now least recently used.
598    assertEquals(12, cache.size());
599    cache.close();
600    createNewCacheWithSize(10);
601
602    set("g", "g", "g");
603    cache.flush();
604    assertEquals(10, cache.size());
605    assertAbsent("a");
606    assertValue("b", "b", "b");
607    assertAbsent("c");
608    assertValue("d", "d", "d");
609    assertValue("e", "e", "e");
610    assertValue("f", "f", "f");
611    assertValue("g", "g", "g");
612  }
613
614  @Test public void cacheSingleEntryOfSizeGreaterThanMaxSize() throws Exception {
615    cache.close();
616    createNewCacheWithSize(10);
617    set("a", "aaaaa", "aaaaaa"); // size=11
618    cache.flush();
619    assertAbsent("a");
620  }
621
622  @Test public void cacheSingleValueOfSizeGreaterThanMaxSize() throws Exception {
623    cache.close();
624    createNewCacheWithSize(10);
625    set("a", "aaaaaaaaaaa", "a"); // size=12
626    cache.flush();
627    assertAbsent("a");
628  }
629
630  @Test public void constructorDoesNotAllowZeroCacheSize() throws Exception {
631    try {
632      DiskLruCache.create(fileSystem, cacheDir, appVersion, 2, 0);
633      fail();
634    } catch (IllegalArgumentException expected) {
635    }
636  }
637
638  @Test public void constructorDoesNotAllowZeroValuesPerEntry() throws Exception {
639    try {
640      DiskLruCache.create(fileSystem, cacheDir, appVersion, 0, 10);
641      fail();
642    } catch (IllegalArgumentException expected) {
643    }
644  }
645
646  @Test public void removeAbsentElement() throws Exception {
647    cache.remove("a");
648  }
649
650  @Test public void readingTheSameStreamMultipleTimes() throws Exception {
651    set("a", "a", "b");
652    DiskLruCache.Snapshot snapshot = cache.get("a");
653    assertSame(snapshot.getSource(0), snapshot.getSource(0));
654    snapshot.close();
655  }
656
657  @Test public void rebuildJournalOnRepeatedReads() throws Exception {
658    set("a", "a", "a");
659    set("b", "b", "b");
660    while (executor.jobs.isEmpty()) {
661      assertValue("a", "a", "a");
662      assertValue("b", "b", "b");
663    }
664  }
665
666  @Test public void rebuildJournalOnRepeatedEdits() throws Exception {
667    while (executor.jobs.isEmpty()) {
668      set("a", "a", "a");
669      set("b", "b", "b");
670    }
671    executor.jobs.removeFirst().run();
672
673    // Sanity check that a rebuilt journal behaves normally.
674    assertValue("a", "a", "a");
675    assertValue("b", "b", "b");
676  }
677
678  /** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/28">Issue #28</a> */
679  @Test public void rebuildJournalOnRepeatedReadsWithOpenAndClose() throws Exception {
680    set("a", "a", "a");
681    set("b", "b", "b");
682    while (executor.jobs.isEmpty()) {
683      assertValue("a", "a", "a");
684      assertValue("b", "b", "b");
685      cache.close();
686      createNewCache();
687    }
688  }
689
690  /** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/28">Issue #28</a> */
691  @Test public void rebuildJournalOnRepeatedEditsWithOpenAndClose() throws Exception {
692    while (executor.jobs.isEmpty()) {
693      set("a", "a", "a");
694      set("b", "b", "b");
695      cache.close();
696      createNewCache();
697    }
698  }
699
700  @Test public void restoreBackupFile() throws Exception {
701    DiskLruCache.Editor creator = cache.edit("k1");
702    setString(creator, 0, "ABC");
703    setString(creator, 1, "DE");
704    creator.commit();
705    cache.close();
706
707    fileSystem.rename(journalFile, journalBkpFile);
708    assertFalse(fileSystem.exists(journalFile));
709
710    createNewCache();
711
712    DiskLruCache.Snapshot snapshot = cache.get("k1");
713    assertSnapshotValue(snapshot, 0, "ABC");
714    assertSnapshotValue(snapshot, 1, "DE");
715
716    assertFalse(fileSystem.exists(journalBkpFile));
717    assertTrue(fileSystem.exists(journalFile));
718  }
719
720  @Test public void journalFileIsPreferredOverBackupFile() throws Exception {
721    DiskLruCache.Editor creator = cache.edit("k1");
722    setString(creator, 0, "ABC");
723    setString(creator, 1, "DE");
724    creator.commit();
725    cache.flush();
726
727    copyFile(journalFile, journalBkpFile);
728
729    creator = cache.edit("k2");
730    setString(creator, 0, "F");
731    setString(creator, 1, "GH");
732    creator.commit();
733    cache.close();
734
735    assertTrue(fileSystem.exists(journalFile));
736    assertTrue(fileSystem.exists(journalBkpFile));
737
738    createNewCache();
739
740    DiskLruCache.Snapshot snapshotA = cache.get("k1");
741    assertSnapshotValue(snapshotA, 0, "ABC");
742    assertSnapshotValue(snapshotA, 1, "DE");
743
744    DiskLruCache.Snapshot snapshotB = cache.get("k2");
745    assertSnapshotValue(snapshotB, 0, "F");
746    assertSnapshotValue(snapshotB, 1, "GH");
747
748    assertFalse(fileSystem.exists(journalBkpFile));
749    assertTrue(fileSystem.exists(journalFile));
750  }
751
752  @Test public void openCreatesDirectoryIfNecessary() throws Exception {
753    cache.close();
754    File dir = tempDir.newFolder("testOpenCreatesDirectoryIfNecessary");
755    cache = DiskLruCache.create(fileSystem, dir, appVersion, 2, Integer.MAX_VALUE);
756    set("a", "a", "a");
757    assertTrue(fileSystem.exists(new File(dir, "a.0")));
758    assertTrue(fileSystem.exists(new File(dir, "a.1")));
759    assertTrue(fileSystem.exists(new File(dir, "journal")));
760  }
761
762  @Test public void fileDeletedExternally() throws Exception {
763    set("a", "a", "a");
764    fileSystem.delete(getCleanFile("a", 1));
765    assertNull(cache.get("a"));
766  }
767
768  @Test public void editSameVersion() throws Exception {
769    set("a", "a", "a");
770    DiskLruCache.Snapshot snapshot = cache.get("a");
771    DiskLruCache.Editor editor = snapshot.edit();
772    setString(editor, 1, "a2");
773    editor.commit();
774    assertValue("a", "a", "a2");
775  }
776
777  @Test public void editSnapshotAfterChangeAborted() throws Exception {
778    set("a", "a", "a");
779    DiskLruCache.Snapshot snapshot = cache.get("a");
780    DiskLruCache.Editor toAbort = snapshot.edit();
781    setString(toAbort, 0, "b");
782    toAbort.abort();
783    DiskLruCache.Editor editor = snapshot.edit();
784    setString(editor, 1, "a2");
785    editor.commit();
786    assertValue("a", "a", "a2");
787  }
788
789  @Test public void editSnapshotAfterChangeCommitted() throws Exception {
790    set("a", "a", "a");
791    DiskLruCache.Snapshot snapshot = cache.get("a");
792    DiskLruCache.Editor toAbort = snapshot.edit();
793    setString(toAbort, 0, "b");
794    toAbort.commit();
795    assertNull(snapshot.edit());
796  }
797
798  @Test public void editSinceEvicted() throws Exception {
799    cache.close();
800    createNewCacheWithSize(10);
801    set("a", "aa", "aaa"); // size 5
802    DiskLruCache.Snapshot snapshot = cache.get("a");
803    set("b", "bb", "bbb"); // size 5
804    set("c", "cc", "ccc"); // size 5; will evict 'A'
805    cache.flush();
806    assertNull(snapshot.edit());
807  }
808
809  @Test public void editSinceEvictedAndRecreated() throws Exception {
810    cache.close();
811    createNewCacheWithSize(10);
812    set("a", "aa", "aaa"); // size 5
813    DiskLruCache.Snapshot snapshot = cache.get("a");
814    set("b", "bb", "bbb"); // size 5
815    set("c", "cc", "ccc"); // size 5; will evict 'A'
816    set("a", "a", "aaaa"); // size 5; will evict 'B'
817    cache.flush();
818    assertNull(snapshot.edit());
819  }
820
821  /** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/2">Issue #2</a> */
822  @Test public void aggressiveClearingHandlesWrite() throws Exception {
823    fileSystem.deleteContents(tempDir.getRoot());
824    set("a", "a", "a");
825    assertValue("a", "a", "a");
826  }
827
828  /** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/2">Issue #2</a> */
829  @Test public void aggressiveClearingHandlesEdit() throws Exception {
830    set("a", "a", "a");
831    DiskLruCache.Editor a = cache.get("a").edit();
832    fileSystem.deleteContents(tempDir.getRoot());
833    setString(a, 1, "a2");
834    a.commit();
835  }
836
837  @Test public void removeHandlesMissingFile() throws Exception {
838    set("a", "a", "a");
839    getCleanFile("a", 0).delete();
840    cache.remove("a");
841  }
842
843  /** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/2">Issue #2</a> */
844  @Test public void aggressiveClearingHandlesPartialEdit() throws Exception {
845    set("a", "a", "a");
846    set("b", "b", "b");
847    DiskLruCache.Editor a = cache.get("a").edit();
848    setString(a, 0, "a1");
849    fileSystem.deleteContents(tempDir.getRoot());
850    setString(a, 1, "a2");
851    a.commit();
852    assertNull(cache.get("a"));
853  }
854
855  /** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/2">Issue #2</a> */
856  @Test public void aggressiveClearingHandlesRead() throws Exception {
857    fileSystem.deleteContents(tempDir.getRoot());
858    assertNull(cache.get("a"));
859  }
860
861  /**
862   * We had a long-lived bug where {@link DiskLruCache#trimToSize} could
863   * infinite loop if entries being edited required deletion for the operation
864   * to complete.
865   */
866  @Test public void trimToSizeWithActiveEdit() throws Exception {
867    set("a", "a1234", "a1234");
868    DiskLruCache.Editor a = cache.edit("a");
869    setString(a, 0, "a123");
870
871    cache.setMaxSize(8); // Smaller than the sum of active edits!
872    cache.flush(); // Force trimToSize().
873    assertEquals(0, cache.size());
874    assertNull(cache.get("a"));
875
876    // After the edit is completed, its entry is still gone.
877    setString(a, 1, "a1");
878    a.commit();
879    assertAbsent("a");
880    assertEquals(0, cache.size());
881  }
882
883  @Test public void evictAll() throws Exception {
884    set("a", "a", "a");
885    set("b", "b", "b");
886    cache.evictAll();
887    assertEquals(0, cache.size());
888    assertAbsent("a");
889    assertAbsent("b");
890  }
891
892  @Test public void evictAllWithPartialCreate() throws Exception {
893    DiskLruCache.Editor a = cache.edit("a");
894    setString(a, 0, "a1");
895    setString(a, 1, "a2");
896    cache.evictAll();
897    assertEquals(0, cache.size());
898    a.commit();
899    assertAbsent("a");
900  }
901
902  @Test public void evictAllWithPartialEditDoesNotStoreAValue() throws Exception {
903    set("a", "a", "a");
904    DiskLruCache.Editor a = cache.edit("a");
905    setString(a, 0, "a1");
906    setString(a, 1, "a2");
907    cache.evictAll();
908    assertEquals(0, cache.size());
909    a.commit();
910    assertAbsent("a");
911  }
912
913  @Test public void evictAllDoesntInterruptPartialRead() throws Exception {
914    set("a", "a", "a");
915    DiskLruCache.Snapshot a = cache.get("a");
916    assertSnapshotValue(a, 0, "a");
917    cache.evictAll();
918    assertEquals(0, cache.size());
919    assertAbsent("a");
920    assertSnapshotValue(a, 1, "a");
921    a.close();
922  }
923
924  @Test public void editSnapshotAfterEvictAllReturnsNullDueToStaleValue() throws Exception {
925    set("a", "a", "a");
926    DiskLruCache.Snapshot a = cache.get("a");
927    cache.evictAll();
928    assertEquals(0, cache.size());
929    assertAbsent("a");
930    assertNull(a.edit());
931    a.close();
932  }
933
934  @Test public void iterator() throws Exception {
935    set("a", "a1", "a2");
936    set("b", "b1", "b2");
937    set("c", "c1", "c2");
938    Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
939
940    assertTrue(iterator.hasNext());
941    DiskLruCache.Snapshot a = iterator.next();
942    assertEquals("a", a.key());
943    assertSnapshotValue(a, 0, "a1");
944    assertSnapshotValue(a, 1, "a2");
945    a.close();
946
947    assertTrue(iterator.hasNext());
948    DiskLruCache.Snapshot b = iterator.next();
949    assertEquals("b", b.key());
950    assertSnapshotValue(b, 0, "b1");
951    assertSnapshotValue(b, 1, "b2");
952    b.close();
953
954    assertTrue(iterator.hasNext());
955    DiskLruCache.Snapshot c = iterator.next();
956    assertEquals("c", c.key());
957    assertSnapshotValue(c, 0, "c1");
958    assertSnapshotValue(c, 1, "c2");
959    c.close();
960
961    assertFalse(iterator.hasNext());
962    try {
963      iterator.next();
964      fail();
965    } catch (NoSuchElementException expected) {
966    }
967  }
968
969  @Test public void iteratorElementsAddedDuringIterationAreOmitted() throws Exception {
970    set("a", "a1", "a2");
971    set("b", "b1", "b2");
972    Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
973
974    DiskLruCache.Snapshot a = iterator.next();
975    assertEquals("a", a.key());
976    a.close();
977
978    set("c", "c1", "c2");
979
980    DiskLruCache.Snapshot b = iterator.next();
981    assertEquals("b", b.key());
982    b.close();
983
984    assertFalse(iterator.hasNext());
985  }
986
987  @Test public void iteratorElementsUpdatedDuringIterationAreUpdated() throws Exception {
988    set("a", "a1", "a2");
989    set("b", "b1", "b2");
990    Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
991
992    DiskLruCache.Snapshot a = iterator.next();
993    assertEquals("a", a.key());
994    a.close();
995
996    set("b", "b3", "b4");
997
998    DiskLruCache.Snapshot b = iterator.next();
999    assertEquals("b", b.key());
1000    assertSnapshotValue(b, 0, "b3");
1001    assertSnapshotValue(b, 1, "b4");
1002    b.close();
1003  }
1004
1005  @Test public void iteratorElementsRemovedDuringIterationAreOmitted() throws Exception {
1006    set("a", "a1", "a2");
1007    set("b", "b1", "b2");
1008    Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
1009
1010    cache.remove("b");
1011
1012    DiskLruCache.Snapshot a = iterator.next();
1013    assertEquals("a", a.key());
1014    a.close();
1015
1016    assertFalse(iterator.hasNext());
1017  }
1018
1019  @Test public void iteratorRemove() throws Exception {
1020    set("a", "a1", "a2");
1021    Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
1022
1023    DiskLruCache.Snapshot a = iterator.next();
1024    a.close();
1025    iterator.remove();
1026
1027    assertEquals(null, cache.get("a"));
1028  }
1029
1030  @Test public void iteratorRemoveBeforeNext() throws Exception {
1031    set("a", "a1", "a2");
1032    Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
1033    try {
1034      iterator.remove();
1035      fail();
1036    } catch (IllegalStateException expected) {
1037    }
1038  }
1039
1040  @Test public void iteratorRemoveOncePerCallToNext() throws Exception {
1041    set("a", "a1", "a2");
1042    Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
1043
1044    DiskLruCache.Snapshot a = iterator.next();
1045    iterator.remove();
1046    a.close();
1047
1048    try {
1049      iterator.remove();
1050      fail();
1051    } catch (IllegalStateException expected) {
1052    }
1053  }
1054
1055  @Test public void cacheClosedTruncatesIterator() throws Exception {
1056    set("a", "a1", "a2");
1057    Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
1058    cache.close();
1059    assertFalse(iterator.hasNext());
1060  }
1061
1062  @Test public void isClosed_uninitializedCache() throws Exception {
1063    // Create an uninitialized cache.
1064    cache = new DiskLruCache(fileSystem, cacheDir, appVersion, 2, Integer.MAX_VALUE, executor);
1065    toClose.add(cache);
1066
1067    assertFalse(cache.isClosed());
1068    cache.close();
1069    assertTrue(cache.isClosed());
1070  }
1071
1072  @Test public void journalWriteFailsDuringEdit() throws Exception {
1073    set("a", "a", "a");
1074    set("b", "b", "b");
1075
1076    // We can't begin the edit if writing 'DIRTY' fails.
1077    fileSystem.setFaulty(journalFile, true);
1078    assertNull(cache.edit("c"));
1079
1080    // Once the journal has a failure, subsequent writes aren't permitted.
1081    fileSystem.setFaulty(journalFile, false);
1082    assertNull(cache.edit("d"));
1083
1084    // Confirm that the fault didn't corrupt entries stored before the fault was introduced.
1085    cache.close();
1086    cache = new DiskLruCache(fileSystem, cacheDir, appVersion, 2, Integer.MAX_VALUE, executor);
1087    assertValue("a", "a", "a");
1088    assertValue("b", "b", "b");
1089    assertAbsent("c");
1090    assertAbsent("d");
1091  }
1092
1093  /**
1094   * We had a bug where the cache was left in an inconsistent state after a journal write failed.
1095   * https://github.com/square/okhttp/issues/1211
1096   */
1097  @Test public void journalWriteFailsDuringEditorCommit() throws Exception {
1098    set("a", "a", "a");
1099    set("b", "b", "b");
1100
1101    // Create an entry that fails to write to the journal during commit.
1102    DiskLruCache.Editor editor = cache.edit("c");
1103    setString(editor, 0, "c");
1104    setString(editor, 1, "c");
1105    fileSystem.setFaulty(journalFile, true);
1106    editor.commit();
1107
1108    // Once the journal has a failure, subsequent writes aren't permitted.
1109    fileSystem.setFaulty(journalFile, false);
1110    assertNull(cache.edit("d"));
1111
1112    // Confirm that the fault didn't corrupt entries stored before the fault was introduced.
1113    cache.close();
1114    cache = new DiskLruCache(fileSystem, cacheDir, appVersion, 2, Integer.MAX_VALUE, executor);
1115    assertValue("a", "a", "a");
1116    assertValue("b", "b", "b");
1117    assertAbsent("c");
1118    assertAbsent("d");
1119  }
1120
1121  @Test public void journalWriteFailsDuringEditorAbort() throws Exception {
1122    set("a", "a", "a");
1123    set("b", "b", "b");
1124
1125    // Create an entry that fails to write to the journal during abort.
1126    DiskLruCache.Editor editor = cache.edit("c");
1127    setString(editor, 0, "c");
1128    setString(editor, 1, "c");
1129    fileSystem.setFaulty(journalFile, true);
1130    editor.abort();
1131
1132    // Once the journal has a failure, subsequent writes aren't permitted.
1133    fileSystem.setFaulty(journalFile, false);
1134    assertNull(cache.edit("d"));
1135
1136    // Confirm that the fault didn't corrupt entries stored before the fault was introduced.
1137    cache.close();
1138    cache = new DiskLruCache(fileSystem, cacheDir, appVersion, 2, Integer.MAX_VALUE, executor);
1139    assertValue("a", "a", "a");
1140    assertValue("b", "b", "b");
1141    assertAbsent("c");
1142    assertAbsent("d");
1143  }
1144
1145  @Test public void journalWriteFailsDuringRemove() throws Exception {
1146    set("a", "a", "a");
1147    set("b", "b", "b");
1148
1149    // Remove, but the journal write will fail.
1150    fileSystem.setFaulty(journalFile, true);
1151    assertTrue(cache.remove("a"));
1152
1153    // Confirm that the entry was still removed.
1154    fileSystem.setFaulty(journalFile, false);
1155    cache.close();
1156    cache = new DiskLruCache(fileSystem, cacheDir, appVersion, 2, Integer.MAX_VALUE, executor);
1157    assertAbsent("a");
1158    assertValue("b", "b", "b");
1159  }
1160
1161  private void assertJournalEquals(String... expectedBodyLines) throws Exception {
1162    List<String> expectedLines = new ArrayList<>();
1163    expectedLines.add(MAGIC);
1164    expectedLines.add(VERSION_1);
1165    expectedLines.add("100");
1166    expectedLines.add("2");
1167    expectedLines.add("");
1168    expectedLines.addAll(Arrays.asList(expectedBodyLines));
1169    assertEquals(expectedLines, readJournalLines());
1170  }
1171
1172  private void createJournal(String... bodyLines) throws Exception {
1173    createJournalWithHeader(MAGIC, VERSION_1, "100", "2", "", bodyLines);
1174  }
1175
1176  private void createJournalWithHeader(String magic, String version, String appVersion,
1177      String valueCount, String blank, String... bodyLines) throws Exception {
1178    BufferedSink sink = Okio.buffer(fileSystem.sink(journalFile));
1179    sink.writeUtf8(magic + "\n");
1180    sink.writeUtf8(version + "\n");
1181    sink.writeUtf8(appVersion + "\n");
1182    sink.writeUtf8(valueCount + "\n");
1183    sink.writeUtf8(blank + "\n");
1184    for (String line : bodyLines) {
1185      sink.writeUtf8(line);
1186      sink.writeUtf8("\n");
1187    }
1188    sink.close();
1189  }
1190
1191  private List<String> readJournalLines() throws Exception {
1192    List<String> result = new ArrayList<>();
1193    BufferedSource source = Okio.buffer(fileSystem.source(journalFile));
1194    for (String line; (line = source.readUtf8Line()) != null; ) {
1195      result.add(line);
1196    }
1197    source.close();
1198    return result;
1199  }
1200
1201  private File getCleanFile(String key, int index) {
1202    return new File(cacheDir, key + "." + index);
1203  }
1204
1205  private File getDirtyFile(String key, int index) {
1206    return new File(cacheDir, key + "." + index + ".tmp");
1207  }
1208
1209  private String readFile(File file) throws Exception {
1210    BufferedSource source = Okio.buffer(fileSystem.source(file));
1211    String result = source.readUtf8();
1212    source.close();
1213    return result;
1214  }
1215
1216  public void writeFile(File file, String content) throws Exception {
1217    BufferedSink sink = Okio.buffer(fileSystem.sink(file));
1218    sink.writeUtf8(content);
1219    sink.close();
1220  }
1221
1222  private static void assertInoperable(DiskLruCache.Editor editor) throws Exception {
1223    try {
1224      setString(editor, 0, "A");
1225      fail();
1226    } catch (IllegalStateException expected) {
1227    }
1228    try {
1229      editor.newSource(0);
1230      fail();
1231    } catch (IllegalStateException expected) {
1232    }
1233    try {
1234      editor.newSink(0);
1235      fail();
1236    } catch (IllegalStateException expected) {
1237    }
1238    try {
1239      editor.commit();
1240      fail();
1241    } catch (IllegalStateException expected) {
1242    }
1243    try {
1244      editor.abort();
1245      fail();
1246    } catch (IllegalStateException expected) {
1247    }
1248  }
1249
1250  private void generateSomeGarbageFiles() throws Exception {
1251    File dir1 = new File(cacheDir, "dir1");
1252    File dir2 = new File(dir1, "dir2");
1253    writeFile(getCleanFile("g1", 0), "A");
1254    writeFile(getCleanFile("g1", 1), "B");
1255    writeFile(getCleanFile("g2", 0), "C");
1256    writeFile(getCleanFile("g2", 1), "D");
1257    writeFile(getCleanFile("g2", 1), "D");
1258    writeFile(new File(cacheDir, "otherFile0"), "E");
1259    writeFile(new File(dir2, "otherFile1"), "F");
1260  }
1261
1262  private void assertGarbageFilesAllDeleted() throws Exception {
1263    assertFalse(fileSystem.exists(getCleanFile("g1", 0)));
1264    assertFalse(fileSystem.exists(getCleanFile("g1", 1)));
1265    assertFalse(fileSystem.exists(getCleanFile("g2", 0)));
1266    assertFalse(fileSystem.exists(getCleanFile("g2", 1)));
1267    assertFalse(fileSystem.exists(new File(cacheDir, "otherFile0")));
1268    assertFalse(fileSystem.exists(new File(cacheDir, "dir1")));
1269  }
1270
1271  private void set(String key, String value0, String value1) throws Exception {
1272    DiskLruCache.Editor editor = cache.edit(key);
1273    setString(editor, 0, value0);
1274    setString(editor, 1, value1);
1275    editor.commit();
1276  }
1277
1278  public static void setString(DiskLruCache.Editor editor, int index, String value) throws IOException {
1279    BufferedSink writer = Okio.buffer(editor.newSink(index));
1280    writer.writeUtf8(value);
1281    writer.close();
1282  }
1283
1284  private void assertAbsent(String key) throws Exception {
1285    DiskLruCache.Snapshot snapshot = cache.get(key);
1286    if (snapshot != null) {
1287      snapshot.close();
1288      fail();
1289    }
1290    assertFalse(fileSystem.exists(getCleanFile(key, 0)));
1291    assertFalse(fileSystem.exists(getCleanFile(key, 1)));
1292    assertFalse(fileSystem.exists(getDirtyFile(key, 0)));
1293    assertFalse(fileSystem.exists(getDirtyFile(key, 1)));
1294  }
1295
1296  private void assertValue(String key, String value0, String value1) throws Exception {
1297    DiskLruCache.Snapshot snapshot = cache.get(key);
1298    assertSnapshotValue(snapshot, 0, value0);
1299    assertSnapshotValue(snapshot, 1, value1);
1300    assertTrue(fileSystem.exists(getCleanFile(key, 0)));
1301    assertTrue(fileSystem.exists(getCleanFile(key, 1)));
1302    snapshot.close();
1303  }
1304
1305  private void assertSnapshotValue(DiskLruCache.Snapshot snapshot, int index, String value)
1306      throws IOException {
1307    assertEquals(value, sourceAsString(snapshot.getSource(index)));
1308    assertEquals(value.length(), snapshot.getLength(index));
1309  }
1310
1311  private String sourceAsString(Source source) throws IOException {
1312    return source != null ? Okio.buffer(source).readUtf8() : null;
1313  }
1314
1315  private void copyFile(File from, File to) throws IOException {
1316    Source source = fileSystem.source(from);
1317    BufferedSink sink = Okio.buffer(fileSystem.sink(to));
1318    sink.writeAll(source);
1319    source.close();
1320    sink.close();
1321  }
1322
1323  private static class TestExecutor implements Executor {
1324    final Deque<Runnable> jobs = new ArrayDeque<>();
1325
1326    @Override public void execute(Runnable command) {
1327      jobs.addLast(command);
1328    }
1329  }
1330}
1331