Back to Repositories

Testing CachedContentIndex Operations in SmartTube ExoPlayer Cache System

This test suite validates the CachedContentIndex class functionality in ExoPlayer’s caching system, focusing on content storage, retrieval, and metadata management. The tests ensure proper handling of cached content indexing, encryption, and file operations.

Test Coverage Overview

The test suite provides comprehensive coverage of CachedContentIndex operations including:
  • Content addition, retrieval and removal operations
  • Legacy storage format compatibility (V1 and V2)
  • Index encryption and key management
  • Cache ID assignment and key mapping
  • Empty content removal and locking mechanisms

Implementation Analysis

The testing approach uses JUnit and AndroidJUnit4 runner for Android-specific testing. Tests implement a systematic verification of core caching operations through:
  • Direct API method validation
  • File system interactions
  • Encryption handling
  • Legacy format compatibility checks

Technical Details

Testing infrastructure includes:
  • JUnit and AndroidJUnit4 test framework
  • Test utilities for file operations
  • Temporary cache directory management
  • Mock database provider
  • Binary test data for legacy format validation

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Thorough setup and teardown procedures
  • Comprehensive edge case handling
  • Encryption validation
  • File system cleanup
  • Isolated test cases with clear assertions

yuliskov/smarttube

exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java

            
/*
 * Copyright (C) 2018 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.google.android.exoplayer2.upstream.cache;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import android.net.Uri;
import androidx.annotation.Nullable;
import android.util.SparseArray;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.Set;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

/** Tests {@link CachedContentIndex}. */
@RunWith(AndroidJUnit4.class)
public class CachedContentIndexTest {

  private final byte[] testIndexV1File = {
      0, 0, 0, 1, // version
      0, 0, 0, 0, // flags
      0, 0, 0, 2, // number_of_CachedContent
      0, 0, 0, 5, // cache_id 5
      0, 5, 65, 66, 67, 68, 69, // cache_key "ABCDE"
      0, 0, 0, 0, 0, 0, 0, 10, // original_content_length
      0, 0, 0, 2, // cache_id 2
      0, 5, 75, 76, 77, 78, 79, // cache_key "KLMNO"
      0, 0, 0, 0, 0, 0, 10, 0, // original_content_length
      (byte) 0xF6, (byte) 0xFB, 0x50, 0x41 // hashcode_of_CachedContent_array
  };

  private final byte[] testIndexV2File = {
      0, 0, 0, 2, // version
      0, 0, 0, 0, // flags
      0, 0, 0, 2, // number_of_CachedContent
      0, 0, 0, 5, // cache_id 5
      0, 5, 65, 66, 67, 68, 69, // cache_key "ABCDE"
      0, 0, 0, 2, // metadata count
      0, 9, 101, 120, 111, 95, 114, 101, 100, 105, 114, // "exo_redir"
      0, 0, 0, 5, // value length
      97, 98, 99, 100, 101, // Redirected Uri "abcde"
      0, 7, 101, 120, 111, 95, 108, 101, 110, // "exo_len"
      0, 0, 0, 8, // value length
      0, 0, 0, 0, 0, 0, 0, 10, // original_content_length
      0, 0, 0, 2, // cache_id 2
      0, 5, 75, 76, 77, 78, 79, // cache_key "KLMNO"
      0, 0, 0, 1, // metadata count
      0, 7, 101, 120, 111, 95, 108, 101, 110, // "exo_len"
      0, 0, 0, 8, // value length
      0, 0, 0, 0, 0, 0, 10, 0, // original_content_length
      0x12, 0x15, 0x66, (byte) 0x8A // hashcode_of_CachedContent_array
  };
  private File cacheDir;

  @Before
  public void setUp() throws Exception {
    cacheDir =
        Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
  }

  @After
  public void tearDown() {
    Util.recursiveDelete(cacheDir);
  }

  @Test
  public void testAddGetRemove() throws Exception {
    final String key1 = "key1";
    final String key2 = "key2";
    final String key3 = "key3";

    CachedContentIndex index = newInstance();

    // Add two CachedContents with add methods
    CachedContent cachedContent1 = index.getOrAdd(key1);
    CachedContent cachedContent2 = index.getOrAdd(key2);
    assertThat(cachedContent1.id != cachedContent2.id).isTrue();

    // add a span
    int cacheFileLength = 20;
    File cacheSpanFile =
        SimpleCacheSpanTest.createCacheSpanFile(
            cacheDir,
            cachedContent1.id,
            /* offset= */ 10,
            cacheFileLength,
            /* lastTouchTimestamp= */ 30);
    SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheSpanFile, cacheFileLength, index);
    assertThat(span).isNotNull();
    cachedContent1.addSpan(span);

    // Check if they are added and get method returns null if the key isn't found
    assertThat(index.get(key1)).isEqualTo(cachedContent1);
    assertThat(index.get(key2)).isEqualTo(cachedContent2);
    assertThat(index.get(key3)).isNull();

    // test getAll()
    Collection<CachedContent> cachedContents = index.getAll();
    assertThat(cachedContents).containsExactly(cachedContent1, cachedContent2);

    // test getKeys()
    Set<String> keys = index.getKeys();
    assertThat(keys).containsExactly(key1, key2);

    // test getKeyForId()
    assertThat(index.getKeyForId(cachedContent1.id)).isEqualTo(key1);
    assertThat(index.getKeyForId(cachedContent2.id)).isEqualTo(key2);

    // test remove()
    index.maybeRemove(key2);
    index.maybeRemove(key3);
    assertThat(index.get(key1)).isEqualTo(cachedContent1);
    assertThat(index.get(key2)).isNull();
    assertThat(cacheSpanFile.exists()).isTrue();

    // test removeEmpty()
    index.getOrAdd(key2);
    index.removeEmpty();
    assertThat(index.get(key1)).isEqualTo(cachedContent1);
    assertThat(index.get(key2)).isNull();
    assertThat(cacheSpanFile.exists()).isTrue();
  }

  @Test
  public void testLegacyStoreAndLoad() throws Exception {
    assertStoredAndLoadedEqual(newLegacyInstance(), newLegacyInstance());
  }

  @Test
  public void testLegacyLoadV1() throws Exception {
    CachedContentIndex index = newLegacyInstance();

    FileOutputStream fos =
        new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME_ATOMIC));
    fos.write(testIndexV1File);
    fos.close();

    index.initialize(/* uid= */ 0);
    assertThat(index.getAll()).hasSize(2);

    assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5);
    ContentMetadata metadata = index.get("ABCDE").getMetadata();
    assertThat(ContentMetadata.getContentLength(metadata)).isEqualTo(10);

    assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2);
    ContentMetadata metadata2 = index.get("KLMNO").getMetadata();
    assertThat(ContentMetadata.getContentLength(metadata2)).isEqualTo(2560);
  }

  @Test
  public void testLegacyLoadV2() throws Exception {
    CachedContentIndex index = newLegacyInstance();

    FileOutputStream fos =
        new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME_ATOMIC));
    fos.write(testIndexV2File);
    fos.close();

    index.initialize(/* uid= */ 0);
    assertThat(index.getAll()).hasSize(2);

    assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5);
    ContentMetadata metadata = index.get("ABCDE").getMetadata();
    assertThat(ContentMetadata.getContentLength(metadata)).isEqualTo(10);
    assertThat(ContentMetadata.getRedirectedUri(metadata)).isEqualTo(Uri.parse("abcde"));

    assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2);
    ContentMetadata metadata2 = index.get("KLMNO").getMetadata();
    assertThat(ContentMetadata.getContentLength(metadata2)).isEqualTo(2560);
  }

  @Test
  public void testAssignIdForKeyAndGetKeyForId() {
    CachedContentIndex index = newInstance();
    final String key1 = "key1";
    final String key2 = "key2";
    int id1 = index.assignIdForKey(key1);
    int id2 = index.assignIdForKey(key2);
    assertThat(index.getKeyForId(id1)).isEqualTo(key1);
    assertThat(index.getKeyForId(id2)).isEqualTo(key2);
    assertThat(id1 != id2).isTrue();
    assertThat(index.assignIdForKey(key1)).isEqualTo(id1);
    assertThat(index.assignIdForKey(key2)).isEqualTo(id2);
  }

  @Test
  public void testGetNewId() {
    SparseArray<String> idToKey = new SparseArray<>();
    assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0);
    idToKey.put(10, "");
    assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(11);
    idToKey.put(Integer.MAX_VALUE, "");
    assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0);
    idToKey.put(0, "");
    assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(1);
  }

  @Test
  public void testLegacyEncryption() throws Exception {
    byte[] key = Util.getUtf8Bytes("Bar12345Bar12345"); // 128 bit key
    byte[] key2 = Util.getUtf8Bytes("Foo12345Foo12345"); // 128 bit key

    assertStoredAndLoadedEqual(newLegacyInstance(key), newLegacyInstance(key));

    // Rename the index file from the test above
    File file1 = new File(cacheDir, CachedContentIndex.FILE_NAME_ATOMIC);
    File file2 = new File(cacheDir, "file2compare");
    assertThat(file1.renameTo(file2)).isTrue();

    // Write a new index file
    assertStoredAndLoadedEqual(newLegacyInstance(key), newLegacyInstance(key));

    assertThat(file1.length()).isEqualTo(file2.length());
    // Assert file content is different
    FileInputStream fis1 = new FileInputStream(file1);
    FileInputStream fis2 = new FileInputStream(file2);
    for (int b; (b = fis1.read()) == fis2.read(); ) {
      assertThat(b != -1).isTrue();
    }

    boolean threw = false;
    try {
      assertStoredAndLoadedEqual(newLegacyInstance(key), newLegacyInstance(key2));
    } catch (AssertionError e) {
      threw = true;
    }
    assertWithMessage("Encrypted index file can not be read with different encryption key")
        .that(threw)
        .isTrue();

    try {
      assertStoredAndLoadedEqual(newLegacyInstance(key), newLegacyInstance());
    } catch (AssertionError e) {
      threw = true;
    }
    assertWithMessage("Encrypted index file can not be read without encryption key")
        .that(threw)
        .isTrue();

    // Non encrypted index file can be read even when encryption key provided.
    assertStoredAndLoadedEqual(newLegacyInstance(), newLegacyInstance(key));

    // Test multiple store() calls
    CachedContentIndex index = newLegacyInstance(key);
    index.getOrAdd("key3");
    index.store();
    assertStoredAndLoadedEqual(index, newLegacyInstance(key));
  }

  @Test
  public void testRemoveEmptyNotLockedCachedContent() {
    CachedContentIndex index = newInstance();
    CachedContent cachedContent = index.getOrAdd("key1");

    index.maybeRemove(cachedContent.key);

    assertThat(index.get(cachedContent.key)).isNull();
  }

  @Test
  public void testCantRemoveNotEmptyCachedContent() throws Exception {
    CachedContentIndex index = newInstance();

    CachedContent cachedContent = index.getOrAdd("key1");
    long cacheFileLength = 20;
    File cacheFile =
        SimpleCacheSpanTest.createCacheSpanFile(
            cacheDir,
            cachedContent.id,
            /* offset= */ 10,
            cacheFileLength,
            /* lastTouchTimestamp= */ 30);
    SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheFile, cacheFileLength, index);
    cachedContent.addSpan(span);

    index.maybeRemove(cachedContent.key);

    assertThat(index.get(cachedContent.key)).isNotNull();
  }

  @Test
  public void testCantRemoveLockedCachedContent() {
    CachedContentIndex index = newInstance();
    CachedContent cachedContent = index.getOrAdd("key1");
    cachedContent.setLocked(true);

    index.maybeRemove(cachedContent.key);

    assertThat(index.get(cachedContent.key)).isNotNull();
  }

  private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2)
      throws IOException {
    ContentMetadataMutations mutations1 = new ContentMetadataMutations();
    ContentMetadataMutations.setContentLength(mutations1, 2560);
    index.getOrAdd("KLMNO").applyMetadataMutations(mutations1);
    ContentMetadataMutations mutations2 = new ContentMetadataMutations();
    ContentMetadataMutations.setContentLength(mutations2, 10);
    ContentMetadataMutations.setRedirectedUri(mutations2, Uri.parse("abcde"));
    index.getOrAdd("ABCDE").applyMetadataMutations(mutations2);
    index.store();

    index2.initialize(/* uid= */ 0);
    Set<String> keys = index.getKeys();
    Set<String> keys2 = index2.getKeys();
    assertThat(keys2).isEqualTo(keys);
    for (String key : keys) {
      assertThat(index2.get(key)).isEqualTo(index.get(key));
    }
  }

  private CachedContentIndex newInstance() {
    return new CachedContentIndex(TestUtil.getTestDatabaseProvider());
  }

  private CachedContentIndex newLegacyInstance() {
    return newLegacyInstance(null);
  }

  private CachedContentIndex newLegacyInstance(@Nullable byte[] key) {
    return new CachedContentIndex(
        /* databaseProvider= */ null,
        cacheDir,
        /* legacyStorageSecretKey= */ key,
        /* legacyStorageEncrypt= */ key != null,
        /* preferLegacyStorage= */ true);
  }
}