blob: 14222f144d9f5d35ed22998c24472204642e2150 [file] [log] [blame]
/*
* Copyright (C) 2016 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.android.exoplayer2.C.LENGTH_UNSET;
import static com.google.android.exoplayer2.util.Util.toByteArray;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.Mockito.doAnswer;
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.upstream.cache.Cache.CacheException;
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.NavigableSet;
import java.util.Random;
import java.util.Set;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
/** Unit tests for {@link SimpleCache}. */
@RunWith(AndroidJUnit4.class)
public class SimpleCacheTest {
private static final String KEY_1 = "key1";
private static final String KEY_2 = "key2";
private File cacheDir;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
cacheDir = Util.createTempFile(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
// Delete the file. SimpleCache initialization should create a directory with the same name.
assertThat(cacheDir.delete()).isTrue();
}
@After
public void tearDown() {
Util.recursiveDelete(cacheDir);
}
@Test
public void cacheInitialization() {
SimpleCache cache = getSimpleCache();
// Cache initialization should have created a non-negative UID.
long uid = cache.getUid();
assertThat(uid).isAtLeast(0L);
// And the cache directory.
assertThat(cacheDir.exists()).isTrue();
// Reinitialization should load the same non-negative UID.
cache.release();
cache = getSimpleCache();
assertThat(cache.getUid()).isEqualTo(uid);
}
@Test
public void cacheInitializationError() throws IOException {
// Creating a file where the cache should be will cause an error during initialization.
assertThat(cacheDir.createNewFile()).isTrue();
// Cache initialization should not throw an exception, but no UID will be generated.
SimpleCache cache = getSimpleCache();
long uid = cache.getUid();
assertThat(uid).isEqualTo(-1L);
}
@Test
public void committingOneFile() throws Exception {
SimpleCache simpleCache = getSimpleCache();
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
assertThat(cacheSpan1.isCached).isFalse();
assertThat(cacheSpan1.isOpenEnded()).isTrue();
assertThat(simpleCache.startReadWriteNonBlocking(KEY_1, 0)).isNull();
NavigableSet<CacheSpan> cachedSpans = simpleCache.getCachedSpans(KEY_1);
assertThat(cachedSpans.isEmpty()).isTrue();
assertThat(simpleCache.getCacheSpace()).isEqualTo(0);
assertNoCacheFiles(cacheDir);
addCache(simpleCache, KEY_1, 0, 15);
Set<String> cachedKeys = simpleCache.getKeys();
assertThat(cachedKeys).containsExactly(KEY_1);
cachedSpans = simpleCache.getCachedSpans(KEY_1);
assertThat(cachedSpans).contains(cacheSpan1);
assertThat(simpleCache.getCacheSpace()).isEqualTo(15);
simpleCache.releaseHoleSpan(cacheSpan1);
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
assertThat(cacheSpan2.isCached).isTrue();
assertThat(cacheSpan2.isOpenEnded()).isFalse();
assertThat(cacheSpan2.length).isEqualTo(15);
assertCachedDataReadCorrect(cacheSpan2);
}
@Test
public void readCacheWithoutReleasingWriteCacheSpan() throws Exception {
SimpleCache simpleCache = getSimpleCache();
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
assertCachedDataReadCorrect(cacheSpan2);
simpleCache.releaseHoleSpan(cacheSpan1);
}
@Test
public void setGetContentMetadata() throws Exception {
SimpleCache simpleCache = getSimpleCache();
assertThat(ContentMetadata.getContentLength(simpleCache.getContentMetadata(KEY_1)))
.isEqualTo(LENGTH_UNSET);
ContentMetadataMutations mutations = new ContentMetadataMutations();
ContentMetadataMutations.setContentLength(mutations, 15);
simpleCache.applyContentMetadataMutations(KEY_1, mutations);
assertThat(ContentMetadata.getContentLength(simpleCache.getContentMetadata(KEY_1)))
.isEqualTo(15);
simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
mutations = new ContentMetadataMutations();
ContentMetadataMutations.setContentLength(mutations, 150);
simpleCache.applyContentMetadataMutations(KEY_1, mutations);
assertThat(ContentMetadata.getContentLength(simpleCache.getContentMetadata(KEY_1)))
.isEqualTo(150);
addCache(simpleCache, KEY_1, 140, 10);
simpleCache.release();
// Check if values are kept after cache is reloaded.
SimpleCache simpleCache2 = getSimpleCache();
assertThat(ContentMetadata.getContentLength(simpleCache2.getContentMetadata(KEY_1)))
.isEqualTo(150);
// Removing the last span shouldn't cause the length be change next time cache loaded
CacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145);
simpleCache2.removeSpan(lastSpan);
simpleCache2.release();
simpleCache2 = getSimpleCache();
assertThat(ContentMetadata.getContentLength(simpleCache2.getContentMetadata(KEY_1)))
.isEqualTo(150);
}
@Test
public void reloadCache() throws Exception {
SimpleCache simpleCache = getSimpleCache();
// write data
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
simpleCache.releaseHoleSpan(cacheSpan1);
simpleCache.release();
// Reload cache
simpleCache = getSimpleCache();
// read data back
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
assertCachedDataReadCorrect(cacheSpan2);
}
@Test
public void reloadCacheWithoutRelease() throws Exception {
SimpleCache simpleCache = getSimpleCache();
// Write data for KEY_1.
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
simpleCache.releaseHoleSpan(cacheSpan1);
// Write and remove data for KEY_2.
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_2, 0);
addCache(simpleCache, KEY_2, 0, 15);
simpleCache.releaseHoleSpan(cacheSpan2);
simpleCache.removeSpan(simpleCache.getCachedSpans(KEY_2).first());
// Don't release the cache. This means the index file won't have been written to disk after the
// data for KEY_2 was removed. Move the cache instead, so we can reload it without failing the
// folder locking check.
File cacheDir2 =
Util.createTempFile(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
cacheDir2.delete();
cacheDir.renameTo(cacheDir2);
// Reload the cache from its new location.
simpleCache = new SimpleCache(cacheDir2, new NoOpCacheEvictor());
// Read data back for KEY_1.
CacheSpan cacheSpan3 = simpleCache.startReadWrite(KEY_1, 0);
assertCachedDataReadCorrect(cacheSpan3);
// Check the entry for KEY_2 was removed when the cache was reloaded.
assertThat(simpleCache.getCachedSpans(KEY_2)).isEmpty();
Util.recursiveDelete(cacheDir2);
}
@Test
public void encryptedIndex() throws Exception {
byte[] key = Util.getUtf8Bytes("Bar12345Bar12345"); // 128 bit key
SimpleCache simpleCache = getEncryptedSimpleCache(key);
// write data
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
simpleCache.releaseHoleSpan(cacheSpan1);
simpleCache.release();
// Reload cache
simpleCache = getEncryptedSimpleCache(key);
// read data back
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
assertCachedDataReadCorrect(cacheSpan2);
}
@Test
public void encryptedIndexWrongKey() throws Exception {
byte[] key = Util.getUtf8Bytes("Bar12345Bar12345"); // 128 bit key
SimpleCache simpleCache = getEncryptedSimpleCache(key);
// write data
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
simpleCache.releaseHoleSpan(cacheSpan1);
simpleCache.release();
// Reload cache
byte[] key2 = Util.getUtf8Bytes("Foo12345Foo12345"); // 128 bit key
simpleCache = getEncryptedSimpleCache(key2);
// Cache should be cleared
assertThat(simpleCache.getKeys()).isEmpty();
assertNoCacheFiles(cacheDir);
}
@Test
public void encryptedIndexLostKey() throws Exception {
byte[] key = Util.getUtf8Bytes("Bar12345Bar12345"); // 128 bit key
SimpleCache simpleCache = getEncryptedSimpleCache(key);
// write data
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
simpleCache.releaseHoleSpan(cacheSpan1);
simpleCache.release();
// Reload cache
simpleCache = getSimpleCache();
// Cache should be cleared
assertThat(simpleCache.getKeys()).isEmpty();
assertNoCacheFiles(cacheDir);
}
@Test
public void getCachedLength() throws Exception {
SimpleCache simpleCache = getSimpleCache();
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
// No cached bytes, returns -'length'
assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(-100);
// Position value doesn't affect the return value
assertThat(simpleCache.getCachedLength(KEY_1, 20, 100)).isEqualTo(-100);
addCache(simpleCache, KEY_1, 0, 15);
// Returns the length of a single span
assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(15);
// Value is capped by the 'length'
assertThat(simpleCache.getCachedLength(KEY_1, 0, 10)).isEqualTo(10);
addCache(simpleCache, KEY_1, 15, 35);
// Returns the length of two adjacent spans
assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(50);
addCache(simpleCache, KEY_1, 60, 10);
// Not adjacent span doesn't affect return value
assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(50);
// Returns length of hole up to the next cached span
assertThat(simpleCache.getCachedLength(KEY_1, 55, 100)).isEqualTo(-5);
simpleCache.releaseHoleSpan(cacheSpan);
}
/* Tests https://github.com/google/ExoPlayer/issues/3260 case. */
@Test
public void exceptionDuringEvictionByLeastRecentlyUsedCacheEvictorNotHang() throws Exception {
CachedContentIndex contentIndex =
Mockito.spy(new CachedContentIndex(TestUtil.getInMemoryDatabaseProvider()));
SimpleCache simpleCache =
new SimpleCache(
cacheDir, new LeastRecentlyUsedCacheEvictor(20), contentIndex, /* fileIndex= */ null);
// Add some content.
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
// Make index.store() throw exception from now on.
doAnswer(
invocation -> {
throw new CacheException("SimpleCacheTest");
})
.when(contentIndex)
.store();
// Adding more content will make LeastRecentlyUsedCacheEvictor evict previous content.
try {
addCache(simpleCache, KEY_1, 15, 15);
assertWithMessage("Exception was expected").fail();
} catch (CacheException e) {
// do nothing.
}
simpleCache.releaseHoleSpan(cacheSpan);
// Although store() has failed, it should remove the first span and add the new one.
NavigableSet<CacheSpan> cachedSpans = simpleCache.getCachedSpans(KEY_1);
assertThat(cachedSpans).isNotEmpty();
assertThat(cachedSpans).hasSize(1);
assertThat(cachedSpans.pollFirst().position).isEqualTo(15);
}
@Test
public void usingReleasedSimpleCacheThrowsException() throws Exception {
SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
simpleCache.release();
try {
simpleCache.startReadWriteNonBlocking(KEY_1, 0);
assertWithMessage("Exception was expected").fail();
} catch (RuntimeException e) {
// Expected. Do nothing.
}
}
@Test
public void multipleSimpleCacheWithSameCacheDirThrowsException() throws Exception {
new SimpleCache(cacheDir, new NoOpCacheEvictor());
try {
new SimpleCache(cacheDir, new NoOpCacheEvictor());
assertWithMessage("Exception was expected").fail();
} catch (IllegalStateException e) {
// Expected. Do nothing.
}
}
@Test
public void multipleSimpleCacheWithSameCacheDirDoesNotThrowsExceptionAfterRelease()
throws Exception {
SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
simpleCache.release();
new SimpleCache(cacheDir, new NoOpCacheEvictor());
}
private SimpleCache getSimpleCache() {
return new SimpleCache(cacheDir, new NoOpCacheEvictor());
}
private SimpleCache getEncryptedSimpleCache(byte[] secretKey) {
return new SimpleCache(cacheDir, new NoOpCacheEvictor(), secretKey);
}
private static void addCache(SimpleCache simpleCache, String key, int position, int length)
throws IOException {
File file = simpleCache.startFile(key, position, length);
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(generateData(key, position, length));
}
simpleCache.commitFile(file, length);
}
private static void assertCachedDataReadCorrect(CacheSpan cacheSpan) throws IOException {
assertThat(cacheSpan.isCached).isTrue();
byte[] expected = generateData(cacheSpan.key, (int) cacheSpan.position, (int) cacheSpan.length);
try (FileInputStream inputStream = new FileInputStream(cacheSpan.file)) {
assertThat(toByteArray(inputStream)).isEqualTo(expected);
}
}
private static void assertNoCacheFiles(File dir) {
File[] files = dir.listFiles();
if (files == null) {
return;
}
for (File file : files) {
if (file.isDirectory()) {
assertNoCacheFiles(file);
} else {
assertThat(file.getName().endsWith(SimpleCacheSpan.COMMON_SUFFIX)).isFalse();
}
}
}
private static byte[] generateData(String key, int position, int length) {
byte[] bytes = new byte[length];
new Random((long) (key.hashCode() ^ position)).nextBytes(bytes);
return bytes;
}
}