Back to Repositories

Testing DiskLruCache Implementation in Glide Library

The DiskLruCacheTest class provides comprehensive testing of disk-based LRU caching functionality in the Glide library, verifying cache operations, eviction policies, and data persistence across sessions. It ensures reliable caching behavior for image loading and resource management.

Test Coverage Overview

The test suite provides extensive coverage of the DiskLruCache implementation, including:
  • Basic cache operations (read, write, delete)
  • Cache size management and eviction policies
  • File system interactions and journal management
  • Error handling and recovery scenarios
  • Cache persistence across sessions

Implementation Analysis

The testing approach utilizes JUnit4 with a combination of unit and integration tests. It employs temporary file system operations and mock objects to verify cache behavior in isolation. The implementation validates both successful operations and edge cases through systematic test methods.

Technical Details

Key technical components include:
  • JUnit4 test framework
  • TemporaryFolder test rule for file management
  • Truth assertion library for validation
  • Custom file system utilities
  • Mock objects for isolation testing

Best Practices Demonstrated

The test suite exemplifies testing best practices through:
  • Comprehensive setup and teardown management
  • Isolation of test cases
  • Thorough edge case coverage
  • Clear test method naming
  • Effective use of test utilities and helpers

bumptech/glide

third_party/disklrucache/src/test/java/com/bumptech/glide/disklrucache/DiskLruCacheTest.java

            
/*
 * 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.bumptech.glide.disklrucache;

import static com.bumptech.glide.disklrucache.DiskLruCache.JOURNAL_FILE;
import static com.bumptech.glide.disklrucache.DiskLruCache.JOURNAL_FILE_BACKUP;
import static com.bumptech.glide.disklrucache.DiskLruCache.MAGIC;
import static com.bumptech.glide.disklrucache.DiskLruCache.VERSION_1;
import static com.google.common.truth.Fact.simpleFact;
import static com.google.common.truth.Truth.assertThat;
import static org.hamcrest.core.IsNot.not;
import static org.junit.Assume.assumeThat;

import com.google.common.truth.ComparableSubject;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.Subject;
import com.google.common.truth.Truth;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.hamcrest.core.StringStartsWith;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public final class DiskLruCacheTest {
  private final int appVersion = 100;
  private File cacheDir;
  private File journalFile;
  private File journalBkpFile;
  private DiskLruCache cache;

  @Rule public TemporaryFolder tempDir = new TemporaryFolder();

  @BeforeClass
  public static void setUpClass() {
    assumeThat(System.getProperty("os.name"), not(StringStartsWith.startsWith("Windows")));
  }

  @Before public void setUp() throws Exception {
    cacheDir = tempDir.newFolder("DiskLruCacheTest");
    journalFile = new File(cacheDir, JOURNAL_FILE);
    journalBkpFile = new File(cacheDir, JOURNAL_FILE_BACKUP);
    for (File file : cacheDir.listFiles()) {
      file.delete();
    }
    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
  }

  @After public void tearDown() throws Exception {
    cache.close();
  }

  @Test public void emptyCache() throws Exception {
    cache.close();
    assertJournalEquals();
  }

  @Test public void writeAndReadEntry() throws Exception {
    DiskLruCache.Editor creator = cache.edit("k1");
    creator.set(0, "ABC");
    creator.set(1, "DE");
    assertThat(creator.getString(0)).isNull();
    assertThat(creator.getString(1)).isNull();
    creator.commit();

    DiskLruCache.Value value = cache.get("k1");
    assertThat(value.getString(0)).isEqualTo("ABC");
    assertThat(value.getLength(0)).isEqualTo(3);
    assertThat(value.getString(1)).isEqualTo("DE");
    assertThat(value.getLength(1)).isEqualTo(2);
  }

  @Test public void readAndWriteEntryAcrossCacheOpenAndClose() throws Exception {
    DiskLruCache.Editor creator = cache.edit("k1");
    creator.set(0, "A");
    creator.set(1, "B");
    creator.commit();
    cache.close();

    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
    DiskLruCache.Value value = cache.get("k1");
    assertThat(value.getString(0)).isEqualTo("A");
    assertThat(value.getLength(0)).isEqualTo(1);
    assertThat(value.getString(1)).isEqualTo("B");
    assertThat(value.getLength(1)).isEqualTo(1);
  }

  @Test public void readAndWriteEntryWithoutProperClose() throws Exception {
    DiskLruCache.Editor creator = cache.edit("k1");
    creator.set(0, "A");
    creator.set(1, "B");
    creator.commit();

    // Simulate a dirty close of 'cache' by opening the cache directory again.
    DiskLruCache cache2 = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
    DiskLruCache.Value value = cache2.get("k1");
    assertThat(value.getString(0)).isEqualTo("A");
    assertThat(value.getLength(0)).isEqualTo(1);
    assertThat(value.getString(1)).isEqualTo("B");
    assertThat(value.getLength(1)).isEqualTo(1);
    cache2.close();
  }

  @Test public void journalWithEditAndPublish() throws Exception {
    DiskLruCache.Editor creator = cache.edit("k1");
    assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed.
    creator.set(0, "AB");
    creator.set(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.
    creator.set(0, "AB");
    creator.set(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");
    assertThat(cache.get("k1")).isNull();
    creator.set(0, "A");
    creator.set(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");
    k1Creator.set(0, "AB");
    k1Creator.set(1, "C");
    k1Creator.commit();
    DiskLruCache.Editor k2Creator = cache.edit("k2");
    k2Creator.set(0, "DEF");
    k2Creator.set(1, "G");
    k2Creator.commit();
    DiskLruCache.Value k1Value = cache.get("k1");
    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");
    editor.set(0, "A");
    editor.set(1, "B");
    editor.commit();
    assertInoperable(editor);
  }

  @Test public void cannotOperateOnEditAfterRevert() throws Exception {
    DiskLruCache.Editor editor = cache.edit("k1");
    editor.set(0, "A");
    editor.set(1, "B");
    editor.abort();
    assertInoperable(editor);
  }

  @Test public void explicitRemoveAppliedToDiskImmediately() throws Exception {
    DiskLruCache.Editor editor = cache.edit("k1");
    editor.set(0, "ABC");
    editor.set(1, "B");
    editor.commit();
    File k1 = getCleanFile("k1", 0);
    assertThat(readFile(k1)).isEqualTo("ABC");
    cache.remove("k1");
    assertThat(k1.exists()).isFalse();
  }

  @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");
    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
    assertThat(cleanFile0.exists()).isFalse();
    assertThat(cleanFile1.exists()).isFalse();
    assertThat(dirtyFile0.exists()).isFalse();
    assertThat(dirtyFile1.exists()).isFalse();
    assertThat(cache.get("k1")).isNull();
  }

  @Test public void openWithInvalidVersionClearsDirectory() throws Exception {
    cache.close();
    generateSomeGarbageFiles();
    createJournalWithHeader(MAGIC, "0", "100", "2", "");
    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
    assertGarbageFilesAllDeleted();
  }

  @Test public void openWithInvalidAppVersionClearsDirectory() throws Exception {
    cache.close();
    generateSomeGarbageFiles();
    createJournalWithHeader(MAGIC, "1", "101", "2", "");
    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
    assertGarbageFilesAllDeleted();
  }

  @Test public void openWithInvalidValueCountClearsDirectory() throws Exception {
    cache.close();
    generateSomeGarbageFiles();
    createJournalWithHeader(MAGIC, "1", "100", "1", "");
    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
    assertGarbageFilesAllDeleted();
  }

  @Test public void openWithInvalidBlankLineClearsDirectory() throws Exception {
    cache.close();
    generateSomeGarbageFiles();
    createJournalWithHeader(MAGIC, "1", "100", "2", "x");
    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
    assertGarbageFilesAllDeleted();
  }

  @Test public void openWithInvalidJournalLineClearsDirectory() throws Exception {
    cache.close();
    generateSomeGarbageFiles();
    createJournal("CLEAN k1 1 1", "BOGUS");
    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
    assertGarbageFilesAllDeleted();
    assertThat(cache.get("k1")).isNull();
  }

  @Test public void openWithInvalidFileSizeClearsDirectory() throws Exception {
    cache.close();
    generateSomeGarbageFiles();
    createJournal("CLEAN k1 0000x001 1");
    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
    assertGarbageFilesAllDeleted();
    assertThat(cache.get("k1")).isNull();
  }

  @Test public void openWithTruncatedLineDiscardsThatLine() throws Exception {
    cache.close();
    writeFile(getCleanFile("k1", 0), "A");
    writeFile(getCleanFile("k1", 1), "B");
    Writer writer = new FileWriter(journalFile);
    writer.write(MAGIC + "\n" + VERSION_1 + "\n100\n2\n\nCLEAN k1 1 1"); // no trailing newline
    writer.close();
    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
    assertThat(cache.get("k1")).isNull();

    // The journal is not corrupt when editing after a truncated line.
    set("k1", "C", "D");
    cache.close();
    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
    assertValue("k1", "C", "D");
  }

  @Test public void openWithTooManyFileSizesClearsDirectory() throws Exception {
    cache.close();
    generateSomeGarbageFiles();
    createJournal("CLEAN k1 1 1 1");
    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
    assertGarbageFilesAllDeleted();
    assertThat(cache.get("k1")).isNull();
  }

  @Test public void nullKeyThrows() throws Exception {
    try {
      cache.edit(null);
      Assert.fail();
    } catch (NullPointerException expected) {
    }
  }

  @Test public void createNewEntryWithTooFewValuesFails() throws Exception {
    DiskLruCache.Editor creator = cache.edit("k1");
    creator.set(1, "A");
    try {
      creator.commit();
      Assert.fail();
    } catch (IllegalStateException expected) {
    }

    assertThat(getCleanFile("k1", 0).exists()).isFalse();
    assertThat(getCleanFile("k1", 1).exists()).isFalse();
    assertThat(getDirtyFile("k1", 0).exists()).isFalse();
    assertThat(getDirtyFile("k1", 1).exists()).isFalse();
    assertThat(cache.get("k1")).isNull();

    DiskLruCache.Editor creator2 = cache.edit("k1");
    creator2.set(0, "B");
    creator2.set(1, "C");
    creator2.commit();
  }

  @Test public void revertWithTooFewValues() throws Exception {
    DiskLruCache.Editor creator = cache.edit("k1");
    creator.set(1, "A");
    creator.abort();
    assertThat(getCleanFile("k1", 0).exists()).isFalse();
    assertThat(getCleanFile("k1", 1).exists()).isFalse();
    assertThat(getDirtyFile("k1", 0).exists()).isFalse();
    assertThat(getDirtyFile("k1", 1).exists()).isFalse();
    assertThat(cache.get("k1")).isNull();
  }

  @Test public void updateExistingEntryWithTooFewValuesReusesPreviousValues() throws Exception {
    DiskLruCache.Editor creator = cache.edit("k1");
    creator.set(0, "A");
    creator.set(1, "B");
    creator.commit();

    DiskLruCache.Editor updater = cache.edit("k1");
    updater.set(0, "C");
    updater.commit();

    DiskLruCache.Value value = cache.get("k1");
    assertThat(value.getString(0)).isEqualTo("C");
    assertThat(value.getLength(0)).isEqualTo(1);
    assertThat(value.getString(1)).isEqualTo("B");
    assertThat(value.getLength(1)).isEqualTo(1);
  }

  @Test public void growMaxSize() throws Exception {
    cache.close();
    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
    set("a", "a", "aaa"); // size 4
    set("b", "bb", "bbbb"); // size 6
    cache.setMaxSize(20);
    set("c", "c", "c"); // size 12
    assertThat(cache.size()).isEqualTo(12);
  }

  @Test public void shrinkMaxSizeEvicts() throws Exception {
    cache.close();
    cache = DiskLruCache.open(cacheDir, appVersion, 2, 20);
    set("a", "a", "aaa"); // size 4
    set("b", "bb", "bbbb"); // size 6
    set("c", "c", "c"); // size 12
    cache.setMaxSize(10);
    cache.executorService.shutdown();
    cache.executorService.awaitTermination(500, TimeUnit.MILLISECONDS);
    assertThat(cache.size()).isEqualTo(8 /* 12 - 4 */);
  }

  @Test public void evictOnInsert() throws Exception {
    cache.close();
    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);

    set("a", "a", "aaa"); // size 4
    set("b", "bb", "bbbb"); // size 6
    assertThat(cache.size()).isEqualTo(10);

    // Cause the size to grow to 12 should evict 'A'.
    set("c", "c", "c");
    cache.flush();
    assertThat(cache.size()).isEqualTo(8);
    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();
    assertThat(cache.size()).isEqualTo(10);
    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();
    assertThat(cache.size()).isEqualTo(10);
    assertAbsent("a");
    assertAbsent("b");
    assertAbsent("c");
    assertValue("d", "d", "d");
    assertValue("e", "eeee", "eeee");
  }

  @Test public void evictOnUpdate() throws Exception {
    cache.close();
    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);

    set("a", "a", "aa"); // size 3
    set("b", "b", "bb"); // size 3
    set("c", "c", "cc"); // size 3
    assertThat(cache.size()).isEqualTo(9);

    // Causing the size to grow to 11 should evict 'A'.
    set("b", "b", "bbbb");
    cache.flush();
    assertThat(cache.size()).isEqualTo(8);
    assertAbsent("a");
    assertValue("b", "b", "bbbb");
    assertValue("c", "c", "cc");
  }

  @Test public void evictionHonorsLruFromCurrentSession() throws Exception {
    cache.close();
    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
    set("a", "a", "a");
    set("b", "b", "b");
    set("c", "c", "c");
    set("d", "d", "d");
    set("e", "e", "e");
    cache.get("b"); // '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();
    assertThat(cache.size()).isEqualTo(10);
    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"); // 'B' is now least recently used.
    assertThat(cache.size()).isEqualTo(12);
    cache.close();
    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);

    set("g", "g", "g");
    cache.flush();
    assertThat(cache.size()).isEqualTo(10);
    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();
    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
    set("a", "aaaaa", "aaaaaa"); // size=11
    cache.flush();
    assertAbsent("a");
  }

  @Test public void cacheSingleValueOfSizeGreaterThanMaxSize() throws Exception {
    cache.close();
    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
    set("a", "aaaaaaaaaaa", "a"); // size=12
    cache.flush();
    assertAbsent("a");
  }

  @Test public void constructorDoesNotAllowZeroCacheSize() throws Exception {
    try {
      DiskLruCache.open(cacheDir, appVersion, 2, 0);
      Assert.fail();
    } catch (IllegalArgumentException expected) {
    }
  }

  @Test public void constructorDoesNotAllowZeroValuesPerEntry() throws Exception {
    try {
      DiskLruCache.open(cacheDir, appVersion, 0, 10);
      Assert.fail();
    } catch (IllegalArgumentException expected) {
    }
  }

  @Test public void removeAbsentElement() throws Exception {
    cache.remove("a");
  }

  @Test public void readingTheSameFileMultipleTimes() throws Exception {
    set("a", "a", "b");
    DiskLruCache.Value value = cache.get("a");
    assertThat(value.getFile(0)).isSameInstanceAs(value.getFile(0));
  }

  @Test public void rebuildJournalOnRepeatedReads() throws Exception {
    set("a", "a", "a");
    set("b", "b", "b");
    long lastJournalLength = 0;
    while (true) {
      long journalLength = journalFile.length();
      assertValue("a", "a", "a");
      assertValue("b", "b", "b");
      if (journalLength < lastJournalLength) {
        System.out
            .printf("Journal compacted from %s bytes to %s bytes\n", lastJournalLength,
                journalLength);
        break; // Test passed!
      }
      lastJournalLength = journalLength;
    }
  }

  @Test public void rebuildJournalOnRepeatedEdits() throws Exception {
    long lastJournalLength = 0;
    while (true) {
      long journalLength = journalFile.length();
      set("a", "a", "a");
      set("b", "b", "b");
      if (journalLength < lastJournalLength) {
        System.out
            .printf("Journal compacted from %s bytes to %s bytes\n", lastJournalLength,
                journalLength);
        break;
      }
      lastJournalLength = journalLength;
    }

    // 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");
    long lastJournalLength = 0;
    while (true) {
      long journalLength = journalFile.length();
      assertValue("a", "a", "a");
      assertValue("b", "b", "b");
      cache.close();
      cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
      if (journalLength < lastJournalLength) {
        System.out
            .printf("Journal compacted from %s bytes to %s bytes\n", lastJournalLength,
                journalLength);
        break; // Test passed!
      }
      lastJournalLength = journalLength;
    }
  }

  /** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/28">Issue #28</a> */
  @Test public void rebuildJournalOnRepeatedEditsWithOpenAndClose() throws Exception {
    long lastJournalLength = 0;
    while (true) {
      long journalLength = journalFile.length();
      set("a", "a", "a");
      set("b", "b", "b");
      cache.close();
      cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
      if (journalLength < lastJournalLength) {
        System.out
            .printf("Journal compacted from %s bytes to %s bytes\n", lastJournalLength,
                journalLength);
        break;
      }
      lastJournalLength = journalLength;
    }
  }

  @Test public void restoreBackupFile() throws Exception {
    DiskLruCache.Editor creator = cache.edit("k1");
    creator.set(0, "ABC");
    creator.set(1, "DE");
    creator.commit();
    cache.close();

    assertThat(journalFile.renameTo(journalBkpFile)).isTrue();
    assertThat(journalFile.exists()).isFalse();

    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);

    DiskLruCache.Value value = cache.get("k1");
    assertThat(value.getString(0)).isEqualTo("ABC");
    assertThat(value.getLength(0)).isEqualTo(3);
    assertThat(value.getString(1)).isEqualTo("DE");
    assertThat(value.getLength(1)).isEqualTo(2);

    assertThat(journalBkpFile.exists()).isFalse();
    assertThat(journalFile.exists()).isTrue();
  }

  @Test public void journalFileIsPreferredOverBackupFile() throws Exception {
    DiskLruCache.Editor creator = cache.edit("k1");
    creator.set(0, "ABC");
    creator.set(1, "DE");
    creator.commit();
    cache.flush();

    Files.copy(journalFile.toPath(), journalBkpFile.toPath());

    creator = cache.edit("k2");
    creator.set(0, "F");
    creator.set(1, "GH");
    creator.commit();
    cache.close();

    assertThat(journalFile.exists()).isTrue();
    assertThat(journalBkpFile.exists()).isTrue();

    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);

    DiskLruCache.Value valueA = cache.get("k1");
    assertThat(valueA.getString(0)).isEqualTo("ABC");
    assertThat(valueA.getLength(0)).isEqualTo(3);
    assertThat(valueA.getString(1)).isEqualTo("DE");
    assertThat(valueA.getLength(1)).isEqualTo(2);

    DiskLruCache.Value valueB = cache.get("k2");
    assertThat(valueB.getString(0)).isEqualTo("F");
    assertThat(valueB.getLength(0)).isEqualTo(1);
    assertThat(valueB.getString(1)).isEqualTo("GH");
    assertThat(valueB.getLength(1)).isEqualTo(2);

    assertThat(journalBkpFile.exists()).isFalse();
    assertThat(journalFile.exists()).isTrue();
  }

  @Test public void openCreatesDirectoryIfNecessary() throws Exception {
    cache.close();
    File dir = tempDir.newFolder("testOpenCreatesDirectoryIfNecessary");
    cache = DiskLruCache.open(dir, appVersion, 2, Integer.MAX_VALUE);
    set("a", "a", "a");
    assertThat(new File(dir, "a.0").exists()).isTrue();
    assertThat(new File(dir, "a.1").exists()).isTrue();
    assertThat(new File(dir, "journal").exists()).isTrue();
  }

  @Test public void fileDeletedExternally() throws Exception {
    set("a", "a", "a");
    getCleanFile("a", 1).delete();
    assertThat(cache.get("a")).isNull();
  }

  @Test public void editSameVersion() throws Exception {
    set("a", "a", "a");
    DiskLruCache.Value value = cache.get("a");
    DiskLruCache.Editor editor = value.edit();
    editor.set(1, "a2");
    editor.commit();
    assertValue("a", "a", "a2");
  }

  @Test public void editSnapshotAfterChangeAborted() throws Exception {
    set("a", "a", "a");
    DiskLruCache.Value value = cache.get("a");
    DiskLruCache.Editor toAbort = value.edit();
    toAbort.set(0, "b");
    toAbort.abort();
    DiskLruCache.Editor editor = value.edit();
    editor.set(1, "a2");
    editor.commit();
    assertValue("a", "a", "a2");
  }

  @Test public void editSnapshotAfterChangeCommitted() throws Exception {
    set("a", "a", "a");
    DiskLruCache.Value value = cache.get("a");
    DiskLruCache.Editor toAbort = value.edit();
    toAbort.set(0, "b");
    toAbort.commit();
    assertThat(value.edit()).isNull();
  }

  @Test public void editSinceEvicted() throws Exception {
    cache.close();
    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
    set("a", "aa", "aaa"); // size 5
    DiskLruCache.Value value = cache.get("a");
    set("b", "bb", "bbb"); // size 5
    set("c", "cc", "ccc"); // size 5; will evict 'A'
    cache.flush();
    assertThat(value.edit()).isNull();
  }

  @Test public void editSinceEvictedAndRecreated() throws Exception {
    cache.close();
    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
    set("a", "aa", "aaa"); // size 5
    DiskLruCache.Value value = 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();
    assertThat(value.edit()).isNull();
  }

  /** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/2">Issue #2</a> */
  @Test public void aggressiveClearingHandlesWrite() throws Exception {
    deleteDirectory(cacheDir);
    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();
    deleteDirectory(cacheDir);
    a.set(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();
    a.set(0, "a1");
    deleteDirectory(cacheDir);
    a.set(1, "a2");
    a.commit();
    assertThat(cache.get("a")).isNull();
  }

  /** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/2">Issue #2</a> */
  @Test public void aggressiveClearingHandlesRead() throws Exception {
    deleteDirectory(cacheDir);
    assertThat(cache.get("a")).isNull();
  }

  private void assertJournalEquals(String... expectedBodyLines) throws Exception {
    List<String> expectedLines = new ArrayList<String>();
    expectedLines.add(MAGIC);
    expectedLines.add(VERSION_1);
    expectedLines.add("100");
    expectedLines.add("2");
    expectedLines.add("");
    expectedLines.addAll(Arrays.asList(expectedBodyLines));
    assertThat(readJournalLines()).isEqualTo(expectedLines);
  }

  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 {
    Writer writer = new FileWriter(journalFile);
    writer.write(magic + "\n");
    writer.write(version + "\n");
    writer.write(appVersion + "\n");
    writer.write(valueCount + "\n");
    writer.write(blank + "\n");
    for (String line : bodyLines) {
      writer.write(line);
      writer.write('\n');
    }
    writer.close();
  }

  private List<String> readJournalLines() throws Exception {
    List<String> result = new ArrayList<String>();
    BufferedReader reader = new BufferedReader(new FileReader(journalFile));
    String line;
    while ((line = reader.readLine()) != null) {
      result.add(line);
    }
    reader.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 static String readFile(File file) throws Exception {
    Reader reader = new FileReader(file);
    StringWriter writer = new StringWriter();
    char[] buffer = new char[1024];
    int count;
    while ((count = reader.read(buffer)) != -1) {
      writer.write(buffer, 0, count);
    }
    reader.close();
    return writer.toString();
  }

  public static void writeFile(File file, String content) throws Exception {
    FileWriter writer = new FileWriter(file);
    writer.write(content);
    writer.close();
  }

  private static void assertInoperable(DiskLruCache.Editor editor) throws Exception {
    try {
      editor.getString(0);
      Assert.fail();
    } catch (IllegalStateException expected) {
    }
    try {
      editor.set(0, "A");
      Assert.fail();
    } catch (IllegalStateException expected) {
    }
    try {
      editor.getFile(0);
      Assert.fail();
    } catch (IllegalStateException expected) {
    }
    try {
      editor.commit();
      Assert.fail();
    } catch (IllegalStateException expected) {
    }
    try {
      editor.abort();
      Assert.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");
    dir1.mkdir();
    dir2.mkdir();
    writeFile(new File(dir2, "otherFile1"), "F");
  }

  private void assertGarbageFilesAllDeleted() throws Exception {
    FileSubject.assertThat(getCleanFile("g1", 0)).doesNotExist();
    FileSubject.assertThat(getCleanFile("g1", 1)).doesNotExist();
    FileSubject.assertThat(getCleanFile("g2", 0)).doesNotExist();
    FileSubject.assertThat(getCleanFile("g2", 1)).doesNotExist();
    FileSubject.assertThat(new File(cacheDir, "otherFile0")).doesNotExist();
    FileSubject.assertThat(new File(cacheDir, "dir1")).doesNotExist();
  }

  private void set(String key, String value0, String value1) throws Exception {
    DiskLruCache.Editor editor = cache.edit(key);
    editor.set(0, value0);
    editor.set(1, value1);
    editor.commit();
  }

  private void assertAbsent(String key) throws Exception {
    DiskLruCache.Value value = cache.get(key);
    if (value != null) {
      Assert.fail();
    }
    FileSubject.assertThat(getCleanFile(key, 0)).doesNotExist();
    FileSubject.assertThat(getCleanFile(key, 1)).doesNotExist();
    FileSubject.assertThat(getDirtyFile(key, 0)).doesNotExist();
    FileSubject.assertThat(getDirtyFile(key, 1)).doesNotExist();
  }

  private void assertValue(String key, String value0, String value1) throws Exception {
    DiskLruCache.Value value = cache.get(key);
    assertThat(value.getString(0)).isEqualTo(value0);
    assertThat(value.getLength(0)).isEqualTo(value0.length());
    assertThat(value.getString(1)).isEqualTo(value1);
    assertThat(value.getLength(1)).isEqualTo(value1.length());
    FileSubject.assertThat(getCleanFile(key, 0)).exists();
    FileSubject.assertThat(getCleanFile(key, 1)).exists();
  }

  private static void deleteDirectory(File file) {
    if (file.isDirectory()) {
      File[] children = file.listFiles();
      if (children != null && children.length > 0) {
        for (File child : children) {
          deleteDirectory(child);
        }
      }
    }
    assertThat(!file.exists() || file.delete()).isTrue();
  }

  // TODO(b/134664588): Remove after go/truth-subject-lsc
  @SuppressWarnings({"rawtypes", "unchecked"})
  static final class FileSubject extends ComparableSubject {
    private static final Subject.Factory<FileSubject, File> FACTORY =
        new Subject.Factory<FileSubject, File>() {
          @Override
          public FileSubject createSubject(FailureMetadata metadata, File actual) {
            return new FileSubject(metadata, actual);
          }
        };
    private final File actual;

    static FileSubject assertThat(File file) {
      return Truth.assertAbout(FACTORY).that(file);
    }

    protected FileSubject(FailureMetadata metadata, File actual) {
      super(metadata, actual);
      this.actual = actual;
    }

    public void doesNotExist() {
      if (actual.exists()) {
        failWithActual(simpleFact("expected to not exist"));
      }
    }

    public void exists() {
      if (!actual.exists()) {
        failWithActual(simpleFact("expected to exist"));
      }
    }
  }
}