blob: 6562c17183b12befe77b264aa974212a5ae96748 [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.testutil.CacheAsserts.assertCacheEmpty;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.net.Uri;
import androidx.annotation.Nullable;
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.CacheAsserts;
import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData;
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.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.NavigableSet;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link CacheDataSource}. */
@RunWith(AndroidJUnit4.class)
public final class CacheDataSourceTest {
private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
private static final int CACHE_FRAGMENT_SIZE = 3;
private static final String DATASPEC_KEY = "dataSpecKey";
// Test data
private Uri testDataUri;
private Map<String, String> httpRequestHeaders;
private DataSpec unboundedDataSpec;
private DataSpec boundedDataSpec;
private DataSpec unboundedDataSpecWithKey;
private DataSpec boundedDataSpecWithKey;
private String defaultCacheKey;
private String customCacheKey;
// Dependencies of SUT
private CacheKeyFactory cacheKeyFactory;
private File tempFolder;
private SimpleCache cache;
private FakeDataSource upstreamDataSource;
@Before
public void setUp() throws Exception {
testDataUri = Uri.parse("https://www.test.com/data");
httpRequestHeaders = new HashMap<>();
httpRequestHeaders.put("Test-key", "Test-val");
unboundedDataSpec = buildDataSpec(/* unbounded= */ true, /* key= */ null);
boundedDataSpec = buildDataSpec(/* unbounded= */ false, /* key= */ null);
unboundedDataSpecWithKey = buildDataSpec(/* unbounded= */ true, DATASPEC_KEY);
boundedDataSpecWithKey = buildDataSpec(/* unbounded= */ false, DATASPEC_KEY);
defaultCacheKey = CacheUtil.DEFAULT_CACHE_KEY_FACTORY.buildCacheKey(unboundedDataSpec);
customCacheKey = "customKey." + defaultCacheKey;
cacheKeyFactory = dataSpec -> customCacheKey;
tempFolder =
Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
upstreamDataSource = new FakeDataSource();
}
@After
public void tearDown() throws Exception {
Util.recursiveDelete(tempFolder);
}
@Test
public void fragmentSize() throws Exception {
CacheDataSource cacheDataSource = createCacheDataSource(false, false);
assertReadDataContentLength(cacheDataSource, boundedDataSpec, false, false);
for (String key : cache.getKeys()) {
for (CacheSpan cacheSpan : cache.getCachedSpans(key)) {
assertThat(cacheSpan.length <= CACHE_FRAGMENT_SIZE).isTrue();
assertThat(cacheSpan.file.length() <= CACHE_FRAGMENT_SIZE).isTrue();
}
}
}
@Test
public void cacheAndReadUnboundedRequest() throws Exception {
assertCacheAndRead(unboundedDataSpec, /* unknownLength= */ false);
}
@Test
public void cacheAndReadUnknownLength() throws Exception {
assertCacheAndRead(boundedDataSpec, /* unknownLength= */ true);
}
@Test
public void cacheAndReadUnboundedRequestUnknownLength() throws Exception {
assertCacheAndRead(unboundedDataSpec, /* unknownLength= */ true);
}
@Test
public void cacheAndRead() throws Exception {
assertCacheAndRead(boundedDataSpec, /* unknownLength= */ false);
}
@Test
public void propagatesHttpHeadersUpstream() throws Exception {
CacheDataSource cacheDataSource =
createCacheDataSource(/* setReadException= */ false, /* unknownLength= */ false);
DataSpec dataSpec = buildDataSpec(/* position= */ 2, /* length= */ 5);
cacheDataSource.open(dataSpec);
DataSpec[] upstreamDataSpecs = upstreamDataSource.getAndClearOpenedDataSpecs();
assertThat(upstreamDataSpecs).hasLength(1);
assertThat(upstreamDataSpecs[0].httpRequestHeaders).isEqualTo(this.httpRequestHeaders);
}
@Test
public void unsatisfiableRange() throws Exception {
// Bounded request but the content length is unknown. This forces all data to be cached but not
// the length.
assertCacheAndRead(boundedDataSpec, /* unknownLength= */ true);
// Now do an unbounded request. This will read all of the data from cache and then try to read
// more from upstream which will cause to a 416 so CDS will store the length.
CacheDataSource cacheDataSource =
createCacheDataSource(/* setReadException= */ true, /* unknownLength= */ true);
assertReadDataContentLength(
cacheDataSource, unboundedDataSpec, /* unknownLength= */ true, /* customCacheKey= */ false);
// If the user try to access off range then it should throw an IOException.
try {
cacheDataSource =
createCacheDataSource(/* setReadException= */ false, /* unknownLength= */ false);
cacheDataSource.open(buildDataSpec(TEST_DATA.length, /* length= */ 1, defaultCacheKey));
fail();
} catch (IOException e) {
// Expected.
}
}
@Test
public void cacheAndReadUnboundedRequestWithCacheKeyFactoryWithNullDataSpecCacheKey()
throws Exception {
assertCacheAndRead(unboundedDataSpec, /* unknownLength= */ false, cacheKeyFactory);
}
@Test
public void cacheAndReadUnknownLengthWithCacheKeyFactoryOverridingWithNullDataSpecCacheKey()
throws Exception {
assertCacheAndRead(boundedDataSpec, /* unknownLength= */ true, cacheKeyFactory);
}
@Test
public void
testCacheAndReadUnboundedRequestUnknownLengthWithCacheKeyFactoryWithNullDataSpecCacheKey()
throws Exception {
assertCacheAndRead(unboundedDataSpec, /* unknownLength= */ true, cacheKeyFactory);
}
@Test
public void cacheAndReadWithCacheKeyFactoryWithNullDataSpecCacheKey() throws Exception {
assertCacheAndRead(boundedDataSpec, /* unknownLength= */ false, cacheKeyFactory);
}
@Test
public void unsatisfiableRangeWithCacheKeyFactoryNullDataSpecCacheKey() throws Exception {
// Bounded request but the content length is unknown. This forces all data to be cached but not
// the length.
assertCacheAndRead(boundedDataSpec, /* unknownLength= */ true, cacheKeyFactory);
// Now do an unbounded request. This will read all of the data from cache and then try to read
// more from upstream which will cause to a 416 so CDS will store the length.
CacheDataSource cacheDataSource =
createCacheDataSource(
/* setReadException= */ true, /* unknownLength= */ true, cacheKeyFactory);
assertReadDataContentLength(
cacheDataSource, unboundedDataSpec, /* unknownLength= */ true, /* customCacheKey= */ true);
// If the user try to access off range then it should throw an IOException.
try {
cacheDataSource =
createCacheDataSource(
/* setReadException= */ false, /* unknownLength= */ false, cacheKeyFactory);
cacheDataSource.open(buildDataSpec(TEST_DATA.length, /* length= */ 1, customCacheKey));
fail();
} catch (IOException e) {
// Expected.
}
}
@Test
public void cacheAndReadUnboundedRequestWithCacheKeyFactoryOverridingDataSpecCacheKey()
throws Exception {
assertCacheAndRead(unboundedDataSpecWithKey, false, cacheKeyFactory);
}
@Test
public void cacheAndReadUnknownLengthWithCacheKeyFactoryOverridingDataSpecCacheKey()
throws Exception {
assertCacheAndRead(boundedDataSpecWithKey, true, cacheKeyFactory);
}
@Test
public void
testCacheAndReadUnboundedRequestUnknownLengthWithCacheKeyFactoryOverridingDataSpecCacheKey()
throws Exception {
assertCacheAndRead(unboundedDataSpecWithKey, /* unknownLength= */ true, cacheKeyFactory);
}
@Test
public void cacheAndReadWithCacheKeyFactoryOverridingDataSpecCacheKey() throws Exception {
assertCacheAndRead(boundedDataSpecWithKey, /* unknownLength= */ false, cacheKeyFactory);
}
@Test
public void unsatisfiableRangeWithCacheKeyFactoryOverridingDataSpecCacheKey() throws Exception {
// Bounded request but the content length is unknown. This forces all data to be cached but not
// the length.
assertCacheAndRead(boundedDataSpecWithKey, /* unknownLength= */ true, cacheKeyFactory);
// Now do an unbounded request. This will read all of the data from cache and then try to read
// more from upstream which will cause to a 416 so CDS will store the length.
CacheDataSource cacheDataSource =
createCacheDataSource(
/* setReadException= */ true, /* unknownLength= */ true, cacheKeyFactory);
assertReadDataContentLength(
cacheDataSource,
unboundedDataSpecWithKey,
/* unknownLength= */ true,
/* customCacheKey= */ true);
// If the user try to access off range then it should throw an IOException.
try {
cacheDataSource =
createCacheDataSource(
/* setReadException= */ false, /* unknownLength= */ false, cacheKeyFactory);
cacheDataSource.open(buildDataSpec(TEST_DATA.length, /* length= */ 1, customCacheKey));
fail();
} catch (IOException e) {
// Expected.
}
}
@Test
public void contentLengthEdgeCases() throws Exception {
DataSpec dataSpec = buildDataSpec(TEST_DATA.length - 2, 2);
// Read partial at EOS but don't cross it so length is unknown.
CacheDataSource cacheDataSource = createCacheDataSource(false, true);
assertReadData(cacheDataSource, dataSpec, true);
assertThat(ContentMetadata.getContentLength(cache.getContentMetadata(defaultCacheKey)))
.isEqualTo(C.LENGTH_UNSET);
// Now do an unbounded request for whole data. This will cause a bounded request from upstream.
// End of data from upstream shouldn't be mixed up with EOS and cause length set wrong.
cacheDataSource = createCacheDataSource(false, true);
assertReadDataContentLength(cacheDataSource, unboundedDataSpec, true, false);
// Now the length set correctly do an unbounded request with offset.
assertThat(
cacheDataSource.open(
buildDataSpec(TEST_DATA.length - 2, C.LENGTH_UNSET, defaultCacheKey)))
.isEqualTo(2);
// An unbounded request with offset for not cached content.
dataSpec =
new DataSpec(Uri.parse("https://www.test.com/other"), TEST_DATA.length - 2, C.LENGTH_UNSET);
assertThat(cacheDataSource.open(dataSpec)).isEqualTo(C.LENGTH_UNSET);
}
@Test
public void unknownLengthContentReadInOneConnectionAndLengthIsResolved() throws Exception {
FakeDataSource upstream = new FakeDataSource();
upstream
.getDataSet()
.newData(testDataUri)
.appendReadData(TEST_DATA)
.setSimulateUnknownLength(true);
CacheDataSource cacheDataSource = new CacheDataSource(cache, upstream, 0);
cacheDataSource.open(unboundedDataSpec);
TestUtil.readToEnd(cacheDataSource);
cacheDataSource.close();
assertThat(upstream.getAndClearOpenedDataSpecs()).hasLength(1);
assertThat(ContentMetadata.getContentLength(cache.getContentMetadata(defaultCacheKey)))
.isEqualTo(TEST_DATA.length);
}
@Test
public void ignoreCacheForUnsetLengthRequests() throws Exception {
FakeDataSource upstream = new FakeDataSource();
upstream.getDataSet().setData(testDataUri, TEST_DATA);
CacheDataSource cacheDataSource =
new CacheDataSource(
cache, upstream, CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS);
cacheDataSource.open(unboundedDataSpec);
TestUtil.readToEnd(cacheDataSource);
cacheDataSource.close();
assertThat(cache.getKeys()).isEmpty();
}
@Test
public void readOnlyCache() throws Exception {
CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null);
assertReadDataContentLength(cacheDataSource, boundedDataSpec, false, false);
assertCacheEmpty(cache);
}
@Test
public void switchToCacheSourceWithReadOnlyCacheDataSource() throws Exception {
// Create a fake data source with a 1 MB default data.
FakeDataSource upstream = new FakeDataSource();
FakeData fakeData = upstream.getDataSet().newDefaultData().appendReadData(1024 * 1024 - 1);
// Insert an action just before the end of the data to fail the test if reading from upstream
// reaches end of the data.
fakeData
.appendReadAction(() -> fail("Read from upstream shouldn't reach to the end of the data."))
.appendReadData(1);
// Create cache read-only CacheDataSource.
CacheDataSource cacheDataSource =
new CacheDataSource(cache, upstream, new FileDataSource(), null, 0, null);
// Open source and read some data from upstream as the data hasn't cached yet.
cacheDataSource.open(unboundedDataSpec);
byte[] buffer = new byte[1024];
cacheDataSource.read(buffer, 0, buffer.length);
// Cache the data. Although we use another FakeDataSource instance, it shouldn't matter.
FakeDataSource upstream2 =
new FakeDataSource(
new FakeDataSource()
.getDataSet()
.newDefaultData()
.appendReadData(1024 * 1024)
.endData());
CacheUtil.cache(
cache, unboundedDataSpec, upstream2, /* progressListener= */ null, /* isCanceled= */ null);
// Read the rest of the data.
TestUtil.readToEnd(cacheDataSource);
cacheDataSource.close();
}
@Test
public void switchToCacheSourceWithNonBlockingCacheDataSource() throws Exception {
// Create a fake data source with a 1 MB default data.
FakeDataSource upstream = new FakeDataSource();
FakeData fakeData = upstream.getDataSet().newDefaultData().appendReadData(1024 * 1024 - 1);
// Insert an action just before the end of the data to fail the test if reading from upstream
// reaches end of the data.
fakeData
.appendReadAction(() -> fail("Read from upstream shouldn't reach to the end of the data."))
.appendReadData(1);
// Lock the content on the cache.
CacheSpan cacheSpan = cache.startReadWriteNonBlocking(defaultCacheKey, 0);
assertThat(cacheSpan).isNotNull();
assertThat(cacheSpan.isHoleSpan()).isTrue();
// Create non blocking CacheDataSource.
CacheDataSource cacheDataSource = new CacheDataSource(cache, upstream, 0);
// Open source and read some data from upstream without writing to cache as the data is locked.
cacheDataSource.open(unboundedDataSpec);
byte[] buffer = new byte[1024];
cacheDataSource.read(buffer, 0, buffer.length);
// Unlock the span.
cache.releaseHoleSpan(cacheSpan);
assertCacheEmpty(cache);
// Cache the data. Although we use another FakeDataSource instance, it shouldn't matter.
FakeDataSource upstream2 =
new FakeDataSource(
new FakeDataSource()
.getDataSet()
.newDefaultData()
.appendReadData(1024 * 1024)
.endData());
CacheUtil.cache(
cache, unboundedDataSpec, upstream2, /* progressListener= */ null, /* isCanceled= */ null);
// Read the rest of the data.
TestUtil.readToEnd(cacheDataSource);
cacheDataSource.close();
}
@Test
public void deleteCachedWhileReadingFromUpstreamWithReadOnlyCacheDataSourceDoesNotCrash()
throws Exception {
// Create a fake data source with a 1 KB default data.
FakeDataSource upstream = new FakeDataSource();
int dataLength = 1024;
upstream.getDataSet().newDefaultData().appendReadData(dataLength).endData();
// Cache the latter half of the data.
int halfDataLength = 512;
DataSpec dataSpec = buildDataSpec(halfDataLength, C.LENGTH_UNSET);
CacheUtil.cache(
cache, dataSpec, upstream, /* progressListener= */ null, /* isCanceled= */ null);
// Create cache read-only CacheDataSource.
CacheDataSource cacheDataSource =
new CacheDataSource(cache, upstream, new FileDataSource(), null, 0, null);
// Open source and read some data from upstream as the data hasn't cached yet.
cacheDataSource.open(unboundedDataSpec);
TestUtil.readExactly(cacheDataSource, 100);
// Delete cached data.
CacheUtil.remove(unboundedDataSpec, cache, /* cacheKeyFactory= */ null);
assertCacheEmpty(cache);
// Read the rest of the data.
TestUtil.readToEnd(cacheDataSource);
cacheDataSource.close();
}
@Test
public void deleteCachedWhileReadingFromUpstreamWithBlockingCacheDataSourceDoesNotBlock()
throws Exception {
// Create a fake data source with a 1 KB default data.
FakeDataSource upstream = new FakeDataSource();
int dataLength = 1024;
upstream.getDataSet().newDefaultData().appendReadData(dataLength).endData();
// Cache the latter half of the data.
int halfDataLength = 512;
DataSpec dataSpec = buildDataSpec(/* position= */ 0, halfDataLength);
CacheUtil.cache(
cache, dataSpec, upstream, /* progressListener= */ null, /* isCanceled= */ null);
// Create blocking CacheDataSource.
CacheDataSource cacheDataSource =
new CacheDataSource(cache, upstream, CacheDataSource.FLAG_BLOCK_ON_CACHE);
cacheDataSource.open(unboundedDataSpec);
// Read the first half from upstream as it hasn't cached yet.
TestUtil.readExactly(cacheDataSource, halfDataLength);
// Delete the cached latter half.
NavigableSet<CacheSpan> cachedSpans = cache.getCachedSpans(defaultCacheKey);
for (CacheSpan cachedSpan : cachedSpans) {
if (cachedSpan.position >= halfDataLength) {
cache.removeSpan(cachedSpan);
}
}
// Read the rest of the data.
TestUtil.readToEnd(cacheDataSource);
cacheDataSource.close();
}
private void assertCacheAndRead(DataSpec dataSpec, boolean unknownLength) throws IOException {
assertCacheAndRead(dataSpec, unknownLength, /* cacheKeyFactory= */ null);
}
private void assertCacheAndRead(
DataSpec dataSpec, boolean unknownLength, @Nullable CacheKeyFactory cacheKeyFactory)
throws IOException {
// Read all data from upstream and write to cache.
CacheDataSource cacheDataSource =
createCacheDataSource(/* setReadException= */ false, unknownLength, cacheKeyFactory);
assertReadDataContentLength(cacheDataSource, dataSpec, unknownLength, cacheKeyFactory != null);
// Just read from cache.
cacheDataSource =
createCacheDataSource(/* setReadException= */ true, unknownLength, cacheKeyFactory);
assertReadDataContentLength(
cacheDataSource,
dataSpec,
/* unknownLength= */ false,
/* customCacheKey= */ cacheKeyFactory != null);
}
/**
* Reads data until EOI and compares it to {@link #TEST_DATA}. Also checks content length returned
* from open() call and the cached content length.
*/
private void assertReadDataContentLength(
CacheDataSource cacheDataSource,
DataSpec dataSpec,
boolean unknownLength,
boolean customCacheKey)
throws IOException {
assertReadData(cacheDataSource, dataSpec, unknownLength);
// If the request was unbounded then the content length should be cached, either because the
// content length was known or because EOS was read. If the request was bounded then the content
// length will not have been determined.
ContentMetadata metadata =
cache.getContentMetadata(customCacheKey ? this.customCacheKey : defaultCacheKey);
assertThat(ContentMetadata.getContentLength(metadata))
.isEqualTo(dataSpec.length == C.LENGTH_UNSET ? TEST_DATA.length : C.LENGTH_UNSET);
}
private void assertReadData(
CacheDataSource cacheDataSource, DataSpec dataSpec, boolean unknownLength)
throws IOException {
int position = (int) dataSpec.position;
int requestLength = (int) dataSpec.length;
int readLength = TEST_DATA.length - position;
if (requestLength != C.LENGTH_UNSET) {
readLength = Math.min(readLength, requestLength);
}
assertThat(cacheDataSource.open(dataSpec))
.isEqualTo(unknownLength ? requestLength : readLength);
cacheDataSource.close();
byte[] expected = Arrays.copyOfRange(TEST_DATA, position, position + readLength);
CacheAsserts.assertReadData(cacheDataSource, dataSpec, expected);
}
private CacheDataSource createCacheDataSource(boolean setReadException, boolean unknownLength) {
return createCacheDataSource(
setReadException, unknownLength, CacheDataSource.FLAG_BLOCK_ON_CACHE);
}
private CacheDataSource createCacheDataSource(
boolean setReadException, boolean unknownLength, CacheKeyFactory cacheKeyFactory) {
return createCacheDataSource(
setReadException,
unknownLength,
CacheDataSource.FLAG_BLOCK_ON_CACHE,
new CacheDataSink(cache, CACHE_FRAGMENT_SIZE),
cacheKeyFactory);
}
private CacheDataSource createCacheDataSource(
boolean setReadException, boolean unknownLength, @CacheDataSource.Flags int flags) {
return createCacheDataSource(
setReadException, unknownLength, flags, new CacheDataSink(cache, CACHE_FRAGMENT_SIZE));
}
private CacheDataSource createCacheDataSource(
boolean setReadException,
boolean unknownLength,
@CacheDataSource.Flags int flags,
CacheDataSink cacheWriteDataSink) {
return createCacheDataSource(
setReadException, unknownLength, flags, cacheWriteDataSink, /* cacheKeyFactory= */ null);
}
private CacheDataSource createCacheDataSource(
boolean setReadException,
boolean unknownLength,
@CacheDataSource.Flags int flags,
CacheDataSink cacheWriteDataSink,
CacheKeyFactory cacheKeyFactory) {
FakeData fakeData =
upstreamDataSource
.getDataSet()
.newDefaultData()
.setSimulateUnknownLength(unknownLength)
.appendReadData(TEST_DATA);
if (setReadException) {
fakeData.appendReadError(new IOException("Shouldn't read from upstream"));
}
return new CacheDataSource(
cache,
upstreamDataSource,
new FileDataSource(),
cacheWriteDataSink,
flags,
/* eventListener= */ null,
cacheKeyFactory);
}
private DataSpec buildDataSpec(boolean unbounded, @Nullable String key) {
return buildDataSpec(/* position= */ 0, unbounded ? C.LENGTH_UNSET : TEST_DATA.length, key);
}
private DataSpec buildDataSpec(long position, long length) {
return buildDataSpec(position, length, /* key= */ null);
}
private DataSpec buildDataSpec(long position, long length, @Nullable String key) {
return new DataSpec.Builder()
.setUri(testDataUri)
.setPosition(position)
.setLength(length)
.setKey(key)
.setFlags(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION)
.setHttpRequestHeaders(httpRequestHeaders)
.build();
}
}