blob: 65c9c4d9e5513baa3ac796c334c95a03cfec0088 [file] [log] [blame]
/*
* Copyright (C) 2017 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.testutil.CacheAsserts.assertCacheEmpty;
import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.net.Uri;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.File;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/** Tests {@link CacheUtil}. */
@RunWith(AndroidJUnit4.class)
public final class CacheUtilTest {
/**
* Abstract fake Cache implementation used by the test. This class must be public so Mockito can
* create a proxy for it.
*/
public abstract static class AbstractFakeCache implements Cache {
// This array is set to alternating length of cached and not cached regions in tests:
// spansAndGaps = {<length of 1st cached region>, <length of 1st not cached region>,
// <length of 2nd cached region>, <length of 2nd not cached region>, ... }
// Ideally it should end with a cached region but it shouldn't matter for any code.
private int[] spansAndGaps;
private long contentLength;
private void init() {
spansAndGaps = new int[] {};
contentLength = C.LENGTH_UNSET;
}
@Override
public long getCachedLength(String key, long position, long length) {
for (int i = 0; i < spansAndGaps.length; i++) {
int spanOrGap = spansAndGaps[i];
if (position < spanOrGap) {
long left = Math.min(spanOrGap - position, length);
return (i & 1) == 1 ? -left : left;
}
position -= spanOrGap;
}
return -length;
}
@Override
public ContentMetadata getContentMetadata(String key) {
DefaultContentMetadata metadata = new DefaultContentMetadata();
ContentMetadataMutations mutations = new ContentMetadataMutations();
ContentMetadataMutations.setContentLength(mutations, contentLength);
return metadata.copyWithMutationsApplied(mutations);
}
}
@Mock(answer = Answers.CALLS_REAL_METHODS) private AbstractFakeCache mockCache;
private File tempFolder;
private SimpleCache cache;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mockCache.init();
tempFolder =
Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
}
@After
public void tearDown() {
Util.recursiveDelete(tempFolder);
}
@Test
public void generateKey() {
assertThat(CacheUtil.generateKey(Uri.EMPTY)).isNotNull();
Uri testUri = Uri.parse("test");
String key = CacheUtil.generateKey(testUri);
assertThat(key).isNotNull();
// Should generate the same key for the same input.
assertThat(CacheUtil.generateKey(testUri)).isEqualTo(key);
// Should generate different key for different input.
assertThat(key.equals(CacheUtil.generateKey(Uri.parse("test2")))).isFalse();
}
@Test
public void defaultCacheKeyFactory_buildCacheKey() {
Uri testUri = Uri.parse("test");
String key = "key";
// If DataSpec.key is present, returns it.
assertThat(
CacheUtil.DEFAULT_CACHE_KEY_FACTORY.buildCacheKey(
new DataSpec.Builder().setUri(testUri).setKey(key).build()))
.isEqualTo(key);
// If not generates a new one using DataSpec.uri.
assertThat(
CacheUtil.DEFAULT_CACHE_KEY_FACTORY.buildCacheKey(
new DataSpec(testUri, /* position= */ 0, /* length= */ LENGTH_UNSET)))
.isEqualTo(testUri.toString());
}
@Test
public void getCachedNoData() {
Pair<Long, Long> contentLengthAndBytesCached =
CacheUtil.getCached(
new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null);
assertThat(contentLengthAndBytesCached.first).isEqualTo(C.LENGTH_UNSET);
assertThat(contentLengthAndBytesCached.second).isEqualTo(0);
}
@Test
public void getCachedDataUnknownLength() {
// Mock there is 100 bytes cached at the beginning
mockCache.spansAndGaps = new int[] {100};
Pair<Long, Long> contentLengthAndBytesCached =
CacheUtil.getCached(
new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null);
assertThat(contentLengthAndBytesCached.first).isEqualTo(C.LENGTH_UNSET);
assertThat(contentLengthAndBytesCached.second).isEqualTo(100);
}
@Test
public void getCachedNoDataKnownLength() {
mockCache.contentLength = 1000;
Pair<Long, Long> contentLengthAndBytesCached =
CacheUtil.getCached(
new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null);
assertThat(contentLengthAndBytesCached.first).isEqualTo(1000);
assertThat(contentLengthAndBytesCached.second).isEqualTo(0);
}
@Test
public void getCached() {
mockCache.contentLength = 1000;
mockCache.spansAndGaps = new int[] {100, 100, 200};
Pair<Long, Long> contentLengthAndBytesCached =
CacheUtil.getCached(
new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null);
assertThat(contentLengthAndBytesCached.first).isEqualTo(1000);
assertThat(contentLengthAndBytesCached.second).isEqualTo(300);
}
@Test
public void getCachedFromNonZeroPosition() {
mockCache.contentLength = 1000;
mockCache.spansAndGaps = new int[] {100, 100, 200};
Pair<Long, Long> contentLengthAndBytesCached =
CacheUtil.getCached(
new DataSpec(Uri.parse("test"), /* position= */ 100, /* length= */ C.LENGTH_UNSET),
mockCache,
/* cacheKeyFactory= */ null);
assertThat(contentLengthAndBytesCached.first).isEqualTo(900);
assertThat(contentLengthAndBytesCached.second).isEqualTo(200);
}
@Test
public void cache() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
CachingCounters counters = new CachingCounters();
CacheUtil.cache(
new DataSpec(Uri.parse("test_data")),
cache,
/* cacheKeyFactory= */ null,
dataSource,
counters,
/* isCanceled= */ null);
counters.assertValues(0, 100, 100);
assertCachedData(cache, fakeDataSet);
}
@Test
public void cacheSetOffsetAndLength() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
Uri testUri = Uri.parse("test_data");
DataSpec dataSpec = new DataSpec(testUri, /* position= */ 10, /* length= */ 20);
CachingCounters counters = new CachingCounters();
CacheUtil.cache(
dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);
counters.assertValues(0, 20, 20);
counters.reset();
CacheUtil.cache(
new DataSpec(testUri),
cache,
/* cacheKeyFactory= */ null,
dataSource,
counters,
/* isCanceled= */ null);
counters.assertValues(20, 80, 100);
assertCachedData(cache, fakeDataSet);
}
@Test
public void cacheUnknownLength() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data")
.setSimulateUnknownLength(true)
.appendReadData(TestUtil.buildTestData(100)).endData();
FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
DataSpec dataSpec = new DataSpec(Uri.parse("test_data"));
CachingCounters counters = new CachingCounters();
CacheUtil.cache(
dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);
counters.assertValues(0, 100, 100);
assertCachedData(cache, fakeDataSet);
}
@Test
public void cacheUnknownLengthPartialCaching() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data")
.setSimulateUnknownLength(true)
.appendReadData(TestUtil.buildTestData(100)).endData();
FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
Uri testUri = Uri.parse("test_data");
DataSpec dataSpec = new DataSpec(testUri, /* position= */ 10, /* length= */ 20);
CachingCounters counters = new CachingCounters();
CacheUtil.cache(
dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);
counters.assertValues(0, 20, 20);
counters.reset();
CacheUtil.cache(
new DataSpec(testUri),
cache,
/* cacheKeyFactory= */ null,
dataSource,
counters,
/* isCanceled= */ null);
counters.assertValues(20, 80, 100);
assertCachedData(cache, fakeDataSet);
}
@Test
public void cacheLengthExceedsActualDataLength() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
Uri testUri = Uri.parse("test_data");
DataSpec dataSpec = new DataSpec(testUri, /* position= */ 0, /* length= */ 1000);
CachingCounters counters = new CachingCounters();
CacheUtil.cache(
dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);
counters.assertValues(0, 100, 1000);
assertCachedData(cache, fakeDataSet);
}
@Test
public void cacheThrowEOFException() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
Uri testUri = Uri.parse("test_data");
DataSpec dataSpec = new DataSpec(testUri, /* position= */ 0, /* length= */ 1000);
try {
CacheUtil.cache(
dataSpec,
cache,
/* cacheKeyFactory= */ null,
new CacheDataSource(cache, dataSource),
new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES],
/* priorityTaskManager= */ null,
/* priority= */ 0,
/* progressListener= */ null,
/* isCanceled= */ null,
/* enableEOFException= */ true);
fail();
} catch (EOFException e) {
// Do nothing.
}
}
@Test
public void cachePolling() throws Exception {
final CachingCounters counters = new CachingCounters();
FakeDataSet fakeDataSet =
new FakeDataSet()
.newData("test_data")
.appendReadData(TestUtil.buildTestData(100))
.appendReadAction(() -> counters.assertValues(0, 100, 300))
.appendReadData(TestUtil.buildTestData(100))
.appendReadAction(() -> counters.assertValues(0, 200, 300))
.appendReadData(TestUtil.buildTestData(100))
.endData();
FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
CacheUtil.cache(
new DataSpec(Uri.parse("test_data")),
cache,
/* cacheKeyFactory= */ null,
dataSource,
counters,
/* isCanceled= */ null);
counters.assertValues(0, 300, 300);
assertCachedData(cache, fakeDataSet);
}
@Test
public void remove() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
DataSpec dataSpec =
new DataSpec.Builder()
.setUri("test_data")
.setFlags(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION)
.build();
CacheUtil.cache(
dataSpec,
cache,
/* cacheKeyFactory= */ null,
// Set fragmentSize to 10 to make sure there are multiple spans.
new CacheDataSource(
cache,
dataSource,
new FileDataSource(),
new CacheDataSink(cache, /* fragmentSize= */ 10),
/* flags= */ 0,
/* eventListener= */ null),
new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES],
/* priorityTaskManager= */ null,
/* priority= */ 0,
/* progressListener= */ null,
/* isCanceled= */ null,
true);
CacheUtil.remove(dataSpec, cache, /* cacheKeyFactory= */ null);
assertCacheEmpty(cache);
}
private static final class CachingCounters implements CacheUtil.ProgressListener {
private long contentLength = C.LENGTH_UNSET;
private long bytesAlreadyCached;
private long bytesNewlyCached;
private boolean seenFirstProgressUpdate;
@Override
public void onProgress(long contentLength, long bytesCached, long newBytesCached) {
this.contentLength = contentLength;
if (!seenFirstProgressUpdate) {
bytesAlreadyCached = bytesCached;
seenFirstProgressUpdate = true;
}
bytesNewlyCached = bytesCached - bytesAlreadyCached;
}
public void assertValues(int bytesAlreadyCached, int bytesNewlyCached, int contentLength) {
assertThat(this.bytesAlreadyCached).isEqualTo(bytesAlreadyCached);
assertThat(this.bytesNewlyCached).isEqualTo(bytesNewlyCached);
assertThat(this.contentLength).isEqualTo(contentLength);
}
public void reset() {
contentLength = C.LENGTH_UNSET;
bytesAlreadyCached = 0;
bytesNewlyCached = 0;
seenFirstProgressUpdate = false;
}
}
}